Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
@@ -1,13 +1,15 @@
package io.f1.backend.domain.game.app;

import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionStartResponse;
import static io.f1.backend.domain.quiz.mapper.QuizMapper.toGameStartResponse;

import io.f1.backend.domain.game.dto.response.GameStartResponse;
import io.f1.backend.domain.game.dto.MessageType;
import io.f1.backend.domain.game.event.RoomUpdatedEvent;
import io.f1.backend.domain.game.model.Player;
import io.f1.backend.domain.game.model.Room;
import io.f1.backend.domain.game.model.RoomState;
import io.f1.backend.domain.game.store.RoomRepository;
import io.f1.backend.domain.game.websocket.MessageSender;
import io.f1.backend.domain.question.entity.Question;
import io.f1.backend.domain.quiz.app.QuizService;
import io.f1.backend.domain.quiz.entity.Quiz;
Expand All @@ -29,11 +31,18 @@
@RequiredArgsConstructor
public class GameService {

private static final int START_DELAY = 5;
private static final int CONTINUE_DELAY = 3;

private final MessageSender messageSender;
private final TimerService timerService;
private final QuizService quizService;
private final RoomRepository roomRepository;
private final ApplicationEventPublisher eventPublisher;

public GameStartResponse gameStart(Long roomId, UserPrincipal principal) {
public void gameStart(Long roomId, UserPrincipal principal) {

String destination = getDestination(roomId);

Room room =
roomRepository
Expand All @@ -47,13 +56,18 @@ public GameStartResponse gameStart(Long roomId, UserPrincipal principal) {
List<Question> questions = prepareQuestions(room, quiz);

room.updateQuestions(questions);

// 방 정보 게임 중으로 변경
room.increaseCurrentRound();
room.updateRoomState(RoomState.PLAYING);

eventPublisher.publishEvent(new RoomUpdatedEvent(room, quiz));

return toGameStartResponse(questions);
timerService.startTimer(room, START_DELAY);

messageSender.send(destination, MessageType.GAME_START, toGameStartResponse(questions));
messageSender.send(
destination,
MessageType.QUESTION_START,
toQuestionStartResponse(room, START_DELAY));
}

private boolean validateReadyStatus(Room room) {
Expand Down Expand Up @@ -83,4 +97,8 @@ private List<Question> prepareQuestions(Room room, Quiz quiz) {
Integer round = room.getGameSetting().getRound();
return quizService.getRandomQuestionsWithoutAnswer(quizId, round);
}

private String getDestination(Long roomId) {
return "/sub/room/" + roomId;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

getDestinatnionpublic static 으로 변경해서 util로 분리하는건 어떨까요?
현재 roomService, gameService에서 중복인 메소드라서요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

util 분리가 좋을 것 같습니다 ! 수정하겠습니다 !

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

흠.. 얘도 WebSocketUtils에서 관리하면 될까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

우선은 WebSocketUtils로 분리해두었습니다 !

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,18 @@
@RequiredArgsConstructor
public class RoomService {

private final TimerService timerService;
private final QuizService quizService;
private final RoomRepository roomRepository;
private final AtomicLong roomIdGenerator = new AtomicLong(0);
private final ApplicationEventPublisher eventPublisher;
private final Map<Long, Object> roomLocks = new ConcurrentHashMap<>();

private final MessageSender messageSender;

private static final int START_DELAY = 5;
private static final int CONTINUE_DELAY = 3;

private static final String PENDING_SESSION_ID = "PENDING_SESSION_ID";

public RoomCreateResponse saveRoom(RoomCreateRequest request) {
Expand Down Expand Up @@ -244,13 +250,26 @@ public void chat(Long roomId, String sessionId, ChatMessage chatMessage) {
messageSender.send(
destination,
MessageType.QUESTION_RESULT,
toQuestionResultResponse(currentQuestion.getId(), chatMessage, answer));
toQuestionResultResponse(chatMessage.nickname(), answer));
messageSender.send(destination, MessageType.RANK_UPDATE, toRankUpdateResponse(room));
messageSender.send(
destination,
MessageType.SYSTEM_NOTICE,
ofPlayerEvent(chatMessage.nickname(), RoomEventType.ENTER));
ofPlayerEvent(chatMessage.nickname(), RoomEventType.CORRECT_ANSWER));
}

timerService.cancelTimer(room);

// TODO : 게임 종료 로직 추가
if (!timerService.validateCurrentRound(room)) {
// 게임 종료 로직
return;
}

room.increaseCurrentRound();

// 타이머 추가하기
timerService.startTimer(room, CONTINUE_DELAY);
Copy link
Collaborator

@sehee123 sehee123 Jul 19, 2025

Choose a reason for hiding this comment

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

261 라인부터 272라인까지 혹시 아래 if문 안에서 실행되는건가요?
if (answer.equals(chatMessage.message())) {
현재로선 메세지 send 후 } 중괄호가 끝난 것 처럼 보입니다!
정답 처리시에만 increaseMessageSend.send를 실행하게 해놓아서 타이머 로직 또한 이 if문에 포함되어야하지 않을까요?

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문이 실행된다고 생각했네요..!! 감사합니다 ! 이 부분도 수정하겠습니다 ! :)

}

private Player getRemovePlayer(Room room, String sessionId, UserPrincipal principal) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.f1.backend.domain.game.app;

import static io.f1.backend.domain.game.mapper.RoomMapper.ofPlayerEvent;
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionResultResponse;
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionStartResponse;

import io.f1.backend.domain.game.dto.MessageType;
import io.f1.backend.domain.game.dto.RoomEventType;
import io.f1.backend.domain.game.model.Room;
import io.f1.backend.domain.game.websocket.MessageSender;

import lombok.RequiredArgsConstructor;

import org.springframework.stereotype.Service;

import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class TimerService {

private final MessageSender messageSender;

private static final String NONE_CORRECT_USER = "";
private static final int CONTINUE_DELAY = 3;

public void startTimer(Room room, int delaySec) {
cancelTimer(room);

ScheduledFuture<?> timer =
room.getScheduler()
.schedule(
() -> {
handleTimeout(room);
},
delaySec + room.getGameSetting().getTimeLimit(),
TimeUnit.SECONDS);
Copy link
Collaborator

Choose a reason for hiding this comment

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

오홍.. 재연결 유예시간 로직 구현할 때 참고해서 구현해봐야겠군요 👀


room.updateTimer(timer);
}

private void handleTimeout(Room room) {
String destination = getDestination(room.getId());

messageSender.send(
destination,
MessageType.QUESTION_RESULT,
toQuestionResultResponse(NONE_CORRECT_USER, room.getCurrentQuestion().getAnswer()));
messageSender.send(
destination,
MessageType.SYSTEM_NOTICE,
ofPlayerEvent(NONE_CORRECT_USER, RoomEventType.TIMEOUT));

// TODO : 게임 종료 로직
if (!validateCurrentRound(room)) {
// 게임 종료 로직
// GAME_SETTING, PLAYER_LIST, GAME_RESULT, ROOM_SETTING
return;
}

// 다음 문제 출제
room.increaseCurrentRound();

startTimer(room, CONTINUE_DELAY);
messageSender.send(
destination,
MessageType.QUESTION_START,
toQuestionStartResponse(room, CONTINUE_DELAY));
}

public boolean validateCurrentRound(Room room) {
if (room.getGameSetting().getRound() != room.getCurrentRound()) {
return true;
}
cancelTimer(room);
room.getScheduler().shutdown();
return false;
}

public void cancelTimer(Room room) {
// 정답 맞혔어요 ~ 타이머 캔슬 부탁
ScheduledFuture<?> timer = room.getTimer();
if (timer != null && !timer.isDone()) {
timer.cancel(false);
}
}

private String getDestination(Long roomId) {
return "/sub/room/" + roomId;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기도 있군요.
확실히 Util로 분리해야할것 같습니다.. 🥕

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.f1.backend.domain.game.dto;

public enum GameEventType {
START,
CONTINUE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.f1.backend.domain.game.dto;

import io.f1.backend.domain.game.dto.response.GameStartResponse;
import io.f1.backend.domain.game.dto.response.QuestionStartResponse;

public record GameStartData(
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.

어머 삭제했는데,, 충돌 해결하면서 제대로 안 사라졌나봅니다... 지우겠습니닷 !

GameStartResponse gameStartResponse, QuestionStartResponse questionStartResponse) {}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public enum MessageType {
CHAT,
QUESTION_RESULT,
RANK_UPDATE,
QUESTION_START
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ public enum RoomEventType {
EXIT,
START,
END,
CORRECT_ANSWER,
TIMEOUT
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package io.f1.backend.domain.game.dto.response;

public record QuestionResultResponse(Long questionId, String correctUser, String answer) {}
public record QuestionResultResponse(String correctUser, String answer) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.f1.backend.domain.game.dto.response;

import java.time.Instant;

public record QuestionStartResponse(Long questionId, int round, Instant timestamp) {}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package io.f1.backend.domain.game.mapper;

import io.f1.backend.domain.game.dto.ChatMessage;
import io.f1.backend.domain.game.dto.Rank;
import io.f1.backend.domain.game.dto.RoomEventType;
import io.f1.backend.domain.game.dto.request.RoomCreateRequest;
import io.f1.backend.domain.game.dto.response.GameSettingResponse;
import io.f1.backend.domain.game.dto.response.PlayerListResponse;
import io.f1.backend.domain.game.dto.response.PlayerResponse;
import io.f1.backend.domain.game.dto.response.QuestionResultResponse;
import io.f1.backend.domain.game.dto.response.QuestionStartResponse;
import io.f1.backend.domain.game.dto.response.QuizResponse;
import io.f1.backend.domain.game.dto.response.RankUpdateResponse;
import io.f1.backend.domain.game.dto.response.RoomResponse;
Expand Down Expand Up @@ -91,13 +91,17 @@ public static SystemNoticeResponse ofPlayerEvent(String nickname, RoomEventType
message = " 님이 입장하셨습니다";
} else if (roomEventType == RoomEventType.EXIT) {
message = " 님이 퇴장하셨습니다";
} else if (roomEventType == RoomEventType.CORRECT_ANSWER) {
message = " 님 정답입니다 !";
} else if (roomEventType == RoomEventType.TIMEOUT) {
message = "땡 ~ ⏰ 제한 시간 초과!";
Copy link
Collaborator

Choose a reason for hiding this comment

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

공지 메시지로 쓰이는 문자열을 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.

확인했습니다 ! 이 부분 수정해보겠습니다 !

}
return new SystemNoticeResponse(nickname + message, Instant.now());
}

public static QuestionResultResponse toQuestionResultResponse(
Long questionId, ChatMessage chatMessage, String answer) {
return new QuestionResultResponse(questionId, chatMessage.nickname(), answer);
String correctUser, String answer) {
return new QuestionResultResponse(correctUser, answer);
}

public static RankUpdateResponse toRankUpdateResponse(Room room) {
Expand All @@ -107,4 +111,11 @@ public static RankUpdateResponse toRankUpdateResponse(Room room) {
.map(player -> new Rank(player.getNickname(), player.getCorrectCount()))
.toList());
}

public static QuestionStartResponse toQuestionStartResponse(Room room, int delay) {
return new QuestionStartResponse(
room.getCurrentQuestion().getId(),
room.getCurrentRound(),
Instant.now().plusSeconds(delay));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class GameSetting {

private Long quizId;
private Integer round; // 게임 변경 시 해당 게임의 총 문제 수로 설정
private int timeLimit = 60;
Copy link
Collaborator

Choose a reason for hiding this comment

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

이부분 전 캐치 못하고있었는뎁 감사합니다 ~!

private int timeLimit;

public boolean validateQuizId(Long quizId) {
return Objects.equals(this.quizId, quizId);
Expand Down
13 changes: 12 additions & 1 deletion backend/src/main/java/io/f1/backend/domain/game/model/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;

@Getter
public class Room {
Expand All @@ -33,6 +36,10 @@ public class Room {

private int currentRound = 0;

private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

private ScheduledFuture<?> timer;

public Room(Long id, RoomSetting roomSetting, GameSetting gameSetting, Player host) {
this.id = id;
this.roomSetting = roomSetting;
Expand All @@ -56,6 +63,10 @@ public void updateRoomState(RoomState newState) {
this.state = newState;
}

public void updateTimer(ScheduledFuture<?> timer) {
this.timer = timer;
}

public void removeUserId(Long id) {
this.userIdSessionMap.remove(id);
}
Expand All @@ -76,7 +87,7 @@ public Boolean isPlaying() {
return state == RoomState.PLAYING;
}

public void increaseCorrectCount() {
public void increaseCurrentRound() {
currentRound++;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
@RequiredArgsConstructor
public class GameSocketController {

// todo 삭제
private final MessageSender messageSender;
private final RoomService roomService;
private final GameService gameService;

Expand Down Expand Up @@ -65,9 +63,4 @@ public void playerReady(@DestinationVariable Long roomId, Message<?> message) {

roomService.handlePlayerReady(roomId, getSessionId(message));
}

// todo 삭제
private String getDestination(Long roomId) {
return "/sub/room/" + roomId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,16 @@ class RoomServiceTests {

@Mock private RoomRepository roomRepository;
@Mock private QuizService quizService;
@Mock private TimerService timerService;
@Mock private ApplicationEventPublisher eventPublisher;
@Mock private MessageSender messageSender;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this); // @Mock 어노테이션이 붙은 필드들을 초기화합니다.
roomService = new RoomService(quizService, roomRepository, eventPublisher, messageSender);
roomService =
new RoomService(
timerService, quizService, roomRepository, eventPublisher, messageSender);

SecurityContextHolder.clearContext();
}
Expand Down