diff --git a/backend/src/main/java/io/f1/backend/domain/game/app/GameService.java b/backend/src/main/java/io/f1/backend/domain/game/app/GameService.java new file mode 100644 index 00000000..12cbd7ed --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/app/GameService.java @@ -0,0 +1,77 @@ +package io.f1.backend.domain.game.app; + +import io.f1.backend.domain.game.dto.GameStartData; +import io.f1.backend.domain.game.dto.response.GameStartResponse; +import io.f1.backend.domain.game.event.RoomUpdatedEvent; +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.RoomState; +import io.f1.backend.domain.game.store.RoomRepository; +import io.f1.backend.domain.quiz.app.QuizService; +import io.f1.backend.domain.quiz.entity.Quiz; + +import lombok.RequiredArgsConstructor; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class GameService { + + private final QuizService quizService; + private final RoomRepository roomRepository; + private final ApplicationEventPublisher eventPublisher; + + public GameStartData gameStart(Long roomId, Long quizId) { + + Room room = + roomRepository + .findRoom(roomId) + .orElseThrow(() -> new IllegalArgumentException("404 존재하지 않는 방입니다.")); + + if (!validateReadyStatus(room)) { + throw new IllegalArgumentException("E403004 : 레디 상태가 아닙니다."); + } + + // 방의 gameSetting에 설정된 퀴즈랑 요청 퀴즈랑 같은지 체크 후 GameSetting에서 라운드 가져오기 + Integer round = checkGameSetting(room, quizId); + + Quiz quiz = quizService.getQuizWithQuestionsById(quizId); + + // 라운드 수만큼 랜덤 Question 추출 + GameStartResponse questions = quizService.getRandomQuestionsWithoutAnswer(quizId, round); + + // 방 정보 게임 중으로 변경 + room.updateRoomState(RoomState.PLAYING); + + eventPublisher.publishEvent(new RoomUpdatedEvent(room, quiz)); + + return new GameStartData(getDestination(roomId), questions); + } + + private Integer checkGameSetting(Room room, Long quizId) { + + GameSetting gameSetting = room.getGameSetting(); + + if (!gameSetting.checkQuizId(quizId)) { + throw new IllegalArgumentException("E409002 : 게임 설정이 다릅니다. (게임을 시작할 수 없습니다.)"); + } + + return gameSetting.getRound(); + } + + private boolean validateReadyStatus(Room room) { + + Map playerSessionMap = room.getPlayerSessionMap(); + + return playerSessionMap.values().stream().allMatch(Player::isReady); + } + + private static String getDestination(Long roomId) { + return "/sub/room/" + roomId; + } +} 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 27c1522c..dd31a875 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 @@ -60,7 +60,7 @@ public class RoomService { public RoomCreateResponse saveRoom(RoomCreateRequest request) { Long quizMinId = quizService.getQuizMinId(); - Quiz quiz = quizService.getQuizById(quizMinId); + Quiz quiz = quizService.getQuizWithQuestionsById(quizMinId); GameSetting gameSetting = toGameSetting(quiz); @@ -132,7 +132,7 @@ public RoomInitialData initializeRoomSocket(Long roomId, String sessionId) { RoomSettingResponse roomSettingResponse = toRoomSettingResponse(room); Long quizId = room.getGameSetting().getQuizId(); - Quiz quiz = quizService.getQuizById(quizId); + Quiz quiz = quizService.getQuizWithQuestionsById(quizId); GameSettingResponse gameSettingResponse = toGameSettingResponse(room.getGameSetting(), quiz); @@ -189,7 +189,7 @@ public RoomListResponse getAllRooms() { .map( room -> { Long quizId = room.getGameSetting().getQuizId(); - Quiz quiz = quizService.getQuizById(quizId); + Quiz quiz = quizService.getQuizWithQuestionsById(quizId); return toRoomResponse(room, quiz); }) diff --git a/backend/src/main/java/io/f1/backend/domain/game/dto/GameStartData.java b/backend/src/main/java/io/f1/backend/domain/game/dto/GameStartData.java new file mode 100644 index 00000000..c68d056e --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/dto/GameStartData.java @@ -0,0 +1,5 @@ +package io.f1.backend.domain.game.dto; + +import io.f1.backend.domain.game.dto.response.GameStartResponse; + +public record GameStartData(String destination, GameStartResponse gameStartResponse) {} 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 f6d40420..22dfdee2 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 @@ -5,4 +5,5 @@ public enum MessageType { GAME_SETTING, PLAYER_LIST, SYSTEM_NOTICE, + GAME_START, } diff --git a/backend/src/main/java/io/f1/backend/domain/game/dto/request/GameStartRequest.java b/backend/src/main/java/io/f1/backend/domain/game/dto/request/GameStartRequest.java new file mode 100644 index 00000000..61f792ea --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/dto/request/GameStartRequest.java @@ -0,0 +1,3 @@ +package io.f1.backend.domain.game.dto.request; + +public record GameStartRequest(Long quizId) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/dto/response/GameStartResponse.java b/backend/src/main/java/io/f1/backend/domain/game/dto/response/GameStartResponse.java new file mode 100644 index 00000000..9545a8ff --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/game/dto/response/GameStartResponse.java @@ -0,0 +1,7 @@ +package io.f1.backend.domain.game.dto.response; + +import io.f1.backend.domain.quiz.dto.GameQuestionResponse; + +import java.util.List; + +public record GameStartResponse(List questions) {} diff --git a/backend/src/main/java/io/f1/backend/domain/game/model/GameSetting.java b/backend/src/main/java/io/f1/backend/domain/game/model/GameSetting.java index 7643d861..7634d114 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/model/GameSetting.java +++ b/backend/src/main/java/io/f1/backend/domain/game/model/GameSetting.java @@ -10,4 +10,11 @@ public class GameSetting { private Long quizId; private Integer round; // 게임 변경 시 해당 게임의 총 문제 수로 설정 private int timeLimit = 60; + + public boolean checkQuizId(Long quizId) { + if (this.quizId != null && this.quizId.equals(quizId)) { + return false; + } + return true; + } } 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 64310c3f..d42abc8d 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 @@ -46,6 +46,10 @@ public void updateHost(Player nextHost) { this.host = nextHost; } + public void updateRoomState(RoomState newState) { + this.state = newState; + } + public void removeUserId(Long id) { this.userIdSessionMap.remove(id); } 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 4cebfc09..e84e9c36 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 @@ -1,9 +1,12 @@ package io.f1.backend.domain.game.websocket; +import io.f1.backend.domain.game.app.GameService; import io.f1.backend.domain.game.app.RoomService; +import io.f1.backend.domain.game.dto.GameStartData; 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 io.f1.backend.domain.game.dto.request.GameStartRequest; import lombok.RequiredArgsConstructor; @@ -19,6 +22,7 @@ public class GameSocketController { private final MessageSender messageSender; private final RoomService roomService; + private final GameService gameService; @MessageMapping("/room/initializeRoomSocket/{roomId}") public void initializeRoomSocket(@DestinationVariable Long roomId, Message message) { @@ -56,6 +60,18 @@ public void exitRoom(@DestinationVariable Long roomId, Message message) { } } + @MessageMapping("/room/start/{roomId}") + public void gameStart(@DestinationVariable Long roomId, Message message) { + + Long quizId = message.getPayload().quizId(); + + GameStartData gameStartData = gameService.gameStart(roomId, quizId); + + String destination = gameStartData.destination(); + + messageSender.send(destination, MessageType.GAME_START, gameStartData.gameStartResponse()); + } + 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 a74f0658..ce705fb6 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 @@ -4,8 +4,10 @@ import static java.nio.file.Files.deleteIfExists; +import io.f1.backend.domain.game.dto.response.GameStartResponse; import io.f1.backend.domain.question.app.QuestionService; import io.f1.backend.domain.question.dto.QuestionRequest; +import io.f1.backend.domain.question.entity.Question; import io.f1.backend.domain.quiz.dao.QuizRepository; import io.f1.backend.domain.quiz.dto.QuizCreateRequest; import io.f1.backend.domain.quiz.dto.QuizCreateResponse; @@ -214,12 +216,11 @@ public QuizListPageResponse getQuizzes(String title, String creator, Pageable pa } @Transactional(readOnly = true) - public Quiz getQuizById(Long quizId) { + public Quiz getQuizWithQuestionsById(Long quizId) { Quiz quiz = quizRepository - .findById(quizId) + .findQuizWithQuestionsById(quizId) .orElseThrow(() -> new RuntimeException("E404002: 존재하지 않는 퀴즈입니다.")); - quiz.getQuestions().size(); return quiz; } @@ -228,6 +229,7 @@ public Long getQuizMinId() { return quizRepository.getQuizMinId(); } + @Transactional(readOnly = true) public QuizQuestionListResponse getQuizWithQuestions(Long quizId) { Quiz quiz = quizRepository @@ -236,4 +238,15 @@ public QuizQuestionListResponse getQuizWithQuestions(Long quizId) { return quizToQuizQuestionListResponse(quiz); } + + @Transactional(readOnly = true) + public GameStartResponse getRandomQuestionsWithoutAnswer(Long quizId, Integer round) { + quizRepository + .findById(quizId) + .orElseThrow(() -> new NoSuchElementException("존재하지 않는 퀴즈입니다.")); + + List randomQuestions = quizRepository.findRandQuestionsByQuizId(quizId, round); + + return toGameStartResponse(randomQuestions); + } } 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 d5021088..b2d1728d 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 @@ -1,5 +1,6 @@ package io.f1.backend.domain.quiz.dao; +import io.f1.backend.domain.question.entity.Question; import io.f1.backend.domain.quiz.entity.Quiz; import org.springframework.data.domain.Page; @@ -7,12 +8,23 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.List; +import java.util.Optional; + public interface QuizRepository extends JpaRepository { Page findQuizzesByTitleContaining(String title, Pageable pageable); Page findQuizzesByCreator_NicknameContaining(String creator, Pageable pageable); + @Query("SELECT q FROM Quiz q LEFT JOIN FETCH q.questions WHERE q.id = :quizId") + Optional findQuizWithQuestionsById(Long quizId); + @Query("SELECT MIN(q.id) FROM Quiz q") Long getQuizMinId(); + + @Query( + value = "SELECT * FROM question WHERE quiz_id = :quizId ORDER BY RAND() LIMIT :round", + nativeQuery = true) + List findRandQuestionsByQuizId(Long quizId, Integer round); } diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/dto/GameQuestionResponse.java b/backend/src/main/java/io/f1/backend/domain/quiz/dto/GameQuestionResponse.java new file mode 100644 index 00000000..a47b93d9 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/domain/quiz/dto/GameQuestionResponse.java @@ -0,0 +1,3 @@ +package io.f1.backend.domain.quiz.dto; + +public record GameQuestionResponse(Long id, String question) {} diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java b/backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java index b515f679..0c0c0513 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/mapper/QuizMapper.java @@ -1,7 +1,9 @@ package io.f1.backend.domain.quiz.mapper; +import io.f1.backend.domain.game.dto.response.GameStartResponse; import io.f1.backend.domain.question.dto.QuestionResponse; import io.f1.backend.domain.question.entity.Question; +import io.f1.backend.domain.quiz.dto.GameQuestionResponse; import io.f1.backend.domain.quiz.dto.QuizCreateRequest; import io.f1.backend.domain.quiz.dto.QuizCreateResponse; import io.f1.backend.domain.quiz.dto.QuizListPageResponse; @@ -83,4 +85,16 @@ public static QuizQuestionListResponse quizToQuizQuestionListResponse(Quiz quiz) quiz.getQuestions().size(), questionsToQuestionResponses(quiz.getQuestions())); } + + public static List toGameQuestionResponseList(List questions) { + return questions.stream().map(QuizMapper::toGameQuestionResponse).toList(); + } + + public static GameQuestionResponse toGameQuestionResponse(Question question) { + return new GameQuestionResponse(question.getId(), question.getTextQuestion().getContent()); + } + + public static GameStartResponse toGameStartResponse(List questions) { + return new GameStartResponse(toGameQuestionResponseList(questions)); + } }