From 517d231ac205c68f81402073c1632b4520ed71dc Mon Sep 17 00:00:00 2001 From: sezeme Date: Tue, 10 Feb 2026 00:04:27 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A0=95=EB=8B=B5=20=EB=A6=AC=ED=84=B4?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/controller/QuizController.java | 13 +++ .../java/com/blaybus/backend/dto/QuizDto.java | 18 ++-- .../backend/exception/CommonErrorCode.java | 1 + .../backend/service/QuizGradingService.java | 9 +- .../blaybus/backend/service/QuizService.java | 21 +++++ .../resources/data/initial_quiz_data.json | 10 +-- .../backend/service/QuizServiceTest.java | 85 +++++++++++++++++++ 7 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 src/test/java/com/blaybus/backend/service/QuizServiceTest.java diff --git a/src/main/java/com/blaybus/backend/controller/QuizController.java b/src/main/java/com/blaybus/backend/controller/QuizController.java index e304bad..59178ce 100644 --- a/src/main/java/com/blaybus/backend/controller/QuizController.java +++ b/src/main/java/com/blaybus/backend/controller/QuizController.java @@ -2,6 +2,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -56,4 +57,16 @@ public ResponseEntity grade( QuizDto.GradeResponse response = gradingService.grade(quizId, request.answer(), user); return ResponseEntity.ok(response); } + + @PatchMapping("/progress") + public ResponseEntity syncProgress( + @AuthenticationPrincipal CustomUserDetails userDetails, + @PathVariable Long sceneId, + @Valid @RequestBody QuizDto.SyncProgressRequest request) { + User user = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new BusinessException(CommonErrorCode.USER_NOT_FOUND)); + + quizService.syncProgress(sceneId, request, user); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/blaybus/backend/dto/QuizDto.java b/src/main/java/com/blaybus/backend/dto/QuizDto.java index aba38c2..6135dcd 100644 --- a/src/main/java/com/blaybus/backend/dto/QuizDto.java +++ b/src/main/java/com/blaybus/backend/dto/QuizDto.java @@ -4,13 +4,21 @@ public class QuizDto { - public record GradeRequest(@NotBlank - String answer) { + public record GradeRequest(@NotBlank String answer) { } public record GradeResponse( - boolean correct, - double score, - String correctAnswer) { + boolean correct, + double score, + String correctAnswer) { + } + + public record SyncProgressRequest( + Long lastQuizId, + Integer totalQuestions, + Integer success, + Integer failure, + Integer solveTime, + boolean isComplete) { } } diff --git a/src/main/java/com/blaybus/backend/exception/CommonErrorCode.java b/src/main/java/com/blaybus/backend/exception/CommonErrorCode.java index 5ac8915..e368d6b 100644 --- a/src/main/java/com/blaybus/backend/exception/CommonErrorCode.java +++ b/src/main/java/com/blaybus/backend/exception/CommonErrorCode.java @@ -24,6 +24,7 @@ public enum CommonErrorCode implements ErrorCode { SCENE_NOT_FOUND(HttpStatus.NOT_FOUND, "씬 정보를 찾을 수 없습니다."), QUIZ_NOT_FOUND(HttpStatus.NOT_FOUND, "퀴즈를 찾을 수 없습니다."), + QUIZ_PROGRESS_NOT_FOUND(HttpStatus.NOT_FOUND, "퀴즈 진행 상황을 찾을 수 없습니다."), EMBEDDING_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "임베딩 API 호출에 실패했습니다."), OPENAI_API_ERROR(HttpStatus.SERVICE_UNAVAILABLE, "AI 서비스 연결에 실패했습니다."), diff --git a/src/main/java/com/blaybus/backend/service/QuizGradingService.java b/src/main/java/com/blaybus/backend/service/QuizGradingService.java index 515c7ed..05158d9 100644 --- a/src/main/java/com/blaybus/backend/service/QuizGradingService.java +++ b/src/main/java/com/blaybus/backend/service/QuizGradingService.java @@ -32,18 +32,21 @@ public QuizDto.GradeResponse grade(Long quizId, String userAnswer, User user) { boolean correct; double score; + String correctAnswer; if (quiz.getType() == QuizType.SELECT) { - correct = quiz.getAnswer().split("|")[0].equalsIgnoreCase(userAnswer.trim()); + correctAnswer = quiz.getAnswer().split(",")[0]; + correct = correctAnswer.equalsIgnoreCase(userAnswer.trim()); score = correct ? 1.0 : 0.0; } else { - score = embeddingService.calculateSimilarity(userAnswer, quiz.getAnswer()); + correctAnswer = quiz.getAnswer(); + score = embeddingService.calculateSimilarity(userAnswer, correctAnswer); correct = score >= SIMILARITY_THRESHOLD; } updateProgress(user, quiz, correct); - return new QuizDto.GradeResponse(correct, score, quiz.getAnswer()); + return new QuizDto.GradeResponse(correct, score, correctAnswer); } private void updateProgress(User user, Quiz quiz, boolean correct) { diff --git a/src/main/java/com/blaybus/backend/service/QuizService.java b/src/main/java/com/blaybus/backend/service/QuizService.java index 9cc3b79..5fa2f50 100644 --- a/src/main/java/com/blaybus/backend/service/QuizService.java +++ b/src/main/java/com/blaybus/backend/service/QuizService.java @@ -10,6 +10,7 @@ import com.blaybus.backend.domain.quiz.QuizUserProgress; import com.blaybus.backend.domain.scene.SceneInformation; import com.blaybus.backend.domain.user.User; +import com.blaybus.backend.dto.QuizDto; import com.blaybus.backend.dto.QuizResponse; import com.blaybus.backend.exception.BusinessException; import com.blaybus.backend.exception.CommonErrorCode; @@ -52,6 +53,26 @@ public QuizResponse getSceneQuizzes(Long sceneId, User user) { return mapToResponse(sceneId, progress, quizzes); } + @Transactional + public void syncProgress(Long sceneId, QuizDto.SyncProgressRequest request, User user) { + QuizUserProgress progress = progressRepository.findByUserIdAndSceneId(user.getId(), sceneId) + .orElseThrow(() -> new BusinessException(CommonErrorCode.QUIZ_PROGRESS_NOT_FOUND)); + + QuizUserProgress updated = QuizUserProgress.builder() + .id(progress.getId()) + .user(progress.getUser()) + .scene(progress.getScene()) + .lastQuizId(request.lastQuizId()) + .totalQuestions(request.totalQuestions()) + .success(request.success()) + .failure(request.failure()) + .solveTime(request.solveTime()) + .isComplete(request.isComplete()) + .build(); + + progressRepository.save(updated); + } + private QuizResponse mapToResponse(Long sceneId, QuizUserProgress progress, List quizzes) { QuizResponse.UserProgressDto progressDto = new QuizResponse.UserProgressDto( progress.getId(), diff --git a/src/main/resources/data/initial_quiz_data.json b/src/main/resources/data/initial_quiz_data.json index 4da174b..a0c29f1 100644 --- a/src/main/resources/data/initial_quiz_data.json +++ b/src/main/resources/data/initial_quiz_data.json @@ -4,7 +4,7 @@ "target_purpose": "명칭 및 구조 파악", "type": "SELECT", "question": "로봇집게에서 물건이 닿는 안쪽 부분에 마찰력을 높이기 위해 덧대는 부품은 무엇인가요?", - "answer": "그립 패드|링크 암|기어 스크루|실린더" + "answer": "그립 패드,링크 암,기어 스크루,실린더" }, { "scene_info_title": "로봇집게", @@ -18,7 +18,7 @@ "target_purpose": "명칭 및 구조 파악", "type": "SELECT", "question": "서스펜션 구조에서 나선형으로 말려 있으며 충격을 받으면 길이가 변하는 부품은 무엇인가요?", - "answer": "코일 스프링|쇼크 업소버|컨트롤 암|너클" + "answer": "코일 스프링,쇼크 업소버,컨트롤 암,너클" }, { "scene_info_title": "서스펜션", @@ -32,7 +32,7 @@ "target_purpose": "명칭 및 구조 파악", "type": "SELECT", "question": "판스프링 끝부분에서 차체와 연결되어 스프링의 길이 변화를 수용하는 부품은 무엇인가요?", - "answer": "셔클(Shackle)|센터 볼트|유볼트|리프 클립" + "answer": "셔클(Shackle),센터 볼트,유볼트,리프 클립" }, { "scene_info_title": "판스프링", @@ -46,7 +46,7 @@ "target_purpose": "명칭 및 구조 파악", "type": "SELECT", "question": "드론의 몸체 중심에서 프로펠러가 달린 모터까지 뻗어 있는 뼈대 부분을 무엇이라 하나요?", - "answer": "암(Arm)|프레임|랜딩 기어|모터 마운트" + "answer": "암(Arm),프레임,랜딩 기어,모터 마운트" }, { "scene_info_title": "드론", @@ -60,7 +60,7 @@ "target_purpose": "명칭 및 구조 파악", "type": "SELECT", "question": "바이스에서 물건을 직접 압착하는 두 개의 벽면 중 움직이지 않는 부분의 명칭은?", - "answer": "고정 조(Fixed Jaw)|가동 조|핸들|슬라이드 베드" + "answer": "고정 조(Fixed Jaw),가동 조,핸들,슬라이드 베드" }, { "scene_info_title": "공작기계바이스", diff --git a/src/test/java/com/blaybus/backend/service/QuizServiceTest.java b/src/test/java/com/blaybus/backend/service/QuizServiceTest.java new file mode 100644 index 0000000..522d260 --- /dev/null +++ b/src/test/java/com/blaybus/backend/service/QuizServiceTest.java @@ -0,0 +1,85 @@ +package com.blaybus.backend.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.blaybus.backend.domain.quiz.QuizUserProgress; +import com.blaybus.backend.domain.scene.SceneInformation; +import com.blaybus.backend.domain.user.User; +import com.blaybus.backend.dto.QuizDto; +import com.blaybus.backend.exception.BusinessException; +import com.blaybus.backend.exception.CommonErrorCode; +import com.blaybus.backend.repository.QuizUserProgressRepository; + +@ExtendWith(MockitoExtension.class) +class QuizServiceTest { + + @Mock + private QuizUserProgressRepository progressRepository; + + @InjectMocks + private QuizService quizService; + + @Test + @DisplayName("퀴즈 진행 상황을 정상적으로 동기화한다.") + void syncProgressSuccess() { + // given + Long sceneId = 1L; + User user = mock(User.class); + given(user.getId()).willReturn(1L); + SceneInformation scene = SceneInformation.builder().id(sceneId).build(); + QuizUserProgress progress = QuizUserProgress.builder() + .id(1L) + .user(user) + .scene(scene) + .totalQuestions(5) + .success(2) + .failure(1) + .solveTime(100) + .isComplete(false) + .build(); + + QuizDto.SyncProgressRequest request = new QuizDto.SyncProgressRequest( + 3L, 5, 4, 1, 250, true); + + given(progressRepository.findByUserIdAndSceneId(user.getId(), sceneId)) + .willReturn(Optional.of(progress)); + + // when + quizService.syncProgress(sceneId, request, user); + + // then + then(progressRepository).should().save(argThat(updated -> updated.getLastQuizId().equals(3L) && + updated.getSuccess().equals(4) && + updated.getSolveTime().equals(250) && + updated.isComplete())); + } + + @Test + @DisplayName("진행 상황이 존재하지 않으면 예외를 던진다.") + void syncProgressNotFound() { + // given + Long sceneId = 1L; + User user = mock(User.class); + given(user.getId()).willReturn(1L); + QuizDto.SyncProgressRequest request = new QuizDto.SyncProgressRequest( + 3L, 5, 4, 1, 250, true); + + given(progressRepository.findByUserIdAndSceneId(user.getId(), sceneId)) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> quizService.syncProgress(sceneId, request, user)) + .isInstanceOf(BusinessException.class) + .hasFieldOrPropertyWithValue("errorCode", CommonErrorCode.QUIZ_PROGRESS_NOT_FOUND); + } +}