Skip to content

Commit 1f88146

Browse files
authored
✨ feat : 재연결 로직 구현
* ♻️ destination 생성 위치 변경 * chore: Java 스타일 수정 * ✨ subscribe 이벤트 리스너 구현 * ♻️ 재연결 중간 커밋 * ✨ 재연결 로직 완료 * chore: Java 스타일 수정 * ♻️ 테스트값 원복 * ♻️ 재입장 시 기본 방 세팅 값 브로드캐스트 * chore: Java 스타일 수정 * ♻️ 중복검사 제거 * ♻️ remove 위치 변경 * chore: Java 스타일 수정 * ♻️ test 수정 * ♻️ NPE 보완 * ♻️ RoomEventType reconnect 추가 * chore: Java 스타일 수정 * ♻️ 리뷰내용 반영 * chore: Java 스타일 수정 * ♻️ 리뷰내용 반영 * chore: Java 스타일 수정 * ♻️ 패키지 소문자변경 * ♻️ 오류 해결 * chore: Java 스타일 수정 * ♻️ 리뷰 반영 - util 추가, 메소드명 수정, 널세이프수정 * chore: Java 스타일 수정 * 🐛 oldSession 관리 순서 변경 * ♻️ 테스트 수정 * chore: Java 스타일 수정 * ♻️ 리뷰 반영 s 삭제 * chore: Java 스타일 수정 * ♻️ 재입장시 connect 상태 변경, 응답 추가 * ♻️ 스레드 풀 1-> 2 변경 * chore: Java 스타일 수정 * ✅ test 코드추가 * chore: Java 스타일 수정 --------- Co-authored-by: github-actions <>
1 parent a07ffb8 commit 1f88146

File tree

18 files changed

+565
-50
lines changed

18 files changed

+565
-50
lines changed

backend/src/main/java/io/f1/backend/domain/auth/dto/CurrentUserAndAdminResponse.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@
33
import io.f1.backend.domain.admin.dto.AdminPrincipal;
44
import io.f1.backend.domain.user.dto.UserPrincipal;
55

6-
public record CurrentUserAndAdminResponse(Long id, String name, String role) {
6+
public record CurrentUserAndAdminResponse(Long id, String name, String role, String providerId) {
77

88
public static CurrentUserAndAdminResponse from(UserPrincipal userPrincipal) {
99
return new CurrentUserAndAdminResponse(
1010
userPrincipal.getUserId(),
1111
userPrincipal.getUserNickname(),
12-
UserPrincipal.ROLE_USER);
12+
UserPrincipal.ROLE_USER,
13+
userPrincipal.getName());
1314
}
1415

1516
public static CurrentUserAndAdminResponse from(AdminPrincipal adminPrincipal) {
1617
return new CurrentUserAndAdminResponse(
1718
adminPrincipal.getAuthenticationAdmin().adminId(),
1819
adminPrincipal.getUsername(),
19-
AdminPrincipal.ROLE_ADMIN);
20+
AdminPrincipal.ROLE_ADMIN,
21+
null);
2022
}
2123
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
@RequiredArgsConstructor
3333
public class GameService {
3434

35-
private static final int START_DELAY = 5;
35+
public static final int START_DELAY = 5;
3636

3737
private final MessageSender messageSender;
3838
private final TimerService timerService;

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

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.f1.backend.domain.game.app;
22

3+
import static io.f1.backend.domain.game.app.GameService.START_DELAY;
34
import static io.f1.backend.domain.game.mapper.RoomMapper.ofPlayerEvent;
45
import static io.f1.backend.domain.game.mapper.RoomMapper.toGameSetting;
56
import static io.f1.backend.domain.game.mapper.RoomMapper.toGameSettingResponse;
@@ -11,6 +12,7 @@
1112
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSetting;
1213
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSettingResponse;
1314
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;
15+
import static io.f1.backend.domain.quiz.mapper.QuizMapper.toGameStartResponse;
1416
import static io.f1.backend.global.util.SecurityUtils.getCurrentUserId;
1517
import static io.f1.backend.global.util.SecurityUtils.getCurrentUserNickname;
1618

@@ -27,6 +29,7 @@
2729
import io.f1.backend.domain.game.dto.response.RoomSettingResponse;
2830
import io.f1.backend.domain.game.dto.response.SystemNoticeResponse;
2931
import io.f1.backend.domain.game.event.RoomCreatedEvent;
32+
import io.f1.backend.domain.game.model.ConnectionState;
3033
import io.f1.backend.domain.game.model.GameSetting;
3134
import io.f1.backend.domain.game.model.Player;
3235
import io.f1.backend.domain.game.model.Room;
@@ -70,8 +73,6 @@ public class RoomService {
7073

7174
private static final int CONTINUE_DELAY = 3;
7275

73-
private static final String PENDING_SESSION_ID = "PENDING_SESSION_ID";
74-
7576
public RoomCreateResponse saveRoom(RoomCreateRequest request) {
7677

7778
QuizMinData quizMinData = quizService.getQuizMinData();
@@ -88,7 +89,7 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request) {
8889

8990
Room room = new Room(newId, roomSetting, gameSetting, host);
9091

91-
room.getUserIdSessionMap().put(host.id, PENDING_SESSION_ID);
92+
room.addValidatedUserId(getCurrentUserId());
9293

9394
roomRepository.saveRoom(room);
9495

@@ -111,7 +112,7 @@ public void enterRoom(RoomValidationRequest request) {
111112
}
112113

113114
int maxUserCnt = room.getRoomSetting().maxUserCount();
114-
int currentCnt = room.getUserIdSessionMap().size();
115+
int currentCnt = room.getCurrentUserCnt();
115116
if (maxUserCnt == currentCnt) {
116117
throw new CustomException(RoomErrorCode.ROOM_USER_LIMIT_REACHED);
117118
}
@@ -121,7 +122,7 @@ public void enterRoom(RoomValidationRequest request) {
121122
throw new CustomException(RoomErrorCode.WRONG_PASSWORD);
122123
}
123124

124-
room.getUserIdSessionMap().put(getCurrentUserId(), PENDING_SESSION_ID);
125+
room.addValidatedUserId(getCurrentUserId());
125126
}
126127
}
127128

@@ -131,19 +132,7 @@ public void initializeRoomSocket(Long roomId, String sessionId, UserPrincipal pr
131132

132133
Player player = createPlayer(principal);
133134

134-
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
135-
Map<Long, String> userIdSessionMap = room.getUserIdSessionMap();
136-
137-
if (room.isHost(player.getId())) {
138-
player.toggleReady();
139-
}
140-
141-
playerSessionMap.put(sessionId, player);
142-
String existingSession = userIdSessionMap.get(player.getId());
143-
/* 정상 흐름 or 재연결 */
144-
if (existingSession.equals(PENDING_SESSION_ID) || !existingSession.equals(sessionId)) {
145-
userIdSessionMap.put(player.getId(), sessionId);
146-
}
135+
room.addPlayer(sessionId, player);
147136

148137
RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);
149138

@@ -175,8 +164,10 @@ public void exitRoom(Long roomId, String sessionId, UserPrincipal principal) {
175164

176165
Player removePlayer = getRemovePlayer(room, sessionId, principal);
177166

167+
String destination = getDestination(roomId);
168+
178169
/* 방 삭제 */
179-
if (isLastPlayer(room, sessionId)) {
170+
if (room.isLastPlayer(sessionId)) {
180171
removeRoom(room);
181172
return;
182173
}
@@ -194,8 +185,6 @@ public void exitRoom(Long roomId, String sessionId, UserPrincipal principal) {
194185

195186
PlayerListResponse playerListResponse = toPlayerListResponse(room);
196187

197-
String destination = getDestination(roomId);
198-
199188
messageSender.send(destination, MessageType.PLAYER_LIST, playerListResponse);
200189
messageSender.send(destination, MessageType.SYSTEM_NOTICE, systemNoticeResponse);
201190
}
@@ -280,6 +269,61 @@ public void chat(Long roomId, String sessionId, ChatMessage chatMessage) {
280269
}
281270
}
282271

272+
public void reconnectSession(
273+
Long roomId, String oldSessionId, String newSessionId, UserPrincipal principal) {
274+
Room room = findRoom(roomId);
275+
room.reconnectSession(oldSessionId, newSessionId);
276+
277+
String destination = getDestination(roomId);
278+
279+
messageSender.send(
280+
destination,
281+
MessageType.SYSTEM_NOTICE,
282+
ofPlayerEvent(principal.getUserNickname(), RoomEventType.RECONNECT));
283+
284+
if (room.isPlaying()) {
285+
// todo 랭킹 리스트 추가
286+
messageSender.send(
287+
destination, MessageType.GAME_START, toGameStartResponse(room.getQuestions()));
288+
messageSender.send(
289+
destination,
290+
MessageType.QUESTION_START,
291+
toQuestionStartResponse(room, START_DELAY));
292+
} else {
293+
RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);
294+
295+
Long quizId = room.getGameSetting().getQuizId();
296+
297+
Quiz quiz = quizService.getQuizWithQuestionsById(quizId);
298+
299+
GameSettingResponse gameSettingResponse =
300+
toGameSettingResponse(room.getGameSetting(), quiz);
301+
302+
PlayerListResponse playerListResponse = toPlayerListResponse(room);
303+
304+
messageSender.send(destination, MessageType.ROOM_SETTING, roomSettingResponse);
305+
messageSender.send(destination, MessageType.GAME_SETTING, gameSettingResponse);
306+
messageSender.send(destination, MessageType.PLAYER_LIST, playerListResponse);
307+
}
308+
}
309+
310+
public void changeConnectedStatus(Long roomId, String sessionId, ConnectionState newState) {
311+
Room room = findRoom(roomId);
312+
room.updatePlayerConnectionState(sessionId, newState);
313+
}
314+
315+
public boolean isExit(String sessionId, Long roomId) {
316+
Room room = findRoom(roomId);
317+
return room.isExit(sessionId);
318+
}
319+
320+
public void exitIfNotPlaying(Long roomId, String sessionId, UserPrincipal principal) {
321+
Room room = findRoom(roomId);
322+
if (!room.isPlaying()) {
323+
exitRoom(roomId, sessionId, principal);
324+
}
325+
}
326+
283327
private Player getRemovePlayer(Room room, String sessionId, UserPrincipal principal) {
284328
Player removePlayer = room.getPlayerSessionMap().get(sessionId);
285329
if (removePlayer == null) {
@@ -303,11 +347,6 @@ private Room findRoom(Long roomId) {
303347
.orElseThrow(() -> new CustomException(RoomErrorCode.ROOM_NOT_FOUND));
304348
}
305349

306-
private boolean isLastPlayer(Room room, String sessionId) {
307-
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
308-
return playerSessionMap.size() == 1 && playerSessionMap.containsKey(sessionId);
309-
}
310-
311350
private void removeRoom(Room room) {
312351
Long roomId = room.getId();
313352
roomRepository.removeRoom(roomId);
@@ -319,8 +358,10 @@ private void changeHost(Room room, String hostSessionId) {
319358
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
320359

321360
Optional<String> nextHostSessionId =
322-
playerSessionMap.keySet().stream()
323-
.filter(key -> !key.equals(hostSessionId))
361+
playerSessionMap.entrySet().stream()
362+
.filter(entry -> !entry.getKey().equals(hostSessionId))
363+
.filter(entry -> entry.getValue().getState() == ConnectionState.CONNECTED)
364+
.map(Map.Entry::getKey)
324365
.findFirst();
325366

326367
Player nextHost =
@@ -335,5 +376,6 @@ private void changeHost(Room room, String hostSessionId) {
335376
private void removePlayer(Room room, String sessionId, Player removePlayer) {
336377
room.removeUserId(removePlayer.getId());
337378
room.removeSessionId(sessionId);
379+
room.removeValidatedUserId(removePlayer.getId());
338380
}
339381
}

backend/src/main/java/io/f1/backend/domain/game/dto/RoomEventType.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ public enum RoomEventType {
66
START(null),
77
END(null),
88
CORRECT_ANSWER(SystemNoticeMessage.CORRECT_ANSWER),
9-
TIMEOUT(SystemNoticeMessage.TIMEOUT);
9+
TIMEOUT(SystemNoticeMessage.TIMEOUT),
10+
RECONNECT(SystemNoticeMessage.RECONNECT);
1011

1112
private final SystemNoticeMessage systemMessage;
1213

backend/src/main/java/io/f1/backend/domain/game/dto/SystemNoticeMessage.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ public enum SystemNoticeMessage {
44
ENTER(" 님이 입장하셨습니다"),
55
EXIT(" 님이 퇴장하셨습니다"),
66
CORRECT_ANSWER(" 님 정답입니다 !"),
7-
TIMEOUT("땡 ~ ⏰ 제한 시간 초과!");
7+
TIMEOUT("땡 ~ ⏰ 제한 시간 초과!"),
8+
RECONNECT(" 님이 재연결 되었습니다.");
89

910
private final String message;
1011

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,8 @@ public static SystemNoticeResponse ofPlayerEvent(String nickname, RoomEventType
9090
return new SystemNoticeResponse(roomEventType.getMessage(nickname), Instant.now());
9191
}
9292

93-
public static QuestionResultResponse toQuestionResultResponse(
94-
String correctUser, String answer) {
95-
return new QuestionResultResponse(correctUser, answer);
93+
public static QuestionResultResponse toQuestionResultResponse(String nickname, String answer) {
94+
return new QuestionResultResponse(nickname, answer);
9695
}
9796

9897
public static RankUpdateResponse toRankUpdateResponse(Room room) {

backend/src/main/java/io/f1/backend/domain/game/model/Player.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,8 @@ public void toggleReady() {
2727
public void increaseCorrectCount() {
2828
correctCount++;
2929
}
30+
31+
public void updateState(ConnectionState newState) {
32+
state = newState;
33+
}
3034
}

backend/src/main/java/io/f1/backend/domain/game/model/Room.java

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package io.f1.backend.domain.game.model;
22

33
import io.f1.backend.domain.question.entity.Question;
4+
import io.f1.backend.global.exception.CustomException;
5+
import io.f1.backend.global.exception.errorcode.RoomErrorCode;
46

57
import lombok.Getter;
68

79
import java.time.LocalDateTime;
810
import java.util.ArrayList;
11+
import java.util.HashSet;
912
import java.util.List;
1013
import java.util.Map;
14+
import java.util.Set;
1115
import java.util.concurrent.ConcurrentHashMap;
1216
import java.util.concurrent.Executors;
1317
import java.util.concurrent.ScheduledExecutorService;
@@ -30,7 +34,7 @@ public class Room {
3034

3135
private Map<String, Player> playerSessionMap = new ConcurrentHashMap<>();
3236

33-
private Map<Long, String> userIdSessionMap = new ConcurrentHashMap<>();
37+
private final Set<Long> validatedUserIds = new HashSet<>();
3438

3539
private final LocalDateTime createdAt = LocalDateTime.now();
3640

@@ -47,6 +51,26 @@ public Room(Long id, RoomSetting roomSetting, GameSetting gameSetting, Player ho
4751
this.host = host;
4852
}
4953

54+
public void addValidatedUserId(Long userId) {
55+
validatedUserIds.add(userId);
56+
}
57+
58+
public int getCurrentUserCnt() {
59+
return validatedUserIds.size();
60+
}
61+
62+
public void addPlayer(String sessionId, Player player) {
63+
Long userId = player.getId();
64+
if (!validatedUserIds.contains(userId)) {
65+
throw new CustomException(RoomErrorCode.ROOM_ENTER_REQUIRED);
66+
}
67+
68+
if (isHost(userId)) {
69+
player.toggleReady();
70+
}
71+
playerSessionMap.put(sessionId, player);
72+
}
73+
5074
public boolean isHost(Long id) {
5175
return this.host.getId().equals(id);
5276
}
@@ -67,14 +91,18 @@ public void updateTimer(ScheduledFuture<?> timer) {
6791
this.timer = timer;
6892
}
6993

70-
public void removeUserId(Long id) {
71-
this.userIdSessionMap.remove(id);
72-
}
73-
7494
public void removeSessionId(String sessionId) {
7595
this.playerSessionMap.remove(sessionId);
7696
}
7797

98+
public void removeValidatedUserId(Long userId) {
99+
validatedUserIds.remove(userId);
100+
}
101+
102+
public void removeUserId(Long userId) {
103+
validatedUserIds.remove(userId);
104+
}
105+
78106
public void increasePlayerCorrectCount(String sessionId) {
79107
this.playerSessionMap.get(sessionId).increaseCorrectCount();
80108
}
@@ -83,11 +111,34 @@ public Question getCurrentQuestion() {
83111
return questions.get(currentRound - 1);
84112
}
85113

86-
public Boolean isPlaying() {
114+
public boolean isPlaying() {
87115
return state == RoomState.PLAYING;
88116
}
89117

90118
public void increaseCurrentRound() {
91119
currentRound++;
92120
}
121+
122+
public void reconnectSession(String oldSessionId, String newSessionId) {
123+
Player player = playerSessionMap.get(oldSessionId);
124+
removeSessionId(oldSessionId);
125+
player.updateState(ConnectionState.CONNECTED);
126+
playerSessionMap.put(newSessionId, player);
127+
}
128+
129+
public void updatePlayerConnectionState(String sessionId, ConnectionState newState) {
130+
playerSessionMap.get(sessionId).updateState(newState);
131+
}
132+
133+
public boolean isExit(String sessionId) {
134+
return playerSessionMap.get(sessionId) == null;
135+
}
136+
137+
public boolean isLastPlayer(String sessionId) {
138+
long connectedCount =
139+
playerSessionMap.values().stream()
140+
.filter(player -> player.getState() == ConnectionState.CONNECTED)
141+
.count();
142+
return connectedCount == 1 && playerSessionMap.containsKey(sessionId);
143+
}
93144
}

backend/src/main/java/io/f1/backend/domain/game/websocket/WebSocketUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ public static UserPrincipal getSessionUser(Message<?> message) {
2222
public static String getDestination(Long roomId) {
2323
return "/sub/room/" + roomId;
2424
}
25+
26+
public static String getRoomSubscriptionDestination(Message<?> message) {
27+
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);
28+
return headerAccessor.getDestination();
29+
}
2530
}

0 commit comments

Comments
 (0)