Skip to content

Commit d4e8f43

Browse files
Merge branch 'dev' into feat/79
2 parents 6444663 + 2d0b0c0 commit d4e8f43

File tree

10 files changed

+168
-11
lines changed

10 files changed

+168
-11
lines changed

backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ public class RoomService {
7575
public RoomCreateResponse saveRoom(RoomCreateRequest request) {
7676

7777
QuizMinData quizMinData = quizService.getQuizMinData();
78+
7879
Quiz quiz = quizService.findQuizById(quizMinData.quizMinId());
7980

8081
GameSetting gameSetting = toGameSetting(quizMinData);
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
package io.f1.backend.domain.game.dto.response;
22

3-
public record RoomSettingResponse(String roomName, int maxUserCount, int currentUserCount) {}
3+
public record RoomSettingResponse(
4+
String roomName, int maxUserCount, int currentUserCount, boolean locked) {}

backend/src/main/java/io/f1/backend/domain/game/mapper/RoomMapper.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public static RoomSettingResponse toRoomSettingResponse(Room room) {
4444
return new RoomSettingResponse(
4545
room.getRoomSetting().roomName(),
4646
room.getRoomSetting().maxUserCount(),
47-
room.getPlayerSessionMap().size());
47+
room.getPlayerSessionMap().size(),
48+
room.getRoomSetting().locked());
4849
}
4950

5051
public static GameSettingResponse toGameSettingResponse(GameSetting gameSetting, Quiz quiz) {

backend/src/main/java/io/f1/backend/domain/stat/api/StatController.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.springframework.data.web.PageableDefault;
1212
import org.springframework.http.ResponseEntity;
1313
import org.springframework.web.bind.annotation.GetMapping;
14+
import org.springframework.web.bind.annotation.PathVariable;
1415
import org.springframework.web.bind.annotation.RequestMapping;
1516
import org.springframework.web.bind.annotation.RestController;
1617

@@ -29,4 +30,13 @@ public ResponseEntity<StatPageResponse> getRankings(
2930

3031
return ResponseEntity.ok().body(response);
3132
}
33+
34+
@LimitPageSize
35+
@GetMapping("/rankings/{nickname}")
36+
public ResponseEntity<StatPageResponse> getRankingsByNickname(
37+
@PathVariable String nickname, @PageableDefault Pageable pageable) {
38+
StatPageResponse response =
39+
statService.getRanksByNickname(nickname, pageable.getPageSize());
40+
return ResponseEntity.ok().body(response);
41+
}
3242
}

backend/src/main/java/io/f1/backend/domain/stat/app/StatService.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,49 @@
55
import io.f1.backend.domain.stat.dao.StatRepository;
66
import io.f1.backend.domain.stat.dto.StatPageResponse;
77
import io.f1.backend.domain.stat.dto.StatWithNickname;
8+
import io.f1.backend.global.exception.CustomException;
9+
import io.f1.backend.global.exception.errorcode.RoomErrorCode;
810

911
import lombok.RequiredArgsConstructor;
1012

1113
import org.springframework.data.domain.Page;
14+
import org.springframework.data.domain.PageRequest;
1215
import org.springframework.data.domain.Pageable;
16+
import org.springframework.data.domain.Sort;
17+
import org.springframework.data.domain.Sort.Direction;
1318
import org.springframework.stereotype.Service;
19+
import org.springframework.transaction.annotation.Transactional;
1420

1521
@Service
1622
@RequiredArgsConstructor
1723
public class StatService {
1824

1925
private final StatRepository statRepository;
2026

27+
@Transactional(readOnly = true)
2128
public StatPageResponse getRanks(Pageable pageable) {
22-
Page<StatWithNickname> stats = statRepository.findWithUser(pageable);
29+
Page<StatWithNickname> stats = statRepository.findAllStatsWithUser(pageable);
2330
return toStatListPageResponse(stats);
2431
}
32+
33+
@Transactional(readOnly = true)
34+
public StatPageResponse getRanksByNickname(String nickname, int pageSize) {
35+
36+
Page<StatWithNickname> stats =
37+
statRepository.findAllStatsWithUser(getPageableFromNickname(nickname, pageSize));
38+
39+
return toStatListPageResponse(stats);
40+
}
41+
42+
private Pageable getPageableFromNickname(String nickname, int pageSize) {
43+
long score =
44+
statRepository
45+
.findScoreByNickname(nickname)
46+
.orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND));
47+
48+
long rowNum = statRepository.countByScoreGreaterThan(score);
49+
50+
int pageNumber = rowNum > 0 ? (int) (rowNum / pageSize) : 0;
51+
return PageRequest.of(pageNumber, pageSize, Sort.by(Direction.DESC, "score"));
52+
}
2553
}

backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.springframework.data.jpa.repository.JpaRepository;
99
import org.springframework.data.jpa.repository.Query;
1010

11+
import java.util.Optional;
12+
1113
public interface StatRepository extends JpaRepository<Stat, Long> {
1214

1315
@Query(
@@ -18,5 +20,10 @@ public interface StatRepository extends JpaRepository<Stat, Long> {
1820
FROM
1921
Stat s JOIN s.user u
2022
""")
21-
Page<StatWithNickname> findWithUser(Pageable pageable);
23+
Page<StatWithNickname> findAllStatsWithUser(Pageable pageable);
24+
25+
@Query("SELECT s.score FROM Stat s WHERE s.user.nickname = :nickname")
26+
Optional<Long> findScoreByNickname(String nickname);
27+
28+
long countByScoreGreaterThan(Long score);
2229
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package io.f1.backend.global.config;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
5+
import org.springframework.http.HttpStatus;
6+
import org.springframework.http.server.ServerHttpRequest;
7+
import org.springframework.http.server.ServerHttpResponse;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.security.core.context.SecurityContextHolder;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.web.socket.WebSocketHandler;
12+
import org.springframework.web.socket.server.HandshakeInterceptor;
13+
14+
import java.util.Map;
15+
16+
@Slf4j
17+
@Component
18+
public class CustomHandshakeInterceptor implements HandshakeInterceptor {
19+
20+
@Override
21+
public boolean beforeHandshake(
22+
ServerHttpRequest request,
23+
ServerHttpResponse response,
24+
WebSocketHandler wsHandler,
25+
Map<String, Object> attributes)
26+
throws Exception {
27+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
28+
if (authentication == null || !authentication.isAuthenticated()) {
29+
response.setStatusCode(HttpStatus.UNAUTHORIZED); // 서버 로그에만 적용되는 StatusCode
30+
return false;
31+
}
32+
33+
attributes.put("auth", authentication);
34+
return true;
35+
}
36+
37+
@Override
38+
public void afterHandshake(
39+
ServerHttpRequest request,
40+
ServerHttpResponse response,
41+
WebSocketHandler wsHandler,
42+
Exception exception) {
43+
// TODO : 연결 이후, 사용자 웹소켓 세션 로그 및 IP 등 추적 및 메트릭 수집 로직 추가
44+
45+
}
46+
}

backend/src/main/java/io/f1/backend/global/config/StompChannelInterceptor.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,21 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
2727
throw new IllegalArgumentException("Stomp command required");
2828
}
2929

30+
String username = "알수없는 사용자";
31+
if (accessor.getUser() != null) {
32+
username = accessor.getUser().getName();
33+
}
34+
3035
if (command.equals(StompCommand.CONNECT)) {
31-
log.info("CONNECT : 세션 연결 - sessionId = {}", sessionId);
36+
log.info("user : {} | CONNECT : 세션 연결 - sessionId = {}", username, sessionId);
3237
} else if (command.equals(StompCommand.SUBSCRIBE)) {
3338
if (destination != null && sessionId != null) {
34-
log.info("SUBSCRIBE : 구독 시작 destination = {}", destination);
39+
log.info("user : {} | SUBSCRIBE : 구독 시작 destination = {}", username, destination);
3540
}
3641
} else if (command.equals(StompCommand.SEND)) {
37-
log.info("SEND : 요청 destination = {}", destination);
42+
log.info("user : {} | SEND : 요청 destination = {}", username, destination);
3843
} else if (command.equals(StompCommand.DISCONNECT)) {
39-
log.info("DISCONNECT : 연결 해제 sessionId = {}", sessionId);
44+
log.info("user : {} | DISCONNECT : 연결 해제 sessionId = {}", username, sessionId);
4045
}
4146

4247
return message;

backend/src/main/java/io/f1/backend/global/config/WebSocketConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@
88
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
99
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
1010
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
11-
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
1211

1312
@Configuration
1413
@RequiredArgsConstructor
1514
@EnableWebSocketMessageBroker
1615
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
1716

1817
private final StompChannelInterceptor stompChannelInterceptor;
18+
private final CustomHandshakeInterceptor customHandshakeInterceptor;
1919

2020
@Override
2121
public void registerStompEndpoints(StompEndpointRegistry registry) {
2222
registry.addEndpoint("/ws/game-room")
23-
.addInterceptors(new HttpSessionHandshakeInterceptor())
23+
.addInterceptors(customHandshakeInterceptor)
2424
.setAllowedOriginPatterns("*");
2525
}
2626

backend/src/test/java/io/f1/backend/domain/stat/StatBrowserTest.java

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.f1.backend.domain.stat;
22

33
import static io.f1.backend.global.exception.errorcode.CommonErrorCode.INVALID_PAGINATION;
4+
import static io.f1.backend.global.exception.errorcode.RoomErrorCode.PLAYER_NOT_FOUND;
45

56
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
67
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -35,7 +36,7 @@ void totalRankingForSingleUser() throws Exception {
3536

3637
@Test
3738
@DisplayName("100을 넘는 페이지 크기 요청이 오면 예외를 발생시킨다")
38-
void totalRankingForSingleUserWithInvalidPageSize() throws Exception {
39+
void totalRankingWithInvalidPageSize() throws Exception {
3940
// when
4041
ResultActions result = mockMvc.perform(get("/stats/rankings").param("size", "101"));
4142

@@ -86,4 +87,61 @@ void totalRankingForThreeUserWithPageSize2() throws Exception {
8687
jsonPath("$.totalElements").value(1),
8788
jsonPath("$.ranks.length()").value(1));
8889
}
90+
91+
@Test
92+
@DataSet("datasets/stat/three-user-stat.yml")
93+
@DisplayName("랭킹 페이지에서 존재하지 않는 닉네임을 검색하면 예외를 발생시킨다.")
94+
void totalRankingWithUnregisteredNickname() throws Exception {
95+
// given
96+
String nickname = "UNREGISTERED";
97+
98+
// when
99+
ResultActions result = mockMvc.perform(get("/stats/rankings/" + nickname));
100+
101+
// then
102+
result.andExpectAll(
103+
status().isNotFound(), jsonPath("$.code").value(PLAYER_NOT_FOUND.getCode()));
104+
}
105+
106+
@Test
107+
@DataSet("datasets/stat/three-user-stat.yml")
108+
@DisplayName("총 유저 수가 3명이고 페이지 크기가 2일 때 1위 유저의 닉네임을 검색하면 첫 번째 페이지에 2개의 결과를 반환한다")
109+
void totalRankingForThreeUserWithFirstRankedNickname() throws Exception {
110+
// given
111+
String nickname = "USER3";
112+
113+
// when
114+
ResultActions result =
115+
mockMvc.perform(get("/stats/rankings/" + nickname).param("size", "2"));
116+
117+
// then
118+
result.andExpectAll(
119+
status().isOk(),
120+
jsonPath("$.totalPages").value(2),
121+
jsonPath("$.currentPage").value(1),
122+
jsonPath("$.totalElements").value(2),
123+
jsonPath("$.ranks.length()").value(2),
124+
jsonPath("$.ranks[0].nickname").value(nickname));
125+
}
126+
127+
@Test
128+
@DataSet("datasets/stat/three-user-stat.yml")
129+
@DisplayName("총 유저 수가 3명이고 페이지 크기가 2일 때 3위 유저의 닉네임을 검색하면 두 번째 페이지에 1개의 결과를 반환한다")
130+
void totalRankingForThreeUserWithLastRankedNickname() throws Exception {
131+
// given
132+
String nickname = "USER1";
133+
134+
// when
135+
ResultActions result =
136+
mockMvc.perform(get("/stats/rankings/" + nickname).param("size", "2"));
137+
138+
// then
139+
result.andExpectAll(
140+
status().isOk(),
141+
jsonPath("$.totalPages").value(2),
142+
jsonPath("$.currentPage").value(2),
143+
jsonPath("$.totalElements").value(1),
144+
jsonPath("$.ranks.length()").value(1),
145+
jsonPath("$.ranks[0].nickname").value(nickname));
146+
}
89147
}

0 commit comments

Comments
 (0)