Skip to content

Commit 2d23468

Browse files
authored
✨ feat: GameSetting 변경 기능 추가 (#107)
* ✨ feat: 게임 설정 변경 인터페이스 및 구현체 추가 - GameSettingChanger 인터페이스 도입 * chore: Java 스타일 수정 * ✨ feat: 게임 설정 변경 기능 추가 * chore: Java 스타일 수정 * 🚚 move: 소켓 요청 DTO request 디렉토리로 이동 * 🚚 move: 소켓 요청 DTO 경로 변경 * chore: Java 스타일 수정 * ♻️ refactor: 게임 세팅 요청 후처리 분리 * chore: Java 스타일 수정 * ♻️ refactor: 코드 리뷰 반영 * chore: Java 스타일 수정 * 🗑️ remove: 불필요 코드 삭제 * chore: Java 스타일 수정 * ♻️ refactor: isHost 원상복구 * ♻️ refactor: QuizChangeRequest, RoundChangeRequest 타입 수정 * 🔧 chore: RoomUpdatedEventListener, RoomDeletedEventListener 빈 등록 * chore: Java 스타일 수정 * chore: Java 스타일 수정 * ♻️ refactor: player 조회 메서드 추가 --------- Co-authored-by: github-actions <>
1 parent 1cea9a4 commit 2d23468

File tree

18 files changed

+318
-47
lines changed

18 files changed

+318
-47
lines changed

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

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

3+
import static io.f1.backend.domain.game.mapper.RoomMapper.toGameSettingResponse;
4+
import static io.f1.backend.domain.game.mapper.RoomMapper.toPlayerListResponse;
35
import static io.f1.backend.domain.game.mapper.RoomMapper.toQuestionStartResponse;
4-
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;
56
import static io.f1.backend.domain.quiz.mapper.QuizMapper.toGameStartResponse;
67

78
import io.f1.backend.domain.game.dto.MessageType;
9+
import io.f1.backend.domain.game.dto.request.GameSettingChanger;
10+
import io.f1.backend.domain.game.dto.response.PlayerListResponse;
811
import io.f1.backend.domain.game.event.RoomUpdatedEvent;
912
import io.f1.backend.domain.game.model.Player;
1013
import io.f1.backend.domain.game.model.Room;
@@ -20,14 +23,15 @@
2023
import io.f1.backend.global.exception.errorcode.RoomErrorCode;
2124

2225
import lombok.RequiredArgsConstructor;
26+
import lombok.extern.slf4j.Slf4j;
2327

2428
import org.springframework.context.ApplicationEventPublisher;
2529
import org.springframework.stereotype.Service;
2630

2731
import java.util.List;
28-
import java.util.Map;
2932
import java.util.Objects;
3033

34+
@Slf4j
3135
@Service
3236
@RequiredArgsConstructor
3337
public class GameService {
@@ -70,19 +74,47 @@ public void gameStart(Long roomId, UserPrincipal principal) {
7074
toQuestionStartResponse(room, START_DELAY));
7175
}
7276

73-
private boolean validateReadyStatus(Room room) {
77+
public void handlePlayerReady(Long roomId, String sessionId) {
7478

75-
Map<String, Player> playerSessionMap = room.getPlayerSessionMap();
79+
Room room = findRoom(roomId);
7680

77-
return playerSessionMap.values().stream().allMatch(Player::isReady);
81+
Player player = room.getPlayerBySessionId(sessionId);
82+
83+
toggleReadyIfPossible(room, player);
84+
85+
String destination = getDestination(roomId);
86+
87+
PlayerListResponse playerListResponse = toPlayerListResponse(room);
88+
log.info(playerListResponse.toString());
89+
messageSender.send(destination, MessageType.PLAYER_LIST, playerListResponse);
90+
}
91+
92+
public void changeGameSetting(
93+
Long roomId, UserPrincipal principal, GameSettingChanger request) {
94+
Room room = findRoom(roomId);
95+
validateHostAndState(room, principal);
96+
97+
if (!request.change(room, quizService)) {
98+
return;
99+
}
100+
request.afterChange(room, messageSender);
101+
102+
broadcastGameSetting(room);
103+
104+
RoomUpdatedEvent roomUpdatedEvent =
105+
new RoomUpdatedEvent(
106+
room,
107+
quizService.getQuizWithQuestionsById(room.getGameSetting().getQuizId()));
108+
109+
eventPublisher.publishEvent(roomUpdatedEvent);
78110
}
79111

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

85-
if (!validateReadyStatus(room)) {
117+
if (!room.validateReadyStatus()) {
86118
throw new CustomException(GameErrorCode.PLAYER_NOT_READY);
87119
}
88120

@@ -97,4 +129,41 @@ private List<Question> prepareQuestions(Room room, Quiz quiz) {
97129
Integer round = room.getGameSetting().getRound();
98130
return quizService.getRandomQuestionsWithoutAnswer(quizId, round);
99131
}
132+
133+
private Room findRoom(Long roomId) {
134+
return roomRepository
135+
.findRoom(roomId)
136+
.orElseThrow(() -> new CustomException(RoomErrorCode.ROOM_NOT_FOUND));
137+
}
138+
139+
private String getDestination(Long roomId) {
140+
return "/sub/room/" + roomId;
141+
}
142+
143+
private void validateHostAndState(Room room, UserPrincipal principal) {
144+
if (!room.isHost(principal.getUserId())) {
145+
throw new CustomException(RoomErrorCode.NOT_ROOM_OWNER);
146+
}
147+
if (room.isPlaying()) {
148+
throw new CustomException(RoomErrorCode.GAME_ALREADY_PLAYING);
149+
}
150+
}
151+
152+
private void toggleReadyIfPossible(Room room, Player player) {
153+
if (room.isPlaying()) {
154+
throw new CustomException(RoomErrorCode.GAME_ALREADY_PLAYING);
155+
}
156+
if (!room.isHost(player.getId())) {
157+
player.toggleReady();
158+
}
159+
}
160+
161+
private void broadcastGameSetting(Room room) {
162+
String destination = getDestination(room.getId());
163+
Quiz quiz = quizService.getQuizWithQuestionsById(room.getGameSetting().getQuizId());
164+
messageSender.send(
165+
destination,
166+
MessageType.GAME_SETTING,
167+
toGameSettingResponse(room.getGameSetting(), quiz));
168+
}
100169
}

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

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -190,21 +190,6 @@ public void exitRoom(Long roomId, String sessionId, UserPrincipal principal) {
190190
}
191191
}
192192

193-
public void handlePlayerReady(Long roomId, String sessionId) {
194-
Player player =
195-
roomRepository
196-
.findPlayerInRoomBySessionId(roomId, sessionId)
197-
.orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND));
198-
199-
player.toggleReady();
200-
201-
Room room = findRoom(roomId);
202-
203-
String destination = getDestination(roomId);
204-
205-
messageSender.send(destination, MessageType.PLAYER_LIST, toPlayerListResponse(room));
206-
}
207-
208193
public RoomListResponse getAllRooms() {
209194
List<Room> rooms = roomRepository.findAll();
210195
List<RoomResponse> roomResponses =
@@ -327,7 +312,7 @@ public void exitIfNotPlaying(Long roomId, String sessionId, UserPrincipal princi
327312
private Player getRemovePlayer(Room room, String sessionId, UserPrincipal principal) {
328313
Player removePlayer = room.getPlayerSessionMap().get(sessionId);
329314
if (removePlayer == null) {
330-
room.removeUserId(principal.getUserId());
315+
room.removeValidatedUserId(principal.getUserId());
331316
throw new CustomException(RoomErrorCode.SOCKET_SESSION_NOT_FOUND);
332317
}
333318
return removePlayer;
@@ -374,7 +359,6 @@ private void changeHost(Room room, String hostSessionId) {
374359
}
375360

376361
private void removePlayer(Room room, String sessionId, Player removePlayer) {
377-
room.removeUserId(removePlayer.getId());
378362
room.removeSessionId(sessionId);
379363
room.removeValidatedUserId(removePlayer.getId());
380364
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.f1.backend.domain.game.dto.request;
2+
3+
import io.f1.backend.domain.game.model.Room;
4+
import io.f1.backend.domain.game.websocket.MessageSender;
5+
import io.f1.backend.domain.quiz.app.QuizService;
6+
7+
public interface GameSettingChanger {
8+
9+
boolean change(Room room, QuizService quizService);
10+
11+
void afterChange(Room room, MessageSender messageSender);
12+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.f1.backend.domain.game.dto.request;
2+
3+
import static io.f1.backend.domain.game.mapper.RoomMapper.toPlayerListResponse;
4+
import static io.f1.backend.domain.game.websocket.WebSocketUtils.getDestination;
5+
6+
import io.f1.backend.domain.game.dto.MessageType;
7+
import io.f1.backend.domain.game.dto.response.PlayerListResponse;
8+
import io.f1.backend.domain.game.model.Room;
9+
import io.f1.backend.domain.game.websocket.MessageSender;
10+
import io.f1.backend.domain.quiz.app.QuizService;
11+
import io.f1.backend.domain.quiz.entity.Quiz;
12+
13+
import lombok.extern.slf4j.Slf4j;
14+
15+
@Slf4j
16+
public record QuizChangeRequest(long quizId) implements GameSettingChanger {
17+
18+
@Override
19+
public boolean change(Room room, QuizService quizService) {
20+
if (room.getQuizId() == quizId) {
21+
return false; // 동일하면 무시
22+
}
23+
Quiz quiz = quizService.getQuizWithQuestionsById(quizId);
24+
int questionSize = quiz.getQuestions().size();
25+
room.changeQuiz(quiz);
26+
// 퀴즈의 문제 갯수로 변경
27+
room.changeRound(questionSize, questionSize);
28+
return true;
29+
}
30+
31+
@Override
32+
public void afterChange(Room room, MessageSender messageSender) {
33+
room.resetAllPlayerReadyStates();
34+
35+
String destination = getDestination(room.getId());
36+
PlayerListResponse response = toPlayerListResponse(room);
37+
38+
log.info(response.toString());
39+
messageSender.send(destination, MessageType.PLAYER_LIST, response);
40+
}
41+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.f1.backend.domain.game.dto.request;
2+
3+
import io.f1.backend.domain.game.model.Room;
4+
import io.f1.backend.domain.game.websocket.MessageSender;
5+
import io.f1.backend.domain.quiz.app.QuizService;
6+
import io.f1.backend.domain.quiz.entity.Quiz;
7+
8+
public record RoundChangeRequest(int round) implements GameSettingChanger {
9+
10+
@Override
11+
public boolean change(Room room, QuizService quizService) {
12+
if (room.getRound() == round) {
13+
return false; // 동일하면 무시
14+
}
15+
16+
Quiz quiz = quizService.findQuizById(room.getQuizId());
17+
int questionSize = quiz.getQuestions().size();
18+
19+
room.changeRound(round, questionSize);
20+
return true;
21+
}
22+
23+
@Override
24+
public void afterChange(Room room, MessageSender messageSender) {
25+
// 고유한 후처리 동작 없음
26+
}
27+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.f1.backend.domain.game.dto.request;
2+
3+
import io.f1.backend.global.exception.CustomException;
4+
import io.f1.backend.global.exception.errorcode.GameErrorCode;
5+
6+
import lombok.Getter;
7+
8+
import java.util.Arrays;
9+
10+
@Getter
11+
public enum TimeLimit {
12+
FIFTEEN(15),
13+
THIRTY(30),
14+
FORTY_FIVE(45),
15+
SIXTY(60);
16+
17+
private final int value;
18+
19+
TimeLimit(int value) {
20+
this.value = value;
21+
}
22+
23+
public static TimeLimit from(int value) {
24+
return Arrays.stream(values())
25+
.filter(t -> t.value == value)
26+
.findFirst()
27+
.orElseThrow(() -> new CustomException(GameErrorCode.GAME_SETTING_CONFLICT));
28+
}
29+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.f1.backend.domain.game.dto.request;
2+
3+
import io.f1.backend.domain.game.model.Room;
4+
import io.f1.backend.domain.game.websocket.MessageSender;
5+
import io.f1.backend.domain.quiz.app.QuizService;
6+
7+
public record TimeLimitChangeRequest(int timeLimit) implements GameSettingChanger {
8+
9+
@Override
10+
public boolean change(Room room, QuizService quizService) {
11+
if (room.getTimeLimit() == timeLimit) {
12+
return false; // 동일하면 무시
13+
}
14+
room.changeTimeLimit(TimeLimit.from(timeLimit));
15+
return true;
16+
}
17+
18+
@Override
19+
public void afterChange(Room room, MessageSender messageSender) {
20+
// 고유한 후처리 동작 없음
21+
}
22+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public static GameSettingResponse toGameSettingResponse(GameSetting gameSetting,
5656
public static PlayerListResponse toPlayerListResponse(Room room) {
5757
List<PlayerResponse> playerResponseList =
5858
room.getPlayerSessionMap().values().stream()
59-
.map(player -> new PlayerResponse(player.getNickname(), false))
59+
.map(player -> new PlayerResponse(player.getNickname(), player.isReady()))
6060
.toList();
6161

6262
return new PlayerListResponse(room.getHost().getNickname(), playerResponseList);
Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
11
package io.f1.backend.domain.game.model;
22

3+
import io.f1.backend.domain.game.dto.request.TimeLimit;
4+
import io.f1.backend.domain.quiz.entity.Quiz;
5+
import io.f1.backend.global.exception.CustomException;
6+
import io.f1.backend.global.exception.errorcode.GameErrorCode;
7+
38
import lombok.AllArgsConstructor;
49
import lombok.Getter;
510

6-
import java.util.Objects;
7-
811
@Getter
912
@AllArgsConstructor
1013
public class GameSetting {
1114

1215
private Long quizId;
13-
private Integer round; // 게임 변경 시 해당 게임의 총 문제 수로 설정
16+
private Integer round;
1417
private int timeLimit;
1518

16-
public boolean validateQuizId(Long quizId) {
17-
return Objects.equals(this.quizId, quizId);
19+
public void changeQuiz(Quiz quiz) {
20+
quizId = quiz.getId();
21+
round = quiz.getQuestions().size(); // 라운드를 바꾼 퀴즈의 문제 수로 동기화
22+
}
23+
24+
public void changeTimeLimit(TimeLimit timeLimit) {
25+
this.timeLimit = timeLimit.getValue();
26+
}
27+
28+
public void changeRound(int round, int questionsCount) {
29+
if (round > questionsCount) {
30+
throw new CustomException(GameErrorCode.ROUND_EXCEEDS_QUESTION_COUNT);
31+
}
32+
this.round = round;
1833
}
1934
}

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
@@ -24,6 +24,10 @@ public void toggleReady() {
2424
this.isReady = !this.isReady;
2525
}
2626

27+
public void setReadyFalse() {
28+
this.isReady = false;
29+
}
30+
2731
public void increaseCorrectCount() {
2832
correctCount++;
2933
}

0 commit comments

Comments
 (0)