Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -18,9 +18,6 @@
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/rooms")
@RequiredArgsConstructor
Expand All @@ -31,12 +28,7 @@ public class RoomController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public RoomCreateResponse saveRoom(@RequestBody @Valid RoomCreateRequest request) {

Map<String, Object> loginUser = new HashMap<>();
loginUser.put("id", 1L);
loginUser.put("nickname", "빵야빵야");

return roomService.saveRoom(request, loginUser);
return roomService.saveRoom(request);
}

@PostMapping("/validation")
Expand Down
102 changes: 82 additions & 20 deletions backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package io.f1.backend.domain.game.app;

import static io.f1.backend.domain.game.mapper.RoomMapper.*;

import static io.f1.backend.domain.game.mapper.RoomMapper.ofPlayerEvent;
import static io.f1.backend.domain.game.mapper.RoomMapper.toGameSetting;
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.toRoomResponse;
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.global.util.SecurityUtils.getCurrentUserId;
import static io.f1.backend.global.util.SecurityUtils.getCurrentUserNickname;

import io.f1.backend.domain.game.dto.RoomEventType;
import io.f1.backend.domain.game.dto.RoomExitData;
import io.f1.backend.domain.game.dto.RoomInitialData;
import io.f1.backend.domain.game.dto.request.RoomCreateRequest;
import io.f1.backend.domain.game.dto.request.RoomValidationRequest;
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.QuizResponse;
import io.f1.backend.domain.game.dto.response.RoomCreateResponse;
import io.f1.backend.domain.game.dto.response.RoomListResponse;
import io.f1.backend.domain.game.dto.response.RoomResponse;
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.GameSetting;
import io.f1.backend.domain.game.model.Player;
Expand All @@ -29,6 +39,7 @@

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;

@Service
Expand All @@ -40,12 +51,15 @@ public class RoomService {
private final AtomicLong roomIdGenerator = new AtomicLong(0);
private final ApplicationEventPublisher eventPublisher;

public RoomCreateResponse saveRoom(RoomCreateRequest request, Map<String, Object> loginUser) {
public RoomCreateResponse saveRoom(RoomCreateRequest request) {

Long quizMinId = quizService.getQuizMinId();
Quiz quiz = quizService.getQuizById(quizMinId);

GameSetting gameSetting = toGameSetting(quiz);

Player host = createPlayer();

// todo 제일 작은 index quizId 가져와서 gameSetting(round 설정)
GameSetting gameSetting = new GameSetting(1L, 10, 60);
// todo security에서 가져오는걸로 변경
Player host = new Player((Long) loginUser.get("id"), loginUser.get("nickname").toString());
RoomSetting roomSetting = toRoomSetting(request);

Long newId = roomIdGenerator.incrementAndGet();
Expand All @@ -54,9 +68,6 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request, Map<String, Object

roomRepository.saveRoom(room);

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

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

return new RoomCreateResponse(newId);
Expand All @@ -67,7 +78,7 @@ public void validateRoom(RoomValidationRequest request) {
Room room =
roomRepository
.findRoom(request.roomId())
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다.-1"));

if (room.getState().equals(RoomState.PLAYING)) {
throw new IllegalArgumentException("403 게임이 진행중입니다.");
Expand All @@ -92,26 +103,69 @@ public RoomInitialData enterRoom(Long roomId, String sessionId) {
.findRoom(roomId)
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));

// todo security
Player player = new Player(1L, "빵야빵야");
Player player = createPlayer();
Copy link
Collaborator

Choose a reason for hiding this comment

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

[L3-중요질문]
새 플레이어가 방에 입장했을 때, SYSTEM NOTICE타입의 입장메시지가 브로드캐스팅되지 않는것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

웹소켓 명세에 빠져있는 부분이라 놓쳤군요..! 감사합니다. 추가하겠습니다. 😇


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

playerSessionMap.put(sessionId, player);

String destination = "/sub/room/" + roomId;

RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room);
// todo quiz 생성 api 완성 후 수정
QuizResponse quiz =
new QuizResponse(room.getGameSetting().getQuizId(), "title", "설명", "url", 10);

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

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

PlayerListResponse playerListResponse = toPlayerListResponse(room);

SystemNoticeResponse systemNoticeResponse = ofPlayerEvent(player, RoomEventType.ENTER);

return new RoomInitialData(
destination, roomSettingResponse, gameSettingResponse, playerListResponse);
getDestination(roomId),
roomSettingResponse,
gameSettingResponse,
playerListResponse,
systemNoticeResponse);
}

public RoomExitData exitRoom(Long roomId, 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-변경제안]
메서드로 분리할 수 있는 로직들은 분리하는 것이 가독성이 좋아보입니다 !

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

안그래도 private static 으로 몇개는 분리해놨는데, 요 부분은 동시성 진행하면서 더 분리해보도록 하겠습니다! 감사합니다 ☺️

Room room =
roomRepository
.findRoom(roomId)
.orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다."));

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

String destination = getDestination(roomId);

if (playerSessionMap.size() == 1 && playerSessionMap.get(sessionId) != null) {
roomRepository.removeRoom(roomId);
return RoomExitData.builder().destination(destination).removedRoom(true).build();
}

Player removedPlayer = playerSessionMap.remove(sessionId);
if (removedPlayer == null) {
throw new IllegalArgumentException("퇴장 처리 불가 - 404 해당 세션 플레이어는 존재하지않습니다.");
}

if (room.getHost().getId().equals(removedPlayer.getId())) {
Optional<String> nextHostSessionId = playerSessionMap.keySet().stream().findFirst();
Player nextHost =
playerSessionMap.get(
nextHostSessionId.orElseThrow(
() ->
new IllegalArgumentException(
"방장 교체 불가 - 404 해당 세션 플레이어는 존재하지않습니다.")));
room.updateHost(nextHost);
}

SystemNoticeResponse systemNoticeResponse =
ofPlayerEvent(removedPlayer, RoomEventType.EXIT);

PlayerListResponse playerListResponse = toPlayerListResponse(room);

return new RoomExitData(destination, playerListResponse, systemNoticeResponse, false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

removedRoom이라는 필드가 방이 삭제되었을 때 true로 사용되고 있지 않은 것 같은데 이 필드의 용도는 뭔지 궁금합니다.

Copy link
Collaborator Author

@sehee123 sehee123 Jul 15, 2025

Choose a reason for hiding this comment

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

removedRoom이라는 필드가 방이 삭제되었을 때 true로 사용되고 있지 않은 것 같은데 이 필드의 용도는 뭔지 궁금합니다.

방이 삭제되었을때는 따로 응답 값을 보내주지 보내주지 않아도되고, 삭제가 안되었을때만 응답값을 보내줘야하기에 구분 필드를 넣었습니다. 원래 record로 사용시에는 true로 생성했는데, 빌더로 바꾸면서 true가 빠진 것 같습니다! 알려주셔서.. 감사합니다 ^_^

}

public RoomListResponse getAllRooms() {
Expand All @@ -128,4 +182,12 @@ public RoomListResponse getAllRooms() {
.toList();
return new RoomListResponse(roomResponses);
}

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

private static Player createPlayer() {
return new Player(getCurrentUserId(), getCurrentUserNickname());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public enum MessageType {
ROOM_SETTING,
GAME_SETTING,
PLAYER_LIST,
SYSTEM_NOTICE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.f1.backend.domain.game.dto;

public enum RoomEventType {
ENTER,
EXIT,
START,
END,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.f1.backend.domain.game.dto;

import io.f1.backend.domain.game.dto.response.PlayerListResponse;
import io.f1.backend.domain.game.dto.response.SystemNoticeResponse;

import lombok.Builder;
import lombok.Getter;

@Getter
public class RoomExitData {

private final String destination;
private final PlayerListResponse playerListResponses;
private final SystemNoticeResponse systemNoticeResponse;
private final boolean removedRoom;

@Builder
public RoomExitData(
String destination,
PlayerListResponse playerListResponses,
SystemNoticeResponse systemNoticeResponse,
boolean removedRoom) {
this.destination = destination;
this.playerListResponses = playerListResponses;
this.systemNoticeResponse = systemNoticeResponse;
this.removedRoom = removedRoom;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
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.RoomSettingResponse;
import io.f1.backend.domain.game.dto.response.SystemNoticeResponse;

public record RoomInitialData(
String destination,
RoomSettingResponse roomSettingResponse,
GameSettingResponse gameSettingResponse,
PlayerListResponse playerListResponse) {}
PlayerListResponse playerListResponse,
SystemNoticeResponse systemNoticeResponse) {}
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 SystemNoticeResponse(String noticeMessage, Instant timestamp) {}
Original file line number Diff line number Diff line change
@@ -1,36 +1,46 @@
package io.f1.backend.domain.game.mapper;

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.QuizResponse;
import io.f1.backend.domain.game.dto.response.RoomResponse;
import io.f1.backend.domain.game.dto.response.RoomSettingResponse;
import io.f1.backend.domain.game.dto.response.SystemNoticeResponse;
import io.f1.backend.domain.game.model.GameSetting;
import io.f1.backend.domain.game.model.Player;
import io.f1.backend.domain.game.model.Room;
import io.f1.backend.domain.game.model.RoomSetting;
import io.f1.backend.domain.quiz.entity.Quiz;

import java.time.Instant;
import java.util.List;

public class RoomMapper {

private static final int DEFAULT_TIME_LIMIT = 60;

public static RoomSetting toRoomSetting(RoomCreateRequest request) {
return new RoomSetting(
request.roomName(), request.maxUserCount(), request.locked(), request.password());
}

public static GameSetting toGameSetting(Quiz quiz) {
return new GameSetting(quiz.getId(), quiz.getQuestions().size(), DEFAULT_TIME_LIMIT);
}

public static RoomSettingResponse toRoomSettingResponse(Room room) {
return new RoomSettingResponse(
room.getRoomSetting().roomName(),
room.getRoomSetting().maxUserCount(),
room.getPlayerSessionMap().size());
}

public static GameSettingResponse toGameSettingResponse(
GameSetting gameSetting, QuizResponse quiz) {
return new GameSettingResponse(gameSetting.getRound(), gameSetting.getTimeLimit(), quiz);
public static GameSettingResponse toGameSettingResponse(GameSetting gameSetting, Quiz quiz) {
return new GameSettingResponse(
gameSetting.getRound(), gameSetting.getTimeLimit(), toQuizResponse(quiz));
}

public static PlayerListResponse toPlayerListResponse(Room room) {
Expand All @@ -56,4 +66,23 @@ public static RoomResponse toRoomResponse(Room room, Quiz quiz) {
quiz.getQuestions().size(),
quiz.getThumbnailUrl());
}

public static QuizResponse toQuizResponse(Quiz quiz) {
return new QuizResponse(
quiz.getId(),
quiz.getTitle(),
quiz.getDescription(),
quiz.getThumbnailUrl(),
quiz.getQuestions().size());
}

public static SystemNoticeResponse ofPlayerEvent(Player player, RoomEventType roomEventType) {
String message = "";
if (roomEventType == RoomEventType.ENTER) {
message = " 님이 입장하셨습니다";
} else if (roomEventType == RoomEventType.EXIT) {
message = " 님이 퇴장하셨습니다";
}
return new SystemNoticeResponse(player.getNickname() + message, Instant.now());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ public Room(Long id, RoomSetting roomSetting, GameSetting gameSetting, Player ho
this.gameSetting = gameSetting;
this.host = host;
}

public void updateHost(Player nextHost) {
this.host = nextHost;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public interface RoomRepository {
Optional<Room> findRoom(Long roomId);

List<Room> findAll();

void removeRoom(Long roomId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public List<Room> findAll() {
return new ArrayList<>(roomMap.values());
}

@Override
public void removeRoom(Long roomId) {
roomMap.remove(roomId);
}

// 테스트 전용 메소드
public Room getRoomForTest(Long roomId) {
return roomMap.get(roomId);
Expand Down
Loading