Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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 @@ -25,6 +25,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 @@ -81,7 +82,7 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request) {

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

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

roomRepository.saveRoom(room);

Expand All @@ -104,7 +105,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 @@ -114,7 +115,7 @@ public void enterRoom(RoomValidationRequest request) {
throw new CustomException(RoomErrorCode.WRONG_PASSWORD);
}

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

Expand All @@ -124,19 +125,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(getCurrentUserId(), sessionId, player);

RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);

Expand Down Expand Up @@ -168,6 +157,8 @@ public void exitRoom(Long roomId, String sessionId, UserPrincipal principal) {

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

String destination = getDestination(roomId);

/* 방 삭제 */
if (isLastPlayer(room, sessionId)) {
removeRoom(room);
Expand All @@ -187,8 +178,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 @@ -255,6 +244,60 @@ public void chat(Long roomId, String sessionId, ChatMessage chatMessage) {
}
}

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

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

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);
}
}

// todo 브로드캐스팅 말고 개인메세지
public void notifyIfReconnected(Long roomId, UserPrincipal principal) {

Room room = findRoom(roomId);

String destination = getDestination(roomId);

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);

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

private Player getRemovePlayer(Room room, String sessionId, UserPrincipal principal) {
Player removePlayer = room.getPlayerSessionMap().get(sessionId);
if (removePlayer == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public enum RoomEventType {
EXIT,
START,
END,
RECONNECT,
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ public static SystemNoticeResponse ofPlayerEvent(String nickname, RoomEventType
message = " 님이 입장하셨습니다";
} else if (roomEventType == RoomEventType.EXIT) {
message = " 님이 퇴장하셨습니다";
} else if (roomEventType == RoomEventType.RECONNECT) {
message = " 님이 재연결 되었습니다.";
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분은 은주님 PR에서 SystemNoticeMessage enum으로 분리한 것으로 알고 있습니다. 확인부탁드립니다 ~!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

넵 충돌 해결하면서 수정했습니다! 정리 후에 push하겠습니닷!

}
return new SystemNoticeResponse(nickname + message, Instant.now());
}
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;
}
}
54 changes: 49 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,18 +1,24 @@
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;

@Getter
public class Room {

private static final String PENDING_SESSION_ID = "PENDING_SESSION_ID";

private final Long id;

private final RoomSetting roomSetting;
Expand All @@ -27,7 +33,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 @@ -40,6 +46,26 @@ public Room(Long id, RoomSetting roomSetting, GameSetting gameSetting, Player ho
this.host = host;
}

public boolean addValidatedUserIds(Long userId) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

[L4-변경제안]
해당 메서드는 하나의 userId를 유효한 유저아이디 세트에 넣는 작업이니까 addValidatedUserId로 수정하면 어떨까 싶습니다 !

return validatedUserIds.add(userId);
}

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

public void addPlayer(Long userId, String sessionId, Player player) {

if (!validatedUserIds.contains(userId)) {
throw new CustomException(RoomErrorCode.ROOM_ENTER_REQUIRED);
}

if (isHost(player.getId())) {
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 @@ -56,14 +82,14 @@ public void updateRoomState(RoomState newState) {
this.state = newState;
}

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

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

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

public void increasePlayerCorrectCount(String sessionId) {
this.playerSessionMap.get(sessionId).increaseCorrectCount();
}
Expand All @@ -79,4 +105,22 @@ public Boolean isPlaying() {
public void increaseCorrectCount() {
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 isReconnectTarget(String sessionId) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

[L4-변경제안]
isExit 메서드와 isReconnectTarget은 보수 관계인 것 같습니다.
isExit 메서드로 통합한 이후 isReconnectTarget 사용부를 !isExit()으로 대체 가능할 것 같습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

감사합니다.. 하나로 통일해서 올리겠습니다.

return playerSessionMap.get(sessionId) != null;
}

public boolean isExit(String sessionId) {
return playerSessionMap.get(sessionId) == null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package io.f1.backend.domain.game.websocket.Service;
Copy link
Collaborator

Choose a reason for hiding this comment

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

패키지 이름이 대문자로 시작합니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

진짜 킹받는 오류네요. 바로 수정해서 push했습니다. 👑


import io.f1.backend.domain.game.app.RoomService;
import io.f1.backend.domain.game.model.ConnectionState;
import io.f1.backend.domain.user.dto.UserPrincipal;

import lombok.RequiredArgsConstructor;

import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class SessionService {

private final RoomService roomService;
private final Map<String, Long> sessionIdUser = new ConcurrentHashMap<>();
private final Map<String, Long> sessionIdRoom = new ConcurrentHashMap<>();
private final Map<Long, String> userIdSession = new ConcurrentHashMap<>();
private final Map<Long, String> userIdLatestSession = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

public void addSession(String sessionId, Long userId) {
sessionIdUser.put(sessionId, userId);
userIdSession.put(userId, sessionId);
}

public void addRoomId(Long roomId, String sessionId) {
sessionIdRoom.put(sessionId, roomId);
}

public void handleUserReconnect(Long roomId, String newSessionId, Long userId) {

String oldSessionId = userIdLatestSession.get(userId);
/* room 재연결 대상인지 아닌지 판별 */
if (roomService.isReconnectTarget(roomId, oldSessionId)) {
roomService.reconnectSession(roomId, oldSessionId, newSessionId);
}
}

public void handleUserDisconnect(String sessionId, UserPrincipal principal) {

Long roomId = sessionIdRoom.get(sessionId);

/* 정상 동작*/
if (roomService.isExit(sessionId, roomId)) {
removeSession(sessionId, roomId);
return;
}

Long userId = principal.getUserId();

roomService.changeConnectedStatus(roomId, sessionId, ConnectionState.DISCONNECTED);

// 5초 뒤 실행
scheduler.schedule(
() -> {
if (userIdSession.get(userId).equals(sessionId)) {
roomService.exitIfNotPlaying(roomId, sessionId, principal);
} else {
roomService.notifyIfReconnected(roomId, principal);
}
userIdLatestSession.remove(principal.getUserId());
removeSession(sessionId, roomId);
},
5,
TimeUnit.SECONDS);
}

public void removeSession(String sessionId, Long userId) {
sessionIdUser.remove(sessionId);
sessionIdRoom.remove(sessionId);
userIdSession.remove(userId);

userIdLatestSession.put(userId, sessionId);
}
}
Loading