diff --git a/backend/src/main/java/io/f1/backend/domain/game/api/RoomController.java b/backend/src/main/java/io/f1/backend/domain/game/api/RoomController.java index ffc33605..79a245de 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/api/RoomController.java +++ b/backend/src/main/java/io/f1/backend/domain/game/api/RoomController.java @@ -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 @@ -31,12 +28,7 @@ public class RoomController { @PostMapping @ResponseStatus(HttpStatus.CREATED) public RoomCreateResponse saveRoom(@RequestBody @Valid RoomCreateRequest request) { - - Map loginUser = new HashMap<>(); - loginUser.put("id", 1L); - loginUser.put("nickname", "빵야빵야"); - - return roomService.saveRoom(request, loginUser); + return roomService.saveRoom(request); } @PostMapping("/validation") diff --git a/backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java b/backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java index 1ae0e82a..723c8355 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java +++ b/backend/src/main/java/io/f1/backend/domain/game/app/RoomService.java @@ -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; @@ -29,6 +39,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; @Service @@ -40,12 +51,15 @@ public class RoomService { private final AtomicLong roomIdGenerator = new AtomicLong(0); private final ApplicationEventPublisher eventPublisher; - public RoomCreateResponse saveRoom(RoomCreateRequest request, Map 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(); @@ -54,9 +68,6 @@ public RoomCreateResponse saveRoom(RoomCreateRequest request, Map new IllegalArgumentException("404 존재하지 않는 방입니다.")); + .orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다.-1")); if (room.getState().equals(RoomState.PLAYING)) { throw new IllegalArgumentException("403 게임이 진행중입니다."); @@ -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(); Map 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) { + Room room = + roomRepository + .findRoom(roomId) + .orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다.")); + + Map 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 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); } public RoomListResponse getAllRooms() { @@ -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()); + } } diff --git a/backend/src/main/java/io/f1/backend/domain/game/dto/MessageType.java b/backend/src/main/java/io/f1/backend/domain/game/dto/MessageType.java index bb748aac..f6d40420 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/dto/MessageType.java +++ b/backend/src/main/java/io/f1/backend/domain/game/dto/MessageType.java @@ -4,4 +4,5 @@ public enum MessageType { ROOM_SETTING, GAME_SETTING, PLAYER_LIST, + SYSTEM_NOTICE, } diff --git a/backend/src/main/java/io/f1/backend/domain/game/dto/RoomEventType.java b/backend/src/main/java/io/f1/backend/domain/game/dto/RoomEventType.java new file mode 100644 index 00000000..c6346c83 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/dto/RoomEventType.java @@ -0,0 +1,8 @@ +package io.f1.backend.domain.game.dto; + +public enum RoomEventType { + ENTER, + EXIT, + START, + END, +} diff --git a/backend/src/main/java/io/f1/backend/domain/game/dto/RoomExitData.java b/backend/src/main/java/io/f1/backend/domain/game/dto/RoomExitData.java new file mode 100644 index 00000000..1fc51f2e --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/dto/RoomExitData.java @@ -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; + } +} diff --git a/backend/src/main/java/io/f1/backend/domain/game/dto/RoomInitialData.java b/backend/src/main/java/io/f1/backend/domain/game/dto/RoomInitialData.java index ecc16eca..c925b690 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/dto/RoomInitialData.java +++ b/backend/src/main/java/io/f1/backend/domain/game/dto/RoomInitialData.java @@ -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) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/dto/response/SystemNoticeResponse.java b/backend/src/main/java/io/f1/backend/domain/game/dto/response/SystemNoticeResponse.java new file mode 100644 index 00000000..911fbf50 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/dto/response/SystemNoticeResponse.java @@ -0,0 +1,5 @@ +package io.f1.backend.domain.game.dto.response; + +import java.time.Instant; + +public record SystemNoticeResponse(String noticeMessage, Instant timestamp) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/mapper/RoomMapper.java b/backend/src/main/java/io/f1/backend/domain/game/mapper/RoomMapper.java index 9044c41d..cd3d109f 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/mapper/RoomMapper.java +++ b/backend/src/main/java/io/f1/backend/domain/game/mapper/RoomMapper.java @@ -1,5 +1,6 @@ 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; @@ -7,20 +8,29 @@ 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(), @@ -28,9 +38,9 @@ public static RoomSettingResponse toRoomSettingResponse(Room room) { 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) { @@ -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()); + } } diff --git a/backend/src/main/java/io/f1/backend/domain/game/model/Room.java b/backend/src/main/java/io/f1/backend/domain/game/model/Room.java index ba0c56b5..00b7e71b 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/model/Room.java +++ b/backend/src/main/java/io/f1/backend/domain/game/model/Room.java @@ -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; + } } diff --git a/backend/src/main/java/io/f1/backend/domain/game/store/RoomRepository.java b/backend/src/main/java/io/f1/backend/domain/game/store/RoomRepository.java index 83f1b415..6e87760d 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/store/RoomRepository.java +++ b/backend/src/main/java/io/f1/backend/domain/game/store/RoomRepository.java @@ -11,4 +11,6 @@ public interface RoomRepository { Optional findRoom(Long roomId); List findAll(); + + void removeRoom(Long roomId); } diff --git a/backend/src/main/java/io/f1/backend/domain/game/store/RoomRepositoryImpl.java b/backend/src/main/java/io/f1/backend/domain/game/store/RoomRepositoryImpl.java index 4daa0a27..cffbba47 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/store/RoomRepositoryImpl.java +++ b/backend/src/main/java/io/f1/backend/domain/game/store/RoomRepositoryImpl.java @@ -30,6 +30,11 @@ public List findAll() { return new ArrayList<>(roomMap.values()); } + @Override + public void removeRoom(Long roomId) { + roomMap.remove(roomId); + } + // 테스트 전용 메소드 public Room getRoomForTest(Long roomId) { return roomMap.get(roomId); diff --git a/backend/src/main/java/io/f1/backend/domain/game/websocket/GameSocketController.java b/backend/src/main/java/io/f1/backend/domain/game/websocket/GameSocketController.java index a93f382c..9bf2f17f 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/websocket/GameSocketController.java +++ b/backend/src/main/java/io/f1/backend/domain/game/websocket/GameSocketController.java @@ -2,6 +2,7 @@ import io.f1.backend.domain.game.app.RoomService; import io.f1.backend.domain.game.dto.MessageType; +import io.f1.backend.domain.game.dto.RoomExitData; import io.f1.backend.domain.game.dto.RoomInitialData; import lombok.RequiredArgsConstructor; @@ -22,8 +23,7 @@ public class GameSocketController { @MessageMapping("/room/enter/{roomId}") public void roomEnter(@DestinationVariable Long roomId, Message message) { - StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); - String websocketSessionId = accessor.getSessionId(); + String websocketSessionId = getSessionId(message); RoomInitialData roomInitialData = roomService.enterRoom(roomId, websocketSessionId); String destination = roomInitialData.destination(); @@ -34,5 +34,29 @@ public void roomEnter(@DestinationVariable Long roomId, Message message) { destination, MessageType.GAME_SETTING, roomInitialData.gameSettingResponse()); messageSender.send( destination, MessageType.PLAYER_LIST, roomInitialData.playerListResponse()); + messageSender.send( + destination, MessageType.SYSTEM_NOTICE, roomInitialData.systemNoticeResponse()); + } + + @MessageMapping("/room/exit/{roomId}") + public void exitRoom(@DestinationVariable Long roomId, Message message) { + + String websocketSessionId = getSessionId(message); + + RoomExitData roomExitData = roomService.exitRoom(roomId, websocketSessionId); + + String destination = roomExitData.getDestination(); + + if (!roomExitData.isRemovedRoom()) { + messageSender.send( + destination, MessageType.PLAYER_LIST, roomExitData.getPlayerListResponses()); + messageSender.send( + destination, MessageType.SYSTEM_NOTICE, roomExitData.getSystemNoticeResponse()); + } + } + + private static String getSessionId(Message message) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + return accessor.getSessionId(); } } diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java b/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java index 94a8dba4..22caf65e 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/app/QuizService.java @@ -194,8 +194,16 @@ public QuizListPageResponse getQuizzes(String title, String creator, Pageable pa @Transactional(readOnly = true) public Quiz getQuizById(Long quizId) { - return quizRepository - .findById(quizId) - .orElseThrow(() -> new RuntimeException("E404002: 존재하지 않는 퀴즈입니다.")); + Quiz quiz = + quizRepository + .findById(quizId) + .orElseThrow(() -> new RuntimeException("E404002: 존재하지 않는 퀴즈입니다.")); + quiz.getQuestions().size(); + return quiz; + } + + @Transactional(readOnly = true) + public Long getQuizMinId() { + return quizRepository.getQuizMinId(); } } diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/dao/QuizRepository.java b/backend/src/main/java/io/f1/backend/domain/quiz/dao/QuizRepository.java index 82b6bd16..d5021088 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/dao/QuizRepository.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/dao/QuizRepository.java @@ -5,10 +5,14 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface QuizRepository extends JpaRepository { Page findQuizzesByTitleContaining(String title, Pageable pageable); Page findQuizzesByCreator_NicknameContaining(String creator, Pageable pageable); + + @Query("SELECT MIN(q.id) FROM Quiz q") + Long getQuizMinId(); }