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 index 6e74acc4..4acd134d 100644 --- 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 @@ -96,6 +96,7 @@ public void gameStart(Long roomId, UserPrincipal principal) { public void onCorrectAnswer(GameCorrectAnswerEvent event) { Room room = event.room(); + log.debug(room.getId() + "번 방 채팅으로 정답! 현재 라운드 : " + room.getCurrentRound()); String sessionId = event.sessionId(); ChatMessage chatMessage = event.chatMessage(); String answer = event.answer(); @@ -135,6 +136,8 @@ public void onCorrectAnswer(GameCorrectAnswerEvent event) { @EventListener public void onTimeout(GameTimeoutEvent event) { Room room = event.room(); + log.debug(room.getId() + "번 방 타임아웃! 현재 라운드 : " + room.getCurrentRound()); + String destination = getDestination(room.getId()); messageSender.sendBroadcast( diff --git a/backend/src/main/java/io/f1/backend/domain/game/app/TimerService.java b/backend/src/main/java/io/f1/backend/domain/game/app/TimerService.java index 93a06df8..4939e19e 100644 --- a/backend/src/main/java/io/f1/backend/domain/game/app/TimerService.java +++ b/backend/src/main/java/io/f1/backend/domain/game/app/TimerService.java @@ -4,6 +4,7 @@ import io.f1.backend.domain.game.model.Room; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -11,6 +12,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +@Slf4j @Service @RequiredArgsConstructor public class TimerService { @@ -18,6 +20,7 @@ public class TimerService { private final ApplicationEventPublisher eventPublisher; public void startTimer(Room room, int delaySec) { + log.debug(room.getId() + "번 방 타이머 시작 ! 현재 라운드 : " + room.getCurrentRound()); cancelTimer(room); ScheduledFuture timer = @@ -43,6 +46,7 @@ public boolean validateCurrentRound(Room room) { public boolean cancelTimer(Room room) { // 정답 맞혔어요 ~ 타이머 캔슬 부탁 + log.debug(room.getId() + "번 방 타이머 취소 ! 현재 라운드 : " + room.getCurrentRound()); ScheduledFuture timer = room.getTimer(); if (timer != null && !timer.isDone()) { return timer.cancel(false); diff --git a/backend/src/main/java/io/f1/backend/domain/question/api/QuestionController.java b/backend/src/main/java/io/f1/backend/domain/question/api/QuestionController.java index 328ba7d9..8f361202 100644 --- a/backend/src/main/java/io/f1/backend/domain/question/api/QuestionController.java +++ b/backend/src/main/java/io/f1/backend/domain/question/api/QuestionController.java @@ -1,15 +1,12 @@ package io.f1.backend.domain.question.api; import io.f1.backend.domain.question.app.QuestionService; -import io.f1.backend.domain.question.dto.QuestionUpdateRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -20,21 +17,6 @@ public class QuestionController { private final QuestionService questionService; - @PutMapping("/{questionId}") - public ResponseEntity updateQuestion( - @PathVariable Long questionId, @RequestBody QuestionUpdateRequest request) { - - if (request.content() != null) { - questionService.updateQuestionContent(questionId, request.content()); - } - - if (request.content() != null) { - questionService.updateQuestionAnswer(questionId, request.answer()); - } - - return ResponseEntity.noContent().build(); - } - @DeleteMapping("/{questionId}") public ResponseEntity deleteQuestion(@PathVariable Long questionId) { questionService.deleteQuestion(questionId); diff --git a/backend/src/main/java/io/f1/backend/domain/question/app/QuestionService.java b/backend/src/main/java/io/f1/backend/domain/question/app/QuestionService.java index d759cd99..45862ace 100644 --- a/backend/src/main/java/io/f1/backend/domain/question/app/QuestionService.java +++ b/backend/src/main/java/io/f1/backend/domain/question/app/QuestionService.java @@ -2,26 +2,23 @@ import static io.f1.backend.domain.question.mapper.QuestionMapper.questionRequestToQuestion; import static io.f1.backend.domain.question.mapper.TextQuestionMapper.questionRequestToTextQuestion; +import static io.f1.backend.domain.quiz.app.QuizService.verifyUserAuthority; import io.f1.backend.domain.question.dao.QuestionRepository; import io.f1.backend.domain.question.dao.TextQuestionRepository; import io.f1.backend.domain.question.dto.QuestionRequest; +import io.f1.backend.domain.question.dto.QuestionUpdateRequest; import io.f1.backend.domain.question.entity.Question; import io.f1.backend.domain.question.entity.TextQuestion; import io.f1.backend.domain.quiz.entity.Quiz; import io.f1.backend.global.exception.CustomException; -import io.f1.backend.global.exception.errorcode.AuthErrorCode; import io.f1.backend.global.exception.errorcode.QuestionErrorCode; -import io.f1.backend.global.security.enums.Role; -import io.f1.backend.global.util.SecurityUtils; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Objects; - @Service @RequiredArgsConstructor public class QuestionService { @@ -41,46 +38,22 @@ public void saveQuestion(Quiz quiz, QuestionRequest request) { question.addTextQuestion(textQuestion); } - @Transactional - public void updateQuestionContent(Long questionId, String content) { - - validateContent(content); + public void updateQuestions(Quiz quiz, QuestionUpdateRequest request) { - Question question = - questionRepository - .findById(questionId) - .orElseThrow( - () -> new CustomException(QuestionErrorCode.QUESTION_NOT_FOUND)); - - verifyUserAuthority(question.getQuiz()); - - TextQuestion textQuestion = question.getTextQuestion(); - textQuestion.changeContent(content); - } - - private static void verifyUserAuthority(Quiz quiz) { - if (SecurityUtils.getCurrentUserRole() == Role.ADMIN) { + if (request.getId() == null) { + saveQuestion(quiz, QuestionRequest.of(request)); return; } - if (!Objects.equals(SecurityUtils.getCurrentUserId(), quiz.getCreator().getId())) { - throw new CustomException(AuthErrorCode.FORBIDDEN); - } - } - - @Transactional - public void updateQuestionAnswer(Long questionId, String answer) { - - validateAnswer(answer); Question question = questionRepository - .findById(questionId) + .findById(request.getId()) .orElseThrow( () -> new CustomException(QuestionErrorCode.QUESTION_NOT_FOUND)); - verifyUserAuthority(question.getQuiz()); - - question.changeAnswer(answer); + TextQuestion textQuestion = question.getTextQuestion(); + textQuestion.changeContent(request.getContent()); + question.changeAnswer(request.getAnswer()); } @Transactional @@ -96,16 +69,4 @@ public void deleteQuestion(Long questionId) { questionRepository.delete(question); } - - private void validateAnswer(String answer) { - if (answer.trim().length() < 5 || answer.trim().length() > 30) { - throw new CustomException(QuestionErrorCode.INVALID_ANSWER_LENGTH); - } - } - - private void validateContent(String content) { - if (content.trim().length() < 5 || content.trim().length() > 30) { - throw new CustomException(QuestionErrorCode.INVALID_CONTENT_LENGTH); - } - } } diff --git a/backend/src/main/java/io/f1/backend/domain/question/dto/QuestionRequest.java b/backend/src/main/java/io/f1/backend/domain/question/dto/QuestionRequest.java index 4cdce5ff..60274656 100644 --- a/backend/src/main/java/io/f1/backend/domain/question/dto/QuestionRequest.java +++ b/backend/src/main/java/io/f1/backend/domain/question/dto/QuestionRequest.java @@ -19,4 +19,12 @@ public class QuestionRequest { @TrimmedSize(min = 1, max = 30) @NotBlank(message = "정답을 입력해주세요.") private String answer; + + public static QuestionRequest of(QuestionUpdateRequest request) { + QuestionRequest questionRequest = new QuestionRequest(); + questionRequest.content = request.getContent(); + questionRequest.answer = request.getAnswer(); + + return questionRequest; + } } diff --git a/backend/src/main/java/io/f1/backend/domain/question/dto/QuestionUpdateRequest.java b/backend/src/main/java/io/f1/backend/domain/question/dto/QuestionUpdateRequest.java index e77a3dea..7cff28d2 100644 --- a/backend/src/main/java/io/f1/backend/domain/question/dto/QuestionUpdateRequest.java +++ b/backend/src/main/java/io/f1/backend/domain/question/dto/QuestionUpdateRequest.java @@ -1,3 +1,24 @@ package io.f1.backend.domain.question.dto; -public record QuestionUpdateRequest(String content, String answer) {} +import io.f1.backend.global.validation.TrimmedSize; + +import jakarta.validation.constraints.NotBlank; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class QuestionUpdateRequest { + + private Long id; + + @TrimmedSize(min = 5, max = 30) + @NotBlank(message = "문제를 입력해주세요.") + private String content; + + @TrimmedSize(min = 1, max = 30) + @NotBlank(message = "정답을 입력해주세요.") + private String answer; +} diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java b/backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java index 4d7a0d60..55a62f7d 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/api/QuizController.java @@ -57,15 +57,9 @@ public ResponseEntity deleteQuiz(@PathVariable Long quizId) { public ResponseEntity updateQuiz( @PathVariable Long quizId, @RequestPart(required = false) MultipartFile thumbnailFile, - @RequestPart QuizUpdateRequest request) { + @Valid @RequestPart QuizUpdateRequest request) { - if (request.title() != null) { - quizService.updateQuizTitle(quizId, request.title()); - } - - if (request.description() != null) { - quizService.updateQuizDesc(quizId, request.description()); - } + quizService.updateQuizAndQuestions(quizId, request); if (thumbnailFile != null && !thumbnailFile.isEmpty()) { quizService.updateThumbnail(quizId, thumbnailFile); 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 bc85f2af..6985b5e0 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 @@ -6,6 +6,7 @@ import io.f1.backend.domain.question.app.QuestionService; import io.f1.backend.domain.question.dto.QuestionRequest; +import io.f1.backend.domain.question.dto.QuestionUpdateRequest; import io.f1.backend.domain.question.entity.Question; import io.f1.backend.domain.quiz.dao.QuizRepository; import io.f1.backend.domain.quiz.dto.QuizCreateRequest; @@ -14,6 +15,7 @@ import io.f1.backend.domain.quiz.dto.QuizListResponse; import io.f1.backend.domain.quiz.dto.QuizMinData; import io.f1.backend.domain.quiz.dto.QuizQuestionListResponse; +import io.f1.backend.domain.quiz.dto.QuizUpdateRequest; import io.f1.backend.domain.quiz.entity.Quiz; import io.f1.backend.domain.user.dao.UserRepository; import io.f1.backend.domain.user.entity.User; @@ -138,7 +140,7 @@ public void deleteQuiz(Long quizId) { quizRepository.deleteById(quizId); } - private static void verifyUserAuthority(Quiz quiz) { + public static void verifyUserAuthority(Quiz quiz) { if (SecurityUtils.getCurrentUserRole() == Role.ADMIN) { return; } @@ -148,7 +150,7 @@ private static void verifyUserAuthority(Quiz quiz) { } @Transactional - public void updateQuizTitle(Long quizId, String title) { + public void updateQuizAndQuestions(Long quizId, QuizUpdateRequest request) { Quiz quiz = quizRepository .findById(quizId) @@ -156,22 +158,14 @@ public void updateQuizTitle(Long quizId, String title) { verifyUserAuthority(quiz); - validateTitle(title); - quiz.changeTitle(title); - } - - @Transactional - public void updateQuizDesc(Long quizId, String description) { - - Quiz quiz = - quizRepository - .findById(quizId) - .orElseThrow(() -> new CustomException(QuizErrorCode.QUIZ_NOT_FOUND)); + quiz.changeTitle(request.getTitle()); + quiz.changeDescription(request.getDescription()); - verifyUserAuthority(quiz); + List questionReqList = request.getQuestions(); - validateDesc(description); - quiz.changeDescription(description); + for (QuestionUpdateRequest questionReq : questionReqList) { + questionService.updateQuestions(quiz, questionReq); + } } @Transactional @@ -191,18 +185,6 @@ public void updateThumbnail(Long quizId, MultipartFile thumbnailFile) { quiz.changeThumbnailUrl(newThumbnailPath); } - private void validateDesc(String desc) { - if (desc.trim().length() < 10 || desc.trim().length() > 50) { - throw new CustomException(QuizErrorCode.INVALID_DESC_LENGTH); - } - } - - private void validateTitle(String title) { - if (title.trim().length() < 2 || title.trim().length() > 30) { - throw new CustomException(QuizErrorCode.INVALID_TITLE_LENGTH); - } - } - private void deleteThumbnailFile(String oldFilename) { if (oldFilename.contains(DEFAULT)) { return; diff --git a/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizUpdateRequest.java b/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizUpdateRequest.java index 30065d4f..d1de1be7 100644 --- a/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizUpdateRequest.java +++ b/backend/src/main/java/io/f1/backend/domain/quiz/dto/QuizUpdateRequest.java @@ -1,3 +1,29 @@ package io.f1.backend.domain.quiz.dto; -public record QuizUpdateRequest(String title, String description) {} +import io.f1.backend.domain.question.dto.QuestionUpdateRequest; +import io.f1.backend.global.validation.TrimmedSize; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class QuizUpdateRequest { + + @TrimmedSize(min = 2, max = 30) + @NotBlank(message = "퀴즈 제목을 설정해주세요.") + private String title; + + @TrimmedSize(min = 10, max = 50) + @NotBlank(message = "퀴즈 설명을 적어주세요.") + private String description; + + @Size(min = 10, max = 80, message = "문제는 최소 10개, 최대 80개로 정해주세요.") + private List questions; +} diff --git a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepositoryAdapter.java b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepositoryAdapter.java index d95aace5..957b1c0b 100644 --- a/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepositoryAdapter.java +++ b/backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepositoryAdapter.java @@ -10,8 +10,6 @@ import io.f1.backend.global.exception.errorcode.RoomErrorCode; import io.f1.backend.global.exception.errorcode.UserErrorCode; -import jakarta.annotation.PostConstruct; - import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -30,7 +28,6 @@ public class StatRepositoryAdapter implements StatRepository { private final StatJpaRepository jpaRepository; private final StatRedisRepository redisRepository; - @PostConstruct public void setup() { redisRepository.setup(); warmingRedis(); diff --git a/backend/src/main/java/io/f1/backend/global/config/RedisConfig.java b/backend/src/main/java/io/f1/backend/global/config/RedisConfig.java index d1d4b7f0..7394e559 100644 --- a/backend/src/main/java/io/f1/backend/global/config/RedisConfig.java +++ b/backend/src/main/java/io/f1/backend/global/config/RedisConfig.java @@ -1,7 +1,9 @@ package io.f1.backend.global.config; +import io.f1.backend.domain.stat.dao.StatRepositoryAdapter; import io.f1.backend.global.util.RedisUserSubscriber; +import org.springframework.boot.ApplicationRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; @@ -38,4 +40,9 @@ public RedisMessageListenerContainer redisMessageListenerContainer( container.addMessageListener(redisUserSubscriber, new PatternTopic("user-*")); return container; } + + @Bean + ApplicationRunner redisWarmingRunner(StatRepositoryAdapter statRepositoryAdapter) { + return args -> statRepositoryAdapter.setup(); + } } diff --git a/backend/src/main/java/io/f1/backend/global/config/SecurityConfig.java b/backend/src/main/java/io/f1/backend/global/config/SecurityConfig.java index 949e60dc..cb176ceb 100644 --- a/backend/src/main/java/io/f1/backend/global/config/SecurityConfig.java +++ b/backend/src/main/java/io/f1/backend/global/config/SecurityConfig.java @@ -50,7 +50,8 @@ public SecurityFilterChain userFilterChain(HttpSecurity http) throws Exception { "/signup", "/css/**", "/js/**", - "/admin/login") + "/admin/login", + actuatorBasePath + "/**") .permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**") .permitAll() @@ -83,7 +84,6 @@ public SecurityFilterChain userFilterChain(HttpSecurity http) throws Exception { userInfo.userService( customOAuthUserService)) .successHandler(oAuthSuccessHandler)) - .httpBasic(Customizer.withDefaults()) .logout( logout -> logout.logoutUrl("/logout") diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 714be876..ec6edd51 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -76,12 +76,6 @@ spring: activate: on-profile: dev - security: - user: - name: ${PROM_NAME} - password: ${PROM_PASSWORD} - roles: PROMETHEUS - management: server: port: ${ACTUATOR_PORT} @@ -99,12 +93,6 @@ spring: activate: on-profile: prod - security: - user: - name: ${PROM_NAME} - password: ${PROM_PASSWORD} - roles: PROMETHEUS - # TODO: Flyway 추가와 함께 설정이 필요한 항목들 # sql: # init: