Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d32fa2c
:recycle: destination 생성 위치 변경
sehee123 Jul 17, 2025
4a87b5b
chore: Java 스타일 수정
Jul 17, 2025
2ebc45b
:sparkles: subscribe 이벤트 리스너 구현
sehee123 Jul 17, 2025
22e2dce
Merge remote-tracking branch 'origin/dev' into feat/34
sehee123 Jul 18, 2025
211a772
Merge remote-tracking branch 'origin/dev' into feat/34
sehee123 Jul 19, 2025
fb25877
:recycle: 재연결 중간 커밋
sehee123 Jul 21, 2025
7be49b7
Merge remote-tracking branch 'origin/dev' into feat/34
sehee123 Jul 21, 2025
51efbed
:sparkles: 재연결 로직 완료
sehee123 Jul 21, 2025
5ba8a13
chore: Java 스타일 수정
Jul 21, 2025
07da462
:recycle: 테스트값 원복
sehee123 Jul 21, 2025
6ce1897
:recycle: 재입장 시 기본 방 세팅 값 브로드캐스트
sehee123 Jul 21, 2025
527a6c5
chore: Java 스타일 수정
Jul 21, 2025
bde0694
:recycle: 중복검사 제거
sehee123 Jul 21, 2025
bdc1700
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 21, 2025
4abe282
:recycle: remove 위치 변경
sehee123 Jul 21, 2025
849ef71
chore: Java 스타일 수정
Jul 21, 2025
bc3fd2b
:recycle: test 수정
sehee123 Jul 21, 2025
435d8f0
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 21, 2025
05b3c60
Merge remote-tracking branch 'origin/dev' into feat/34
sehee123 Jul 21, 2025
781c283
:recycle: NPE 보완
sehee123 Jul 21, 2025
fd9baf5
:recycle: RoomEventType reconnect 추가
sehee123 Jul 21, 2025
fd9aff1
chore: Java 스타일 수정
Jul 21, 2025
7bd4453
:recycle: 리뷰내용 반영
sehee123 Jul 21, 2025
e3dcb00
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 21, 2025
15bfbab
chore: Java 스타일 수정
Jul 21, 2025
700c734
:recycle: 리뷰내용 반영
sehee123 Jul 21, 2025
4af5a2c
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 21, 2025
147ab05
chore: Java 스타일 수정
Jul 21, 2025
31d1f76
:recycle: 패키지 소문자변경
sehee123 Jul 21, 2025
1b54ded
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 21, 2025
357443d
Merge remote-tracking branch 'origin/dev' into feat/34
sehee123 Jul 21, 2025
6061c87
:recycle: 오류 해결
sehee123 Jul 21, 2025
cb1f75d
chore: Java 스타일 수정
Jul 21, 2025
3914981
:recycle: 리뷰 반영 - util 추가, 메소드명 수정, 널세이프수정
sehee123 Jul 22, 2025
e8616f4
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 22, 2025
f1dd3a2
chore: Java 스타일 수정
Jul 22, 2025
ba3e466
:bug: oldSession 관리 순서 변경
sehee123 Jul 22, 2025
a568032
:recycle: 테스트 수정
sehee123 Jul 22, 2025
e25f2f2
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 22, 2025
cce37c4
chore: Java 스타일 수정
Jul 22, 2025
3aef18f
:recycle: 리뷰 반영 s 삭제
sehee123 Jul 22, 2025
6a5ad37
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 22, 2025
c506712
chore: Java 스타일 수정
Jul 22, 2025
5a1b865
:recycle: 재입장시 connect 상태 변경, 응답 추가
sehee123 Jul 22, 2025
6424848
:recycle: 스레드 풀 1-> 2 변경
sehee123 Jul 22, 2025
0e644b7
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 22, 2025
b408205
chore: Java 스타일 수정
Jul 22, 2025
cbb9a72
Merge remote-tracking branch 'origin/dev' into feat/34
sehee123 Jul 22, 2025
fc37e91
Merge remote-tracking branch 'origin/feat/34' into feat/34
sehee123 Jul 22, 2025
f9606ba
:white_check_mark: test 코드추가
sehee123 Jul 23, 2025
83544c5
chore: Java 스타일 수정
Jul 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@
import io.f1.backend.domain.admin.dto.AdminPrincipal;
import io.f1.backend.domain.user.dto.UserPrincipal;

public record CurrentUserAndAdminResponse(Long id, String name, String role) {
public record CurrentUserAndAdminResponse(Long id, String name, String role, String providerId) {

public static CurrentUserAndAdminResponse from(UserPrincipal userPrincipal) {
return new CurrentUserAndAdminResponse(
userPrincipal.getUserId(),
userPrincipal.getUserNickname(),
UserPrincipal.ROLE_USER);
UserPrincipal.ROLE_USER,
userPrincipal.getName());
Copy link
Collaborator

@jiwon1217 jiwon1217 Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API 명세서에 /auth/me response를 수정했습니다. 해당 PR이 merge가 되면 재연결 로직이 구현됨에 따라 명세서가 수정되었다고 말씀드리면 될 것 같습니다 !

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 재연결때문에 추가한건 아니였고, 추후에 1:1 응답에서 추가될 예정이라서 그부분 구현하고나서 제가 말씀드리겠습니다!
감사합니다.. 💪

}

public static CurrentUserAndAdminResponse from(AdminPrincipal adminPrincipal) {
return new CurrentUserAndAdminResponse(
adminPrincipal.getAuthenticationAdmin().adminId(),
adminPrincipal.getUsername(),
AdminPrincipal.ROLE_ADMIN);
AdminPrincipal.ROLE_ADMIN,
null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSetting;
import static io.f1.backend.domain.game.mapper.RoomMapper.toRoomSettingResponse;
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;
import static io.f1.backend.domain.quiz.mapper.QuizMapper.toGameStartResponse;
import static io.f1.backend.global.util.SecurityUtils.getCurrentUserId;
import static io.f1.backend.global.util.SecurityUtils.getCurrentUserNickname;

Expand All @@ -27,6 +28,7 @@
import io.f1.backend.domain.game.dto.response.RoomSettingResponse;
import io.f1.backend.domain.game.dto.response.SystemNoticeResponse;
import io.f1.backend.domain.game.event.RoomCreatedEvent;
import io.f1.backend.domain.game.model.ConnectionState;
import io.f1.backend.domain.game.model.GameSetting;
import io.f1.backend.domain.game.model.Player;
import io.f1.backend.domain.game.model.Room;
Expand Down Expand Up @@ -88,7 +90,7 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request) {

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

room.getUserIdSessionMap().put(host.id, PENDING_SESSION_ID);
room.addValidatedUserId(getCurrentUserId());

roomRepository.saveRoom(room);

Expand All @@ -111,7 +113,7 @@ public void enterRoom(RoomValidationRequest request) {
}

int maxUserCnt = room.getRoomSetting().maxUserCount();
int currentCnt = room.getUserIdSessionMap().size();
int currentCnt = room.getCurrentUserCnt();
if (maxUserCnt == currentCnt) {
throw new CustomException(RoomErrorCode.ROOM_USER_LIMIT_REACHED);
}
Expand All @@ -121,7 +123,7 @@ public void enterRoom(RoomValidationRequest request) {
throw new CustomException(RoomErrorCode.WRONG_PASSWORD);
}

room.getUserIdSessionMap().put(getCurrentUserId(), PENDING_SESSION_ID);
room.addValidatedUserId(getCurrentUserId());
}
}

Expand All @@ -131,19 +133,7 @@ public void initializeRoomSocket(Long roomId, String sessionId, UserPrincipal pr

Player player = createPlayer(principal);

Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
Map<Long, String> userIdSessionMap = room.getUserIdSessionMap();

if (room.isHost(player.getId())) {
player.toggleReady();
}

playerSessionMap.put(sessionId, player);
String existingSession = userIdSessionMap.get(player.getId());
/* 정상 흐름 or 재연결 */
if (existingSession.equals(PENDING_SESSION_ID) || !existingSession.equals(sessionId)) {
userIdSessionMap.put(player.getId(), sessionId);
}
room.addPlayer(sessionId, player);

RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);

Expand Down Expand Up @@ -175,8 +165,10 @@ public void exitRoom(Long roomId, String sessionId, UserPrincipal principal) {

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

String destination = getDestination(roomId);

/* 방 삭제 */
if (isLastPlayer(room, sessionId)) {
if (room.isLastPlayer(sessionId)) {
removeRoom(room);
return;
}
Expand All @@ -194,8 +186,6 @@ public void exitRoom(Long roomId, String sessionId, UserPrincipal principal) {

PlayerListResponse playerListResponse = toPlayerListResponse(room);

String destination = getDestination(roomId);

messageSender.send(destination, MessageType.PLAYER_LIST, playerListResponse);
messageSender.send(destination, MessageType.SYSTEM_NOTICE, systemNoticeResponse);
}
Expand Down Expand Up @@ -280,6 +270,59 @@ public void chat(Long roomId, String sessionId, ChatMessage chatMessage) {
}
}

public void reconnectSession(
Long roomId, String oldSessionId, String newSessionId, UserPrincipal principal) {
Room room = findRoom(roomId);
room.reconnectSession(oldSessionId, newSessionId);

String destination = getDestination(roomId);

messageSender.send(
destination,
MessageType.SYSTEM_NOTICE,
ofPlayerEvent(principal.getUserNickname(), RoomEventType.RECONNECT));

if (room.isPlaying()) {
// todo 현재 round 및 타이머 ..
// todo 랭킹 리스트
messageSender.send(
destination, MessageType.GAME_START, toGameStartResponse(room.getQuestions()));

} else {

RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);

Long quizId = room.getGameSetting().getQuizId();
Quiz quiz = quizService.getQuizWithQuestionsById(quizId);

GameSettingResponse gameSettingResponse =
toGameSettingResponse(room.getGameSetting(), quiz);

PlayerListResponse playerListResponse = toPlayerListResponse(room);

messageSender.send(destination, MessageType.ROOM_SETTING, roomSettingResponse);
messageSender.send(destination, MessageType.GAME_SETTING, gameSettingResponse);
messageSender.send(destination, MessageType.PLAYER_LIST, playerListResponse);
}
}

public void changeConnectedStatus(Long roomId, String sessionId, ConnectionState newState) {
Room room = findRoom(roomId);
room.updatePlayerConnectionState(sessionId, newState);
}

public boolean isExit(String sessionId, Long roomId) {
Room room = findRoom(roomId);
return room.isExit(sessionId);
}

public void exitIfNotPlaying(Long roomId, String sessionId, UserPrincipal principal) {
Room room = findRoom(roomId);
if (!room.isPlaying()) {
exitRoom(roomId, sessionId, principal);
}
}

private Player getRemovePlayer(Room room, String sessionId, UserPrincipal principal) {
Player removePlayer = room.getPlayerSessionMap().get(sessionId);
if (removePlayer == null) {
Expand All @@ -303,11 +346,6 @@ private Room findRoom(Long roomId) {
.orElseThrow(() -> new CustomException(RoomErrorCode.ROOM_NOT_FOUND));
}

private boolean isLastPlayer(Room room, String sessionId) {
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
return playerSessionMap.size() == 1 && playerSessionMap.containsKey(sessionId);
}

private void removeRoom(Room room) {
Long roomId = room.getId();
roomRepository.removeRoom(roomId);
Expand All @@ -319,8 +357,10 @@ private void changeHost(Room room, String hostSessionId) {
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();

Optional<String> nextHostSessionId =
playerSessionMap.keySet().stream()
.filter(key -> !key.equals(hostSessionId))
playerSessionMap.entrySet().stream()
.filter(entry -> !entry.getKey().equals(hostSessionId))
.filter(entry -> entry.getValue().getState() == ConnectionState.CONNECTED)
.map(Map.Entry::getKey)
.findFirst();

Player nextHost =
Expand All @@ -335,5 +375,6 @@ private void changeHost(Room room, String hostSessionId) {
private void removePlayer(Room room, String sessionId, Player removePlayer) {
room.removeUserId(removePlayer.getId());
room.removeSessionId(sessionId);
room.removeValidatedUserIds(removePlayer.getId());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[L5-참고의견]
얘도 아이디 하나를 지워주는 것이니 복수형보다는 removeValidatedUserId 단수형으로 수정되면 좋을 것 같습니다 !

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ public enum RoomEventType {
START(null),
END(null),
CORRECT_ANSWER(SystemNoticeMessage.CORRECT_ANSWER),
TIMEOUT(SystemNoticeMessage.TIMEOUT);
TIMEOUT(SystemNoticeMessage.TIMEOUT),
RECONNECT(SystemNoticeMessage.RECONNECT);

private final SystemNoticeMessage systemMessage;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ public enum SystemNoticeMessage {
ENTER(" 님이 입장하셨습니다"),
EXIT(" 님이 퇴장하셨습니다"),
CORRECT_ANSWER(" 님 정답입니다 !"),
TIMEOUT("땡 ~ ⏰ 제한 시간 초과!");
TIMEOUT("땡 ~ ⏰ 제한 시간 초과!"),
RECONNECT(" 님이 재연결 되었습니다.");

private final String message;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,8 @@ public static SystemNoticeResponse ofPlayerEvent(String nickname, RoomEventType
return new SystemNoticeResponse(roomEventType.getMessage(nickname), Instant.now());
}

public static QuestionResultResponse toQuestionResultResponse(
String correctUser, String answer) {
return new QuestionResultResponse(correctUser, answer);
public static QuestionResultResponse toQuestionResultResponse(String nickname, String answer) {
return new QuestionResultResponse(nickname, answer);
}

public static RankUpdateResponse toRankUpdateResponse(Room room) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ public void toggleReady() {
public void increaseCorrectCount() {
correctCount++;
}

public void updateState(ConnectionState newState) {
state = newState;
}
}
60 changes: 55 additions & 5 deletions backend/src/main/java/io/f1/backend/domain/game/model/Room.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package io.f1.backend.domain.game.model;

import io.f1.backend.domain.question.entity.Question;
import io.f1.backend.global.exception.CustomException;
import io.f1.backend.global.exception.errorcode.RoomErrorCode;

import lombok.Getter;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
Expand All @@ -30,7 +34,7 @@ public class Room {

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

private Map<Long, String> userIdSessionMap = new ConcurrentHashMap<>();
private final Set<Long> validatedUserIds = new HashSet<>();

private final LocalDateTime createdAt = LocalDateTime.now();

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

public void addValidatedUserId(Long userId) {
validatedUserIds.add(userId);
}

public int getCurrentUserCnt() {
return validatedUserIds.size();
}

public void addPlayer(String sessionId, Player player) {
Long userId = player.getId();
if (!validatedUserIds.contains(userId)) {
throw new CustomException(RoomErrorCode.ROOM_ENTER_REQUIRED);
}

if (isHost(userId)) {
player.toggleReady();
}
playerSessionMap.put(sessionId, player);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 로직을 room 내부로 옮기니까 서비스 코드가 간결해지고, 가독성이 좋아진 것 같습니다 ! 👍


public boolean isHost(Long id) {
return this.host.getId().equals(id);
}
Expand All @@ -67,14 +91,18 @@ public void updateTimer(ScheduledFuture<?> timer) {
this.timer = timer;
}

public void removeUserId(Long id) {
this.userIdSessionMap.remove(id);
}

public void removeSessionId(String sessionId) {
this.playerSessionMap.remove(sessionId);
}

public void removeValidatedUserIds(Long userId) {
validatedUserIds.remove(userId);
}

public void removeUserId(Long userId) {
validatedUserIds.remove(userId);
}

public void increasePlayerCorrectCount(String sessionId) {
this.playerSessionMap.get(sessionId).increaseCorrectCount();
}
Expand All @@ -90,4 +118,26 @@ public Boolean isPlaying() {
public void increaseCurrentRound() {
currentRound++;
}

public void reconnectSession(String oldSessionId, String newSessionId) {
Player player = playerSessionMap.get(oldSessionId);
removeSessionId(oldSessionId);
playerSessionMap.put(newSessionId, player);
}

public void updatePlayerConnectionState(String sessionId, ConnectionState newState) {
playerSessionMap.get(sessionId).updateState(newState);
}

public boolean isExit(String sessionId) {
return playerSessionMap.get(sessionId) == null;
}

public boolean isLastPlayer(String sessionId) {
long connectedCount =
playerSessionMap.values().stream()
.filter(player -> player.getState() == ConnectionState.CONNECTED)
.count();
return connectedCount == 1 && playerSessionMap.containsKey(sessionId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ public static UserPrincipal getSessionUser(Message<?> message) {
public static String getDestination(Long roomId) {
return "/sub/room/" + roomId;
}

public static String getRoomSubscriptionDestination(Message<?> message) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);
return headerAccessor.getDestination();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.f1.backend.domain.game.websocket;
package io.f1.backend.domain.game.websocket.controller;

import static io.f1.backend.domain.game.websocket.WebSocketUtils.getSessionId;
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getSessionUser;
Expand All @@ -7,6 +7,7 @@
import io.f1.backend.domain.game.app.RoomService;
import io.f1.backend.domain.game.dto.ChatMessage;
import io.f1.backend.domain.game.dto.request.DefaultWebSocketRequest;
import io.f1.backend.domain.game.websocket.service.SessionService;
import io.f1.backend.domain.user.dto.UserPrincipal;

import lombok.RequiredArgsConstructor;
Expand All @@ -22,15 +23,25 @@ public class GameSocketController {

private final RoomService roomService;
private final GameService gameService;
private final SessionService sessionService;

@MessageMapping("/room/initializeRoomSocket/{roomId}")
public void initializeRoomSocket(@DestinationVariable Long roomId, Message<?> message) {

String websocketSessionId = getSessionId(message);

UserPrincipal principal = getSessionUser(message);

roomService.initializeRoomSocket(roomId, websocketSessionId, principal);
Long userId = principal.getUserId();

if (sessionService.hasOldSessionId(userId)) {
String oldSessionId = sessionService.getOldSessionId(userId);
/* room 재연결 대상인지 아닌지 판별 */
if (!roomService.isExit(oldSessionId, roomId)) {
roomService.reconnectSession(roomId, oldSessionId, websocketSessionId, principal);
}
} else {
roomService.initializeRoomSocket(roomId, websocketSessionId, principal);
}
}

@MessageMapping("/room/exit/{roomId}")
Expand Down
Loading