Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
be3f034
:sparkles: feat: 게임 설정 변경 인터페이스 및 구현체 추가
LimKangHyun Jul 22, 2025
e1d79fc
chore: Java 스타일 수정
Jul 22, 2025
214bc54
:sparkles: feat: 게임 설정 변경 기능 추가
LimKangHyun Jul 22, 2025
ccac167
chore: Java 스타일 수정
Jul 22, 2025
9af131a
:truck: move: 소켓 요청 DTO request 디렉토리로 이동
LimKangHyun Jul 22, 2025
2fb0b8f
:truck: move: 소켓 요청 DTO 경로 변경
LimKangHyun Jul 22, 2025
a7ead79
chore: Java 스타일 수정
Jul 22, 2025
6981459
:recycle: refactor: 게임 세팅 요청 후처리 분리
LimKangHyun Jul 22, 2025
fdaaee7
chore: Java 스타일 수정
Jul 22, 2025
655929e
:recycle: refactor: 코드 리뷰 반영
LimKangHyun Jul 23, 2025
39d9f54
chore: Java 스타일 수정
Jul 23, 2025
80101d0
:wastebasket: remove: 불필요 코드 삭제
LimKangHyun Jul 23, 2025
db62cbc
chore: Java 스타일 수정
Jul 23, 2025
181f4ab
:recycle: refactor: isHost 원상복구
LimKangHyun Jul 23, 2025
c7506d9
:recycle: refactor: QuizChangeRequest, RoundChangeRequest 타입 수정
LimKangHyun Jul 23, 2025
b6ae413
:wrench: chore: RoomUpdatedEventListener, RoomDeletedEventListener 빈 등록
LimKangHyun Jul 23, 2025
476ceea
chore: Java 스타일 수정
Jul 23, 2025
b7852c2
:wrench: fix: 충돌 해결
LimKangHyun Jul 23, 2025
2db2e95
Merge remote-tracking branch 'origin/feat/100' into feat/100
LimKangHyun Jul 23, 2025
7c6515e
chore: Java 스타일 수정
Jul 23, 2025
482bc72
:recycle: refactor: player 조회 메서드 추가
LimKangHyun Jul 24, 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
@@ -1,10 +1,13 @@
package io.f1.backend.domain.game.app;

import static io.f1.backend.domain.game.mapper.RoomMapper.toGameSettingResponse;
import static io.f1.backend.domain.game.mapper.RoomMapper.toPlayerListResponse;
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionStartResponse;
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;
import static io.f1.backend.domain.quiz.mapper.QuizMapper.toGameStartResponse;

import io.f1.backend.domain.game.dto.MessageType;
import io.f1.backend.domain.game.dto.request.GameSettingChanger;
import io.f1.backend.domain.game.dto.response.PlayerListResponse;
import io.f1.backend.domain.game.event.RoomUpdatedEvent;
import io.f1.backend.domain.game.model.Player;
import io.f1.backend.domain.game.model.Room;
Expand All @@ -20,14 +23,15 @@
import io.f1.backend.global.exception.errorcode.RoomErrorCode;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Objects;

@Slf4j
@Service
@RequiredArgsConstructor
public class GameService {
Expand Down Expand Up @@ -70,19 +74,50 @@ public void gameStart(Long roomId, UserPrincipal principal) {
toQuestionStartResponse(room, START_DELAY));
}

private boolean validateReadyStatus(Room room) {
public void handlePlayerReady(Long roomId, String sessionId) {

Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
Room room = findRoom(roomId);

return playerSessionMap.values().stream().allMatch(Player::isReady);
Player player = room.getPlayerSessionMap().get(sessionId);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분도 Room의 메서드로 분리할 수 있을 것 같습니다 !

Copy link
Collaborator Author

@LimKangHyun LimKangHyun Jul 24, 2025

Choose a reason for hiding this comment

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

Room클래스 안에, getPlayerBySessionId 만들어서 교체했습니다!

if (player == null) {
throw new CustomException(RoomErrorCode.PLAYER_NOT_FOUND);
}

toggleReadyIfPossible(room, player);

String destination = getDestination(roomId);

PlayerListResponse playerListResponse = toPlayerListResponse(room);
log.info(playerListResponse.toString());
messageSender.send(destination, MessageType.PLAYER_LIST, playerListResponse);
}

public void changeGameSetting(
Long roomId, UserPrincipal principal, GameSettingChanger request) {
Room room = findRoom(roomId);
validateHostAndState(room, principal);

if (!request.change(room, quizService)) {
return;
}
request.afterChange(room, messageSender);

broadcastGameSetting(room);

RoomUpdatedEvent roomUpdatedEvent =
new RoomUpdatedEvent(
room,
quizService.getQuizWithQuestionsById(room.getGameSetting().getQuizId()));

eventPublisher.publishEvent(roomUpdatedEvent);
}

private void validateRoomStart(Room room, UserPrincipal principal) {
if (!Objects.equals(principal.getUserId(), room.getHost().getId())) {
throw new CustomException(RoomErrorCode.NOT_ROOM_OWNER);
}

if (!validateReadyStatus(room)) {
if (!room.validateReadyStatus()) {
throw new CustomException(GameErrorCode.PLAYER_NOT_READY);
}

Expand All @@ -97,4 +132,41 @@ private List<Question> prepareQuestions(Room room, Quiz quiz) {
Integer round = room.getGameSetting().getRound();
return quizService.getRandomQuestionsWithoutAnswer(quizId, round);
}

private Room findRoom(Long roomId) {
return roomRepository
.findRoom(roomId)
.orElseThrow(() -> new CustomException(RoomErrorCode.ROOM_NOT_FOUND));
}

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

private void validateHostAndState(Room room, UserPrincipal principal) {
if (!room.isHost(principal.getUserId())) {
throw new CustomException(RoomErrorCode.NOT_ROOM_OWNER);
}
if (room.isPlaying()) {
throw new CustomException(RoomErrorCode.GAME_ALREADY_PLAYING);
}
}

private void toggleReadyIfPossible(Room room, Player player) {
if (room.isPlaying()) {
throw new CustomException(RoomErrorCode.GAME_ALREADY_PLAYING);
}
if (!room.isHost(player.getId())) {
player.toggleReady();
}
}

private void broadcastGameSetting(Room room) {
String destination = getDestination(room.getId());
Quiz quiz = quizService.getQuizWithQuestionsById(room.getGameSetting().getQuizId());
messageSender.send(
destination,
MessageType.GAME_SETTING,
toGameSettingResponse(room.getGameSetting(), quiz));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,21 +190,6 @@ public void exitRoom(Long roomId, String sessionId, UserPrincipal principal) {
}
}

public void handlePlayerReady(Long roomId, String sessionId) {
Player player =
roomRepository
.findPlayerInRoomBySessionId(roomId, sessionId)
.orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND));

player.toggleReady();

Room room = findRoom(roomId);

String destination = getDestination(roomId);

messageSender.send(destination, MessageType.PLAYER_LIST, toPlayerListResponse(room));
}

public RoomListResponse getAllRooms() {
List<Room> rooms = roomRepository.findAll();
List<RoomResponse> roomResponses =
Expand Down Expand Up @@ -327,7 +312,7 @@ public void exitIfNotPlaying(Long roomId, String sessionId, UserPrincipal princi
private Player getRemovePlayer(Room room, String sessionId, UserPrincipal principal) {
Player removePlayer = room.getPlayerSessionMap().get(sessionId);
if (removePlayer == null) {
room.removeUserId(principal.getUserId());
room.removeValidatedUserId(principal.getUserId());
throw new CustomException(RoomErrorCode.SOCKET_SESSION_NOT_FOUND);
}
return removePlayer;
Expand Down Expand Up @@ -374,7 +359,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.removeValidatedUserId(removePlayer.getId());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.f1.backend.domain.game.dto.request;

import io.f1.backend.domain.game.model.Room;
import io.f1.backend.domain.game.websocket.MessageSender;
import io.f1.backend.domain.quiz.app.QuizService;

public interface GameSettingChanger {

boolean change(Room room, QuizService quizService);

void afterChange(Room room, MessageSender messageSender);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.f1.backend.domain.game.dto.request;

import static io.f1.backend.domain.game.mapper.RoomMapper.toPlayerListResponse;
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;

import io.f1.backend.domain.game.dto.MessageType;
import io.f1.backend.domain.game.dto.response.PlayerListResponse;
import io.f1.backend.domain.game.model.Room;
import io.f1.backend.domain.game.websocket.MessageSender;
import io.f1.backend.domain.quiz.app.QuizService;
import io.f1.backend.domain.quiz.entity.Quiz;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public record QuizChangeRequest(long quizId) implements GameSettingChanger {

@Override
public boolean change(Room room, QuizService quizService) {
if (room.getQuizId() == quizId) {
return false; // 동일하면 무시
}
Quiz quiz = quizService.getQuizWithQuestionsById(quizId);
int questionSize = quiz.getQuestions().size();
room.changeQuiz(quiz);
// 퀴즈의 문제 갯수로 변경
room.changeRound(questionSize, questionSize);
return true;
}

@Override
public void afterChange(Room room, MessageSender messageSender) {
room.resetAllPlayerReadyStates();

String destination = getDestination(room.getId());
PlayerListResponse response = toPlayerListResponse(room);

log.info(response.toString());
messageSender.send(destination, MessageType.PLAYER_LIST, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.f1.backend.domain.game.dto.request;

import io.f1.backend.domain.game.model.Room;
import io.f1.backend.domain.game.websocket.MessageSender;
import io.f1.backend.domain.quiz.app.QuizService;
import io.f1.backend.domain.quiz.entity.Quiz;

public record RoundChangeRequest(int round) implements GameSettingChanger {

@Override
public boolean change(Room room, QuizService quizService) {
if (room.getRound() == round) {
return false; // 동일하면 무시
}

Quiz quiz = quizService.findQuizById(room.getQuizId());
int questionSize = quiz.getQuestions().size();

room.changeRound(round, questionSize);
return true;
}

@Override
public void afterChange(Room room, MessageSender messageSender) {
// 고유한 후처리 동작 없음
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.f1.backend.domain.game.dto.request;

import io.f1.backend.global.exception.CustomException;
import io.f1.backend.global.exception.errorcode.GameErrorCode;

import lombok.Getter;

import java.util.Arrays;

@Getter
public enum TimeLimit {
FIFTEEN(15),
THIRTY(30),
FORTY_FIVE(45),
SIXTY(60);

private final int value;

TimeLimit(int value) {
this.value = value;
}

public static TimeLimit from(int value) {
return Arrays.stream(values())
.filter(t -> t.value == value)
.findFirst()
.orElseThrow(() -> new CustomException(GameErrorCode.GAME_SETTING_CONFLICT));
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.

게임 내에서 일반 사용자가 유효하지 않은 타이머를 보낼일은 없다고 생각해서, 비정상적인 요청에 대해서 만들어놓은 방어적 예외라 예외 내용을 구체화하지는 않았습니다!

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

import io.f1.backend.domain.game.model.Room;
import io.f1.backend.domain.game.websocket.MessageSender;
import io.f1.backend.domain.quiz.app.QuizService;

public record TimeLimitChangeRequest(int timeLimit) implements GameSettingChanger {

@Override
public boolean change(Room room, QuizService quizService) {
if (room.getTimeLimit() == timeLimit) {
return false; // 동일하면 무시
}
room.changeTimeLimit(TimeLimit.from(timeLimit));
return true;
}

@Override
public void afterChange(Room room, MessageSender messageSender) {
// 고유한 후처리 동작 없음
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static GameSettingResponse toGameSettingResponse(GameSetting gameSetting,
public static PlayerListResponse toPlayerListResponse(Room room) {
List<PlayerResponse> playerResponseList =
room.getPlayerSessionMap().values().stream()
.map(player -> new PlayerResponse(player.getNickname(), false))
.map(player -> new PlayerResponse(player.getNickname(), player.isReady()))
.toList();

return new PlayerListResponse(room.getHost().getNickname(), playerResponseList);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
package io.f1.backend.domain.game.model;

import io.f1.backend.domain.game.dto.request.TimeLimit;
import io.f1.backend.domain.quiz.entity.Quiz;
import io.f1.backend.global.exception.CustomException;
import io.f1.backend.global.exception.errorcode.GameErrorCode;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;

@Getter
@AllArgsConstructor
public class GameSetting {

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

public boolean validateQuizId(Long quizId) {
return Objects.equals(this.quizId, quizId);
public void changeQuiz(Quiz quiz) {
quizId = quiz.getId();
round = quiz.getQuestions().size(); // 라운드를 바꾼 퀴즈의 문제 수로 동기화
}

public void changeTimeLimit(TimeLimit timeLimit) {
this.timeLimit = timeLimit.getValue();
}

public void changeRound(int round, int questionsCount) {
if (round > questionsCount) {
throw new CustomException(GameErrorCode.ROUND_EXCEEDS_QUESTION_COUNT);
}
this.round = round;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public void toggleReady() {
this.isReady = !this.isReady;
}

public void setReadyFalse() {
this.isReady = false;
}

public void increaseCorrectCount() {
correctCount++;
}
Expand Down
Loading