Feat : 각종 기능 추가#84
Conversation
Walkthrough퀴즈 기능 전반을 재구성. Admin 측에 퀴즈 생성 API 및 옵션 추가 플로우를 도입·변경하고, 서비스 레이어에서 퀴즈/옵션/데일리미션 연계를 구현. 도메인에 answerNumber 필드 추가 및 옵션의 isCorrect 제거. 사용자 퀴즈 제출 플로우를 QuizService 중심으로 변경하고 결과 조회 엔드포인트 제거. 일부 DTO 구조·필드 업데이트. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Admin as Admin
participant AdminAPI as AdminController
participant AdminSvc as AdminService
participant DMRepo as DailyMissionMastersRepo
participant QuizRepo as QuizRepository
participant QOptRepo as QuizOptionsRepository
Admin->>AdminAPI: POST /admin/quiz/create (CreateQuizRequestDTO)
AdminAPI->>AdminSvc: createQuiz(dto)
AdminSvc->>DMRepo: save(CreateMissionRequestDTO)
DMRepo-->>AdminSvc: DailyMissionMaster
AdminSvc->>QuizRepo: save(Quiz{quizType, quizQuestion, answerNumber, dailyMission})
QuizRepo-->>AdminSvc: Quiz
AdminSvc->>QOptRepo: saveAll(QuizOptions{optionText, optionOrder, quiz}...)
QOptRepo-->>AdminSvc: List<QuizOptions>
AdminSvc-->>AdminAPI: CreateQuizResponseDTO
AdminAPI-->>Admin: ApiResponse(CreateQuizResponseDTO)
sequenceDiagram
autonumber
participant User as User
participant UDMAPI as UserDailyMissionController
participant QuizSvc as QuizService
participant QRepo as QuizRepository
participant QOptRepo as QuizOptionsRepository
User->>UDMAPI: POST /quiz/{userDailyMissionId}/answer (QuizRequestDTO)
UDMAPI->>QuizSvc: summitQuizAnswer(request, userDailyMissionId)
QuizSvc->>QRepo: find Quiz by userDailyMissionId's mission
QRepo-->>QuizSvc: Quiz{answerNumber}
QuizSvc->>QOptRepo: findAllByQuiz(Quiz)
QOptRepo-->>QuizSvc: List<QuizOptions>
QuizSvc->>QuizSvc: 선택 옵션(ID) 검증 및 정답 비교(optionOrder vs answerNumber)
QuizSvc-->>UDMAPI: CompletedQuizResponseDTO{isCorrect, selectedAnswerNumber, options}
UDMAPI-->>User: ApiResponse(CompletedQuizResponseDTO)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–90 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 16
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java (1)
56-64: PathVariable 매핑 불일치: 400 오류 유발 가능매핑은
@PutMapping("/missions/daily/{dailyMissionMasterId}")인데, 파라미터는@PathVariable Long id입니다. 이름이 달라 매핑 실패 위험이 큽니다. 명시적으로 이름을 맞추세요.권장 diff:
- public ResponseEntity<ApiResponse<AdminResponseDTO.DailyMissionMastersResDTO>> updateDailyMission( - @RequestBody AdminRequestDTO.UpdateMissionRequestDTO requestDTO, @PathVariable Long id) { - DailyMissionMaster response = adminService.updateDailyMissionMasters(requestDTO, id); + public ResponseEntity<ApiResponse<AdminResponseDTO.DailyMissionMastersResDTO>> updateDailyMission( + @RequestBody AdminRequestDTO.UpdateMissionRequestDTO requestDTO, + @PathVariable(name = "dailyMissionMasterId") Long id) { + DailyMissionMaster response = + adminService.updateDailyMissionMasters(requestDTO, id);src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java (1)
32-39: Repo 필드 주입 추가 필요
QuizOptionsRepository가 필요합니다.적용 diff:
private final ReportRepository reportRepository; private final S3Uploader s3Uploader; private final QuizRepository quizRepository; + private final QuizOptionsRepository quizOptionsRepository;
🧹 Nitpick comments (7)
build.gradle (1)
44-45: springdoc 버전 주석 제거 또는 의도대로 BOM 도입 필요
// 버전을 삭제!라는 주석과 달리 여전히 고정 버전을 사용 중입니다. Springdoc는 Spring Boot BOM에서 관리되지 않으므로(일반적으로) 버전을 명시하는 편이 안전합니다. 버전을 지울 계획이라면 별도의 BOM을 명시적으로 추가해야 합니다. 당장은 혼동을 줄이기 위해 주석만 제거하는 것을 권장합니다.적용 diff:
- implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9' // 버전을 삭제! + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9'src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/Quiz.java (1)
25-27: answerNumber 타입과 제약 명시로 일관성/정합성 강화
answerNumber는 옵션의 순서를 가리키는 “번호”이므로 식별자(Long)보다 정수 범주의Integer가 더 자연스럽습니다. 또한 null/음수 저장을 차단하기 위해 DB 제약을 명확히 하는 편이 좋습니다.적용 diff:
- @Column(name = "answer_number") - private Long answerNumber; + @Column(name = "answer_number", nullable = false) + private Integer answerNumber;추가 권장(선택): 서비스/검증 레이어에서
- answerNumber >= 1 보장
- answerNumber ∈ {제출된 quizOptions.optionOrder 집합}
을 강제해 저장 시점에 정합성을 보장하세요.src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java (1)
38-44: DTO 내 optionOrder 타입 일관화 제안다른 DTO(예: CompletedQuizResponseDTO)는
Integer를 사용하고, 엔티티의answerNumber도 정수 의미입니다. 응답 DTO에서도 wrapper 타입으로 통일하면 null 표현/직렬화 시 유연성이 높습니다.적용 diff:
public static class QuizOptionsResponseDTO { private Long optionId; private String optionText; - private int optionOrder; + private Integer optionOrder; }src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java (1)
75-82: 대규모 주석 블록 제거 권장 (죽은 코드 유지 금지)결과 조회 엔드포인트 주석이 대량 남아 있습니다. 유지보수성과 정적 분석(Spotless) 모두에 불리하므로 완전히 제거하세요.
적용 diff(라인 전체 삭제):
-// // 새로운 엔드포인트 - 완료된 퀴즈 결과 조회 -// @GetMapping("/quiz/{userDailyMissionId}/result") -// @Operation(summary = "완료된 퀴즈 결과 조회 API (정답 정보 포함)") -// public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> getQuizResult( -// @PathVariable(name = "userDailyMissionId") Long userDailyMissionId) { -// CompletedQuizResponseDTO result = quizService.getCompletedQuizResult(userDailyMissionId); -// return ResponseEntity.ok(ApiResponse.success(result)); -// }src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (3)
133-144: DTO 생성은 적절. 다만 stream().toList()는 최소 Java 16+ 전제현재 프로젝트 타깃 JDK가 17이라면 문제 없고, 그 미만이라면
collect(Collectors.toList())를 사용해야 합니다. 빌드 JDK 버전을 확인해 주세요.
61-115: 죽은 코드 제거: 대규모 주석 메서드 삭제 권장이 블록은 유지할 이유가 없어 보이며 포맷팅/가독성에 악영향입니다. 완전 삭제를 권장합니다.
31-35: 미사용 의존성 제거 또는 실제 사용
private final UserDailyMissionService userDailyMissionService;가 현재 사용되지 않습니다. 제거하거나, 제출 시 미션 완료 처리/로그 적재 등 실제 로직에 활용하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (11)
build.gradle(1 hunks)src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java(2 hunks)src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java(2 hunks)src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java(2 hunks)src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java(7 hunks)src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/Quiz.java(1 hunks)src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/QuizOptions.java(0 hunks)src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/CompletedQuizResponseDTO.java(1 hunks)src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java(3 hunks)src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java(2 hunks)src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java(0 hunks)
💤 Files with no reviewable changes (2)
- src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/QuizOptions.java
- src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java
🧰 Additional context used
🧬 Code graph analysis (4)
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java (2)
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java (8)
Getter(16-31)Getter(33-43)Getter(45-51)Getter(53-58)Getter(60-67)Getter(69-78)Getter(81-89)Getter(91-99)src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/CompletedQuizResponseDTO.java (2)
Getter(8-27)Getter(18-26)
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java (2)
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java (6)
Getter(16-26)Getter(28-36)Getter(38-44)Getter(46-57)Getter(59-65)Getter(67-76)src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/CompletedQuizResponseDTO.java (2)
Getter(8-27)Getter(18-26)
src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java (1)
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java (1)
AdminResponseDTO(15-77)
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (1)
src/main/java/com/example/cp_main_be/global/exception/QuizNotFoundException.java (1)
QuizNotFoundException(3-7)
🪛 GitHub Actions: Spring Boot CI/CD with AWS
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java
[error] 1-1: spotlessJavaCheck failed: formatting violations detected. Step: './gradlew spotlessCheck'. Run './gradlew :spotlessApply' to fix.
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java
[error] 1-1: spotlessJavaCheck failed: formatting violations detected. Step: './gradlew spotlessCheck'. Run './gradlew :spotlessApply' to fix.
src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java
[error] 1-1: spotlessJavaCheck failed: formatting violations detected. Step: './gradlew spotlessCheck'. Run './gradlew :spotlessApply' to fix.
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java
[error] 1-1: spotlessJavaCheck failed: formatting violations detected. Step: './gradlew spotlessCheck'. Run './gradlew :spotlessApply' to fix.
src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java
[error] 1-1: spotlessJavaCheck failed: formatting violations detected. Step: './gradlew spotlessCheck'. Run './gradlew :spotlessApply' to fix.
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java
[error] 1-1: spotlessJavaCheck failed: formatting violations detected. Step: './gradlew spotlessCheck'. Run './gradlew :spotlessApply' to fix.
🔇 Additional comments (7)
src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/CompletedQuizResponseDTO.java (1)
16-26: 정답 노출 범위 확인 필요
CompletedQuizOptionResponseDTO에isAnswer가 포함되어 있어 정답 정보가 응답에 실립니다. 이 DTO가 “정답 제출 이후 결과 화면”에만 사용된다면 OK이지만, 문제 조회(풀이 전) 응답에 재사용되면 정답이 노출됩니다. 컨트롤러/서비스에서 용도별 DTO 분리 또는 필드 마스킹을 보장해 주세요.src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java (1)
28-36: Admin 응답에 answerNumber 포함 — 권한별 노출 검토Admin 생성 응답에
answerNumber포함은 타당합니다. 단, 동일 DTO를 사용자-facing API에서 재사용하지 않는지 확인해 주세요. 혼용 시 정답 유출 위험이 있습니다.src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java (1)
60-67: Spotless CI 실패: JAVA_HOME 설정 필요 및 포매팅 확인 요청로컬 환경에서
JAVA_HOME을 올바른 JDK 설치 디렉토리(예:/usr/lib/jvm/java-17-openjdk-amd64가 아닌 실제 설치 경로)로 설정한 뒤, 아래 명령을 실행해 주세요:./gradlew :spotlessApply ./gradlew :spotlessCheck포맷팅 및 미사용 import 이슈가 해결된 것을 확인하신 뒤 다시 커밋해 주시기 바랍니다.
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java (1)
1-106: Spotless 포맷팅 오류: 로컬에서 JAVA_HOME 재설정 후 적용 필요사내 CI에서
spotlessJavaCheck가 실패했으며, 로컬 환경에서 아래 사항을 확인해 주세요:
- 현재
JAVA_HOME이 잘못 설정되어 있어./gradlew :spotlessApply실행이 불가합니다.
→ 유효한 JDK 설치 경로로JAVA_HOME을 재설정하세요.- 설정 완료 후, 로컬에서 다음 명령을 실행하여 코드 포맷팅을 자동 적용 및 검증해 주세요.
./gradlew :spotlessApply && ./gradlew :spotlessCheck- 변경된 포맷팅 결과를 커밋한 뒤, CI가 통과하는지 확인 부탁드립니다.
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (1)
1-193: Spotless 포맷팅 검증 실패: 환경 설정 확인 필요현재 Spotless 자동 포맷터가 실행되지 않고 다음 오류가 발생했습니다:
ERROR: JAVA_HOME is set to an invalid directory: /usr/lib/jvm/java-17-openjdk-amd64아래 사항을 확인하신 후 다시 검증을 요청드립니다.
- 로컬 및 CI 환경의 JAVA_HOME이 올바른 JDK 설치 경로를 가리키는지 확인
java -version명령으로 설치된 Java 버전과 JAVA_HOME의 경로가 일치하는지 검증- 환경 변수를 수정한 이후
./gradlew :spotlessApply && ./gradlew :spotlessCheck를 재실행하여 Spotless 포맷팅이 성공하는지 확인위 작업 완료 후, 포맷팅이 적용된 상태로 다시 푸시해 주세요.
src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java (1)
1-189: 로컬 환경에javac바이너리가 누락되어 있어 Gradle이 제대로 동작하지 않고 있습니다.
CI 환경에는 JDK(컴파일러 포함)가 설치되어 있어야 Spotless 적용 및 검증이 정상 동작하니, 아래 사항을 확인 부탁드립니다:
- 시스템에 JDK(예:
openjdk-17-jdk)가 설치되어 있는지JAVA_HOME이 설치된 JDK 경로(예:/usr/lib/jvm/java-17-openjdk-amd64)를 가리키는지JAVA_HOME/bin/javac실행이 가능한지설치 및 설정 후 아래 명령을 다시 실행하시고 결과를 공유해 주세요:
./gradlew :spotlessApply && ./gradlew :spotlessChecksrc/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java (1)
1-169: Spotless 포맷터가 CI 환경에서 데몬 연결 문제로 실행되지 않아, 로컬에서 아래 명령어가 정상 통과하는지 확인 부탁드립니다.로컬 검증:
./gradlew spotlessApply && ./gradlew spotlessCheck정상 통과 여부를 확인하신 후 결과를 공유해 주세요.
| import com.example.cp_main_be.domain.mission.daily_mission_master.domain.DailyMissionMaster; | ||
| import com.example.cp_main_be.domain.mission.quiz.enums.QuizType; | ||
| import jakarta.persistence.*; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import java.time.LocalDateTime; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.Setter; | ||
| import java.util.List; | ||
|
|
||
| import lombok.*; | ||
|
|
There was a problem hiding this comment.
미사용 import 정리 (Spotless 에러 방지)
DailyMissionMaster, jakarta.persistence.*는 본 파일에서 사용되지 않습니다. Spotless가 미사용 import를 문제로 보고 있으니 제거하세요.
적용 diff:
-import com.example.cp_main_be.domain.mission.daily_mission_master.domain.DailyMissionMaster;
@@
-import jakarta.persistence.*;
@@
-import lombok.*;
+import lombok.*;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import com.example.cp_main_be.domain.mission.daily_mission_master.domain.DailyMissionMaster; | |
| import com.example.cp_main_be.domain.mission.quiz.enums.QuizType; | |
| import jakarta.persistence.*; | |
| import jakarta.validation.constraints.NotNull; | |
| import java.time.LocalDateTime; | |
| import lombok.Builder; | |
| import lombok.Getter; | |
| import lombok.Setter; | |
| import java.util.List; | |
| import lombok.*; | |
| import com.example.cp_main_be.domain.mission.quiz.enums.QuizType; | |
| import jakarta.validation.constraints.NotNull; | |
| import java.time.LocalDateTime; | |
| import java.util.List; | |
| import lombok.*; |
🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java
around lines 5 to 13, remove the unused imports DailyMissionMaster and the
jakarta.persistence.* wildcard; update the import list to only include the
actually used types (e.g., QuizType, NotNull, LocalDateTime, List, and Lombok
imports) so Spotless no longer flags unused imports.
| public static class CreateQuizOptionRequestDTO { | ||
|
|
||
| @NotNull private String optionText; | ||
|
|
||
| @NotNull private boolean isCorrect; | ||
|
|
||
| @NotNull private int optionOrder; | ||
|
|
||
| @NotNull private Long missionMasterId; | ||
| } |
There was a problem hiding this comment.
원시타입에 @NotNull 무효 — 옵션 순서 검증 보강 및 타입 수정
optionOrder가 int 원시타입이라 @NotNull이 효과가 없습니다. 누락 검증과 양수 제약을 위해 Integer로 변경하고 @NotNull, @Positive 적용을 권장합니다. 또한 optionText에는 공백 문자열 방지를 위해 @NotBlank를 권장합니다.
적용 diff:
public static class CreateQuizOptionRequestDTO {
- @NotNull private String optionText;
- @NotNull private int optionOrder;
+ @NotBlank private String optionText;
+ @NotNull @Positive private Integer optionOrder;
}추가(파일 상단 import 보강):
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java
around lines 63 to 67, the CreateQuizOptionRequestDTO uses a primitive int with
@NotNull (which is ineffective) and optionText uses @NotNull instead of
forbidding blank strings; change optionOrder from int to Integer and annotate it
with @NotNull and @Positive, change optionText to use @NotBlank (retain @NotNull
only if desired), and add the necessary imports
(jakarta.validation.constraints.NotBlank and
jakarta.validation.constraints.Positive, plus any existing Valid import if
needed).
| public static class CreateQuizRequestDTO { | ||
| private CreateMissionRequestDTO forCreateMission; //미션 마스터 생성을 위한 요청 | ||
| private String quizQuestion; | ||
| private QuizType quizType; | ||
| private Long answerNumber; | ||
| private List<CreateQuizOptionRequestDTO> quizOptions; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
퀴즈 생성 요청 필수값/정합성 검증 추가
핵심 필드에 제약이 없어 불완전 데이터가 저장될 수 있습니다. 아래와 같이 Bean Validation을 추가하고 중첩 객체/리스트에는 @Valid를 적용하세요.
적용 diff:
public static class CreateQuizRequestDTO {
- private CreateMissionRequestDTO forCreateMission; //미션 마스터 생성을 위한 요청
- private String quizQuestion;
- private QuizType quizType;
- private Long answerNumber;
- private List<CreateQuizOptionRequestDTO> quizOptions;
+ @NotNull @Valid
+ private CreateMissionRequestDTO forCreateMission; // 미션 마스터 생성을 위한 요청
+
+ @NotBlank
+ private String quizQuestion;
+
+ @NotNull
+ private QuizType quizType;
+
+ @NotNull @Positive
+ private Long answerNumber;
+
+ @NotNull @Valid @jakarta.validation.constraints.Size(min = 2)
+ private List<CreateQuizOptionRequestDTO> quizOptions;
}추가 가이드:
- answerNumber는 1부터 시작해
quizOptions.optionOrder중 하나와 일치해야 합니다. - 서비스 계층에서
optionOrder중복/범위(1..N) 검증도 함께 수행하세요.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public static class CreateQuizRequestDTO { | |
| private CreateMissionRequestDTO forCreateMission; //미션 마스터 생성을 위한 요청 | |
| private String quizQuestion; | |
| private QuizType quizType; | |
| private Long answerNumber; | |
| private List<CreateQuizOptionRequestDTO> quizOptions; | |
| } | |
| public static class CreateQuizRequestDTO { | |
| @NotNull @Valid | |
| private CreateMissionRequestDTO forCreateMission; // 미션 마스터 생성을 위한 요청 | |
| @NotBlank | |
| private String quizQuestion; | |
| @NotNull | |
| private QuizType quizType; | |
| @NotNull @Positive | |
| private Long answerNumber; | |
| @NotNull @Valid @jakarta.validation.constraints.Size(min = 2) | |
| private List<CreateQuizOptionRequestDTO> quizOptions; | |
| } |
| import com.example.cp_main_be.domain.mission.quiz.domain.QuizOptions; | ||
| import com.example.cp_main_be.domain.reports.domain.ReportReason; | ||
| import com.example.cp_main_be.domain.reports.enums.ReportStatus; | ||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
| import java.util.UUID; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import lombok.Builder; |
There was a problem hiding this comment.
미사용 import로 Spotless 실패 유발 가능성 — 제거 필요
QuizOptions, jakarta.persistence.Column import가 본 파일에서 사용되지 않습니다. Spotless 설정에 removeUnusedImports()가 있으므로 CI에서 실패 원인이 됩니다. 제거하시거나 ./gradlew :spotlessApply를 실행하세요.
적용 diff:
-import com.example.cp_main_be.domain.mission.quiz.domain.QuizOptions;
@@
-import jakarta.persistence.Column;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import com.example.cp_main_be.domain.mission.quiz.domain.QuizOptions; | |
| import com.example.cp_main_be.domain.reports.domain.ReportReason; | |
| import com.example.cp_main_be.domain.reports.enums.ReportStatus; | |
| import java.time.LocalDateTime; | |
| import java.util.List; | |
| import java.util.UUID; | |
| import jakarta.persistence.Column; | |
| import lombok.Builder; | |
| import com.example.cp_main_be.domain.reports.domain.ReportReason; | |
| import com.example.cp_main_be.domain.reports.enums.ReportStatus; | |
| import java.time.LocalDateTime; | |
| import java.util.List; | |
| import java.util.UUID; | |
| import lombok.Builder; |
🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java
around lines 4 to 12, remove the unused imports for
com.example.cp_main_be.domain.mission.quiz.domain.QuizOptions and
jakarta.persistence.Column that cause Spotless to fail; delete those two import
lines and either run ./gradlew :spotlessApply to auto-fix or rerun the build to
confirm the unused imports are gone.
| @PostMapping("/quiz/create") | ||
| @Operation(summary = "퀴즈 생성 API") | ||
| public ApiResponse<AdminResponseDTO.CreateQuizResponseDTO> createQuiz ( | ||
| @RequestBody AdminRequestDTO.CreateQuizRequestDTO requestDTO | ||
| ) | ||
| { | ||
| AdminResponseDTO.CreateQuizResponseDTO result = adminService.createQuiz(requestDTO); | ||
| return ApiResponse.ok(result); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
일관되지 않은 반환 타입 및 ApiResponse 사용: 컴파일/런타임 오류 가능
다른 엔드포인트는 ResponseEntity<ApiResponse<...>> + ApiResponse.success(...)를 사용하는데 여기만 ApiResponse.ok(...)를 직접 반환합니다. ok(...)가 없다면 컴파일 실패합니다. 스타일 일관성 측면에서도 통일을 권장합니다.
적용 diff:
- @PostMapping("/quiz/create")
- @Operation(summary = "퀴즈 생성 API")
- public ApiResponse<AdminResponseDTO.CreateQuizResponseDTO> createQuiz (
- @RequestBody AdminRequestDTO.CreateQuizRequestDTO requestDTO
- )
- {
- AdminResponseDTO.CreateQuizResponseDTO result = adminService.createQuiz(requestDTO);
- return ApiResponse.ok(result);
- }
+ @PostMapping("/quiz/create")
+ @Operation(summary = "퀴즈 생성 API")
+ public ResponseEntity<ApiResponse<AdminResponseDTO.CreateQuizResponseDTO>> createQuiz(
+ @RequestBody AdminRequestDTO.CreateQuizRequestDTO requestDTO) {
+ AdminResponseDTO.CreateQuizResponseDTO result = adminService.createQuiz(requestDTO);
+ return ResponseEntity.ok(ApiResponse.success(result));
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @PostMapping("/quiz/create") | |
| @Operation(summary = "퀴즈 생성 API") | |
| public ApiResponse<AdminResponseDTO.CreateQuizResponseDTO> createQuiz ( | |
| @RequestBody AdminRequestDTO.CreateQuizRequestDTO requestDTO | |
| ) | |
| { | |
| AdminResponseDTO.CreateQuizResponseDTO result = adminService.createQuiz(requestDTO); | |
| return ApiResponse.ok(result); | |
| } | |
| @PostMapping("/quiz/create") | |
| @Operation(summary = "퀴즈 생성 API") | |
| public ResponseEntity<ApiResponse<AdminResponseDTO.CreateQuizResponseDTO>> createQuiz( | |
| @RequestBody AdminRequestDTO.CreateQuizRequestDTO requestDTO) { | |
| AdminResponseDTO.CreateQuizResponseDTO result = adminService.createQuiz(requestDTO); | |
| return ResponseEntity.ok(ApiResponse.success(result)); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java
around lines 66 to 74, the createQuiz endpoint returns ApiResponse.ok(...)
directly which is inconsistent with other endpoints and may not exist; change
the method to return
ResponseEntity<ApiResponse<AdminResponseDTO.CreateQuizResponseDTO>> and wrap the
service result with ApiResponse.success(result) then return
ResponseEntity.ok(ApiResponse.success(result)); ensure the method signature and
return type match the other controllers and add any missing imports for
ResponseEntity and ApiResponse.
| UserDailyMission userDailyMission = userDailyMissionRepository.findById(userDailyMissionId).orElseThrow( | ||
| () -> new RuntimeException("미션을 찾을 수 없습니다.") | ||
| ); | ||
| DailyMissionMaster dailyMissionMaster = userDailyMission.getDailyMissionMaster(); | ||
| Quiz quiz = getQuizByMissionId(dailyMissionMaster.getId()); | ||
| List<QuizOptions> quizOptions = quizOptionsRepository.findAllByQuizId(quiz.getId()); | ||
|
|
||
| // 사용자가 선택한 답안 정보 | ||
| Long userSelectedOptionId = userDailyMission.getSelectedOptionId(); | ||
|
|
||
| // 정답 여부 계산 (엔티티에 저장되지 않으므로 다시 계산) | ||
| boolean isCorrect = | ||
| quizOptions.stream() | ||
| .anyMatch(option -> option.getId().equals(userSelectedOptionId) && option.isCorrect()); | ||
|
|
||
| // 사용자가 선택한 답안 번호 계산 (엔티티에 저장되지 않으므로 역계산) | ||
| Integer userSelectedAnswerNumber = null; | ||
| for (QuizOptions option : quizOptions) { | ||
| if (option.getId().equals(userSelectedOptionId)) { | ||
| userSelectedAnswerNumber = option.getOptionOrder(); | ||
| break; | ||
| } | ||
| } | ||
| //선택한 선지 정보 불러오기 | ||
| QuizOptions selectedQuizOption = quizOptionsRepository.findById(request.getSelectedOptionId()).orElseThrow( | ||
| ()-> new RuntimeException("해당하는 선지를 찾을 수 없습니다.") | ||
| ); | ||
| //퀴즈 정답 확인 |
There was a problem hiding this comment.
🛠️ Refactor suggestion
소유권 검증 누락: IDOR 방지 체크 추가 필요
userDailyMissionId로 조회한 엔티티가 인증 사용자 소유인지 확인이 없습니다. 컨트롤러 변경(사용자 ID 전달)과 함께 여기서 소유권을 검사하세요.
권장 diff(메서드 시그니처 확장 및 검증 추가):
- public CompletedQuizResponseDTO submitQuizAnswer(QuizRequestDTO request, Long userDailyMissionId) {
+ public CompletedQuizResponseDTO submitQuizAnswer(
+ QuizRequestDTO request, Long userDailyMissionId, Long userId) {
UserDailyMission userDailyMission = userDailyMissionRepository.findById(userDailyMissionId).orElseThrow(
() -> new RuntimeException("미션을 찾을 수 없습니다.")
);
+ if (!Objects.equals(userDailyMission.getUser().getId(), userId)) {
+ throw new org.springframework.security.access.AccessDeniedException("본인 소유 미션에만 답안을 제출할 수 있습니다.");
+ }추가 import 필요:
org.springframework.security.access.AccessDeniedException
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| UserDailyMission userDailyMission = userDailyMissionRepository.findById(userDailyMissionId).orElseThrow( | |
| () -> new RuntimeException("미션을 찾을 수 없습니다.") | |
| ); | |
| DailyMissionMaster dailyMissionMaster = userDailyMission.getDailyMissionMaster(); | |
| Quiz quiz = getQuizByMissionId(dailyMissionMaster.getId()); | |
| List<QuizOptions> quizOptions = quizOptionsRepository.findAllByQuizId(quiz.getId()); | |
| // 사용자가 선택한 답안 정보 | |
| Long userSelectedOptionId = userDailyMission.getSelectedOptionId(); | |
| // 정답 여부 계산 (엔티티에 저장되지 않으므로 다시 계산) | |
| boolean isCorrect = | |
| quizOptions.stream() | |
| .anyMatch(option -> option.getId().equals(userSelectedOptionId) && option.isCorrect()); | |
| // 사용자가 선택한 답안 번호 계산 (엔티티에 저장되지 않으므로 역계산) | |
| Integer userSelectedAnswerNumber = null; | |
| for (QuizOptions option : quizOptions) { | |
| if (option.getId().equals(userSelectedOptionId)) { | |
| userSelectedAnswerNumber = option.getOptionOrder(); | |
| break; | |
| } | |
| } | |
| //선택한 선지 정보 불러오기 | |
| QuizOptions selectedQuizOption = quizOptionsRepository.findById(request.getSelectedOptionId()).orElseThrow( | |
| ()-> new RuntimeException("해당하는 선지를 찾을 수 없습니다.") | |
| ); | |
| //퀴즈 정답 확인 | |
| // + 추가 import | |
| import org.springframework.security.access.AccessDeniedException; | |
| public class QuizService { | |
| // … | |
| public CompletedQuizResponseDTO submitQuizAnswer( | |
| QuizRequestDTO request, | |
| Long userDailyMissionId, | |
| Long userId | |
| ) { | |
| UserDailyMission userDailyMission = userDailyMissionRepository.findById(userDailyMissionId).orElseThrow( | |
| () -> new RuntimeException("미션을 찾을 수 없습니다.") | |
| ); | |
| // 소유권 검증 추가 | |
| if (!Objects.equals(userDailyMission.getUser().getId(), userId)) { | |
| throw new AccessDeniedException("본인 소유 미션에만 답안을 제출할 수 있습니다."); | |
| } | |
| DailyMissionMaster dailyMissionMaster = userDailyMission.getDailyMissionMaster(); | |
| Quiz quiz = getQuizByMissionId(dailyMissionMaster.getId()); | |
| List<QuizOptions> quizOptions = quizOptionsRepository.findAllByQuizId(quiz.getId()); | |
| // 선택한 선지 정보 불러오기 | |
| QuizOptions selectedQuizOption = quizOptionsRepository.findById(request.getSelectedOptionId()).orElseThrow( | |
| () -> new RuntimeException("해당하는 선지를 찾을 수 없습니다.") | |
| ); | |
| // 퀴즈 정답 확인 | |
| // … | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java
around lines 118 to 128, add an ownership check to prevent IDOR: extend the
method signature to accept the authenticated userId (or obtain it from
SecurityContext if preferred), after loading UserDailyMission compare
userDailyMission.getUser().getId() (or appropriate getter) with the supplied
authenticated userId and if they differ throw new AccessDeniedException("권한이
없습니다."); ensure you import
org.springframework.security.access.AccessDeniedException and adjust callers
(controller) to pass the authenticated userId.
| QuizOptions selectedQuizOption = quizOptionsRepository.findById(request.getSelectedOptionId()).orElseThrow( | ||
| ()-> new RuntimeException("해당하는 선지를 찾을 수 없습니다.") | ||
| ); |
There was a problem hiding this comment.
선지 소속 검증 누락: 다른 퀴즈의 선지를 참조 가능
selectedOptionId로 조회만 하고, 해당 선지가 현재 퀴즈 소속인지 확인하지 않습니다. 교차 퀴즈 참조를 차단해 주세요.
적용 diff:
QuizOptions selectedQuizOption = quizOptionsRepository.findById(request.getSelectedOptionId()).orElseThrow(
()-> new RuntimeException("해당하는 선지를 찾을 수 없습니다.")
);
+ if (!Objects.equals(selectedQuizOption.getQuiz().getId(), quiz.getId())) {
+ throw new IllegalArgumentException("선택한 선지가 해당 퀴즈에 속하지 않습니다.");
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| QuizOptions selectedQuizOption = quizOptionsRepository.findById(request.getSelectedOptionId()).orElseThrow( | |
| ()-> new RuntimeException("해당하는 선지를 찾을 수 없습니다.") | |
| ); | |
| QuizOptions selectedQuizOption = quizOptionsRepository.findById(request.getSelectedOptionId()).orElseThrow( | |
| () -> new RuntimeException("해당하는 선지를 찾을 수 없습니다.") | |
| ); | |
| if (!Objects.equals(selectedQuizOption.getQuiz().getId(), quiz.getId())) { | |
| throw new IllegalArgumentException("선택한 선지가 해당 퀴즈에 속하지 않습니다."); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java
around lines 125-127, the code fetches QuizOptions by selectedOptionId but does
not verify it belongs to the current quiz, allowing cross-quiz references; fix
by validating ownership: after loading the option, ensure
option.getQuiz().getId() (or option.getQuizId()) matches the quiz id from the
request (or the loaded Quiz entity) and throw a specific exception if it does
not; alternatively replace the lookup with a repository method like
findByIdAndQuizId(request.getSelectedOptionId(), quizId).orElseThrow(...) to
atomically enforce the relation.
| boolean isCorrect = quiz.getAnswerNumber().equals(request.getSelectedOptionId()); | ||
|
|
There was a problem hiding this comment.
정답 판정 로직 버그: answerNumber(선지 순번)과 optionId(Long)를 비교
quiz.getAnswerNumber()는 선지 순번(예: 1,2,3,4)로 보이는데, 현재 selectedOptionId(DB PK)와 비교하고 있습니다. 정답 판정이 항상 오동작할 수 있습니다. 선택한 선지의 optionOrder와 비교해야 합니다.
적용 diff:
- boolean isCorrect = quiz.getAnswerNumber().equals(request.getSelectedOptionId());
+ boolean isCorrect = Objects.equals(quiz.getAnswerNumber(), selectedQuizOption.getOptionOrder());📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| boolean isCorrect = quiz.getAnswerNumber().equals(request.getSelectedOptionId()); | |
| boolean isCorrect = Objects.equals(quiz.getAnswerNumber(), selectedQuizOption.getOptionOrder()); |
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java
around lines 129-130, the code incorrectly compares quiz.getAnswerNumber() (the
correct option order) to request.getSelectedOptionId() (the DB PK); instead,
load the selected Option entity by the provided selectedOptionId (via the
OptionRepository), get its optionOrder (or selectedOption.getOptionOrder()), and
compare that order to quiz.getAnswerNumber(); also handle missing option (throw
or return error) and nulls appropriately so the correctness check uses
optionOrder vs answerNumber rather than id vs answerNumber.
| public QuizResponseDTO getQuizByType(QuizType quizType) { | ||
| Quiz quiz = quizRepository.findAllByQuizType(quizType).get(0); | ||
| if (quiz == null) throw new QuizNotFoundException("퀴즈가 존재하지 않습니다."); | ||
| if (quiz == null) throw new QuizNotFoundException("해당 종류의 퀴즈가 존재하지 않습니다."); | ||
| List<QuizOptions> quizOptions = quizOptionsRepository.findAllByQuizId(quiz.getId()); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
빈 리스트 접근 버그: get(0) + null 체크는 무의미
빈 결과에서 get(0)이 먼저 NPE/IndexOutOfBounds를 유발합니다. stream().findFirst().orElseThrow(...)로 치환하세요.
적용 diff:
- Quiz quiz = quizRepository.findAllByQuizType(quizType).get(0);
- if (quiz == null) throw new QuizNotFoundException("해당 종류의 퀴즈가 존재하지 않습니다.");
+ Quiz quiz =
+ quizRepository.findAllByQuizType(quizType).stream()
+ .findFirst()
+ .orElseThrow(() -> new QuizNotFoundException("해당 종류의 퀴즈가 존재하지 않습니다."));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public QuizResponseDTO getQuizByType(QuizType quizType) { | |
| Quiz quiz = quizRepository.findAllByQuizType(quizType).get(0); | |
| if (quiz == null) throw new QuizNotFoundException("퀴즈가 존재하지 않습니다."); | |
| if (quiz == null) throw new QuizNotFoundException("해당 종류의 퀴즈가 존재하지 않습니다."); | |
| List<QuizOptions> quizOptions = quizOptionsRepository.findAllByQuizId(quiz.getId()); | |
| public QuizResponseDTO getQuizByType(QuizType quizType) { | |
| - Quiz quiz = quizRepository.findAllByQuizType(quizType).get(0); | |
| Quiz quiz = | |
| quizRepository.findAllByQuizType(quizType).stream() | |
| .findFirst() | |
| .orElseThrow(() -> new QuizNotFoundException("해당 종류의 퀴즈가 존재하지 않습니다.")); | |
| List<QuizOptions> quizOptions = quizOptionsRepository.findAllByQuizId(quiz.getId()); | |
| // …rest of method… |
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java
around lines 155-158, the code uses
quizRepository.findAllByQuizType(quizType).get(0) which will throw
IndexOutOfBoundsException on an empty list and makes the subsequent null check
meaningless; change this to safely obtain the first element using
stream().findFirst().orElseThrow(() -> new QuizNotFoundException("해당 종류의 퀴즈가
존재하지 않습니다.")) (or, if available, call a repository method like
findFirstByQuizType returning Optional), remove the redundant null check, then
continue to call quizOptionsRepository.findAllByQuizId(quiz.getId()).
| public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> summitAnswerToQuiz( | ||
| @PathVariable(name = "userDailyMissionId") Long userDailyMissionId, | ||
| @RequestBody QuizRequestDTO request) { | ||
| Boolean result = userDailyMissionService.summitAnswer(request, userDailyMissionId); | ||
| CompletedQuizResponseDTO result = quizService.summitQuizAnswer(request, userDailyMissionId); | ||
| return ResponseEntity.ok(ApiResponse.success(result)); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
오타: summit → submit으로 메서드/서비스명 정정 필요
엔드포인트/서비스 메서드명이 모두 summit(정상회담)으로 표기되어 있습니다. 의도는 submit(제출)로 보입니다. API 문서화/검색성/가독성 모두에 영향이 있으니 지금 PR에서 바로 정정하는 것을 권장합니다.
적용 diff:
- @Operation(summary = "퀴즈 답변 제출 API")
- public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> summitAnswerToQuiz(
+ @Operation(summary = "퀴즈 답변 제출 API")
+ public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> submitAnswerToQuiz(
@PathVariable(name = "userDailyMissionId") Long userDailyMissionId,
- @RequestBody QuizRequestDTO request) {
- CompletedQuizResponseDTO result = quizService.summitQuizAnswer(request, userDailyMissionId);
+ @RequestBody /* @Valid 권장 */ QuizRequestDTO request) {
+ CompletedQuizResponseDTO result = quizService.submitQuizAnswer(request, userDailyMissionId);
return ResponseEntity.ok(ApiResponse.success(result));
}추가로 DTO 유효성 검증을 위해 @Valid를 붙이는 것을 권장합니다. (Spring Boot 3+ 기준 jakarta.validation.Valid import 필요)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> summitAnswerToQuiz( | |
| @PathVariable(name = "userDailyMissionId") Long userDailyMissionId, | |
| @RequestBody QuizRequestDTO request) { | |
| Boolean result = userDailyMissionService.summitAnswer(request, userDailyMissionId); | |
| CompletedQuizResponseDTO result = quizService.summitQuizAnswer(request, userDailyMissionId); | |
| return ResponseEntity.ok(ApiResponse.success(result)); | |
| } | |
| @Operation(summary = "퀴즈 답변 제출 API") | |
| public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> submitAnswerToQuiz( | |
| @PathVariable(name = "userDailyMissionId") Long userDailyMissionId, | |
| @RequestBody @Valid QuizRequestDTO request) { | |
| CompletedQuizResponseDTO result = quizService.submitQuizAnswer(request, userDailyMissionId); | |
| return ResponseEntity.ok(ApiResponse.success(result)); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java
around lines 98-103, the method and service call use the incorrect name "summit"
instead of "submit" and the request body lacks validation; rename the controller
method and the called service method from summit* to submit* (e.g.,
summitAnswerToQuiz -> submitAnswerToQuiz and summitQuizAnswer ->
submitQuizAnswer), update all references/usages and tests accordingly, and add
jakarta.validation.Valid to the @RequestBody parameter (import
jakarta.validation.Valid) so the DTO is validated on input.
💡 Verification agent
🧩 Analysis chain
IDOR 위험: 본인 소유 미션인지 검증 누락
현재 인증 사용자 정보(@AuthenticationPrincipal) 없이 userDailyMissionId만으로 답안을 제출할 수 있어, 타 사용자 미션에 대한 제출이 가능할 수 있습니다. 컨트롤러에서 사용자 정보를 전달하고, 서비스에서 소유권을 검증하도록 보완해 주세요.
권장 diff(컨트롤러):
- public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> submitAnswerToQuiz(
- @PathVariable(name = "userDailyMissionId") Long userDailyMissionId,
- @RequestBody QuizRequestDTO request) {
- CompletedQuizResponseDTO result = quizService.submitQuizAnswer(request, userDailyMissionId);
+ public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> submitAnswerToQuiz(
+ @PathVariable(name = "userDailyMissionId") Long userDailyMissionId,
+ @AuthenticationPrincipal User user,
+ @RequestBody QuizRequestDTO request) {
+ CompletedQuizResponseDTO result =
+ quizService.submitQuizAnswer(request, userDailyMissionId, user.getId());
return ResponseEntity.ok(ApiResponse.success(result));
}서비스 쪽에서도 userId를 받아 소유권을 검증하는 로직이 필요합니다. 관련 변경은 QuizService 코멘트에 첨부합니다.
검증 스크립트:
🏁 Script executed:
#!/bin/bash
# summit 오타와 엔드포인트 사용처 점검
rg -nP "summit[A-Za-z]*" -C2
# 컨트롤러에서 @AuthenticationPrincipal 사용 여부 확인
rg -nP "@AuthenticationPrincipal\s+User\s+user" -C2 src/main/javaLength of output: 28138
보안 강화 필요: IDOR 취약점 방지용 소유권 검증 추가
현재 UserDailyMissionController#summitAnswerToQuiz 엔드포인트는 인증된 사용자 정보 없이 userDailyMissionId만으로 퀴즈 답안을 제출하도록 되어 있어, 다른 사용자의 미션에 대한 제출이 가능할 수 있습니다. 아래 항목을 반영해 주세요.
– 컨트롤러: summitAnswerToQuiz 메서드에 @AuthenticationPrincipal User user 파라미터를 추가
– 서비스: summitQuizAnswer(또는 submitQuizAnswer) 메서드에 userId 인자를 추가하고, 실제 미션 소유자인지 검증하는 로직 구현
– 메서드명: summit* → submit* 오타 수정 권장
수정 예시 (컨트롤러):
public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> summitAnswerToQuiz(
- @PathVariable Long userDailyMissionId,
- @RequestBody QuizRequestDTO request) {
- CompletedQuizResponseDTO result = quizService.summitQuizAnswer(request, userDailyMissionId);
+ @PathVariable Long userDailyMissionId,
+ @AuthenticationPrincipal User user,
+ @RequestBody QuizRequestDTO request) {
+ CompletedQuizResponseDTO result =
+ quizService.submitQuizAnswer(request, userDailyMissionId, user.getId());
return ResponseEntity.ok(ApiResponse.success(result));
}수정 예시 (서비스):
- public CompletedQuizResponseDTO summitQuizAnswer(QuizRequestDTO request, Long userDailyMissionId) {
+ public CompletedQuizResponseDTO submitQuizAnswer(QuizRequestDTO request,
+ Long userDailyMissionId,
+ Long userId) {
UserDailyMission mission = userDailyMissionRepository.findById(userDailyMissionId)
.orElseThrow(() -> new RuntimeException("미션을 찾을 수 없습니다."));
+ if (!mission.getUser().getId().equals(userId)) {
+ throw new AccessDeniedException("권한이 없습니다.");
+ }
// 퀴즈 답안 처리 로직...
}위 변경을 통해 인증된 사용자만 자신의 일일 미션에 답안을 제출할 수 있도록 보장해야 합니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> summitAnswerToQuiz( | |
| @PathVariable(name = "userDailyMissionId") Long userDailyMissionId, | |
| @RequestBody QuizRequestDTO request) { | |
| Boolean result = userDailyMissionService.summitAnswer(request, userDailyMissionId); | |
| CompletedQuizResponseDTO result = quizService.summitQuizAnswer(request, userDailyMissionId); | |
| return ResponseEntity.ok(ApiResponse.success(result)); | |
| } | |
| public ResponseEntity<ApiResponse<CompletedQuizResponseDTO>> summitAnswerToQuiz( | |
| @PathVariable(name = "userDailyMissionId") Long userDailyMissionId, | |
| @AuthenticationPrincipal User user, | |
| @RequestBody QuizRequestDTO request) { | |
| CompletedQuizResponseDTO result = | |
| quizService.submitQuizAnswer(request, userDailyMissionId, user.getId()); | |
| return ResponseEntity.ok(ApiResponse.success(result)); | |
| } |
한 일
Summary by CodeRabbit