Skip to content

Feat : 각종 기능 추가#84

Merged
c5ln merged 1 commit into
developfrom
refactor/mission-refactoring
Aug 25, 2025
Merged

Feat : 각종 기능 추가#84
c5ln merged 1 commit into
developfrom
refactor/mission-refactoring

Conversation

@c5ln

@c5ln c5ln commented Aug 25, 2025

Copy link
Copy Markdown
Member

한 일

  • 퀴즈 생성 API 추가 그리고, 이에 필요한 DTO 추가
  • 퀴즈 정답 확인과 해설 API 합치기

Summary by CodeRabbit

  • 신기능
    • 관리자용 퀴즈 생성 API 추가. 생성 결과에 퀴즈 정보, 정답 번호, 옵션 목록, 연관 미션 정보가 포함됩니다.
  • 리팩터링
    • 퀴즈 옵션 생성 엔드포인트 경로/요청 변경: quizId 경로 변수 추가 및 요청 본문 형태 갱신.
    • 퀴즈 제출 API가 Boolean 대신 상세 채점 결과를 반환하며, 별도의 “결과 조회” API는 제거되었습니다.
    • 퀴즈 데이터 구조 정리: 옵션의 정답 여부 필드 제거, 퀴즈에 정답 번호 도입, 선택한 옵션 ID 노출 제거(선택 번호로 대체).
  • 스타일
    • 빌드 스크립트 공백 정리.

@c5ln c5ln self-assigned this Aug 25, 2025
@c5ln c5ln linked an issue Aug 25, 2025 that may be closed by this pull request
4 tasks
@coderabbitai

coderabbitai Bot commented Aug 25, 2025

Copy link
Copy Markdown

Walkthrough

퀴즈 기능 전반을 재구성. Admin 측에 퀴즈 생성 API 및 옵션 추가 플로우를 도입·변경하고, 서비스 레이어에서 퀴즈/옵션/데일리미션 연계를 구현. 도메인에 answerNumber 필드 추가 및 옵션의 isCorrect 제거. 사용자 퀴즈 제출 플로우를 QuizService 중심으로 변경하고 결과 조회 엔드포인트 제거. 일부 DTO 구조·필드 업데이트.

Changes

Cohort / File(s) Summary
Build formatting
build.gradle
dependencies 블록 내 공백행 추가(포맷팅만).
Admin 요청 DTO 재구성
.../domain/admin/dto/AdminRequestDTO.java
퀴즈 생성용 CreateQuizRequestDTO 추가(미션 생성 DTO 포함, quizType/answerNumber/옵션 리스트). 옵션 DTO CreateQuizOptionRequestDTO 신설(optionText/optionOrder), isCorrect/missionMasterId 제거.
Admin 응답 DTO 확장
.../domain/admin/dto/AdminResponseDTO.java
CreateQuizResponseDTO, QuizOptionsResponseDTO 추가(퀴즈/옵션/데일리미션 응답 구조).
Admin 컨트롤러 API 변경
.../domain/admin/presentation/AdminController.java
POST /quiz/create 추가(퀴즈 생성). 기존 옵션 생성 엔드포인트를 POST "/{quizId}/options"로 변경, DTO/파라미터 갱신.
Admin 서비스 로직 추가/변경
.../domain/admin/service/AdminService.java
createQuiz(...) 추가: 데일리미션 생성→퀴즈 저장→옵션 일괄 생성→응답 DTO 구성. createQuizOption(...) 시그니처 변경(quizId 필요) 및 검증 추가. 여러 메서드에 @Transactional 부여. addDeliveryPlant 추가.
퀴즈 도메인 변경
.../domain/mission/quiz/domain/Quiz.java
answerNumber(Long) 필드 및 컬럼(answer_number) 추가.
퀴즈 옵션 도메인 변경
.../domain/mission/quiz/domain/QuizOptions.java
isCorrect 필드/컬럼 제거.
퀴즈 응답 DTO 조정
.../domain/mission/quiz/dto/CompletedQuizResponseDTO.java
selectedOptionId 제거; selectedAnswerNumber 유지.
퀴즈 서비스 리팩토링
.../domain/mission/quiz/service/QuizService.java
getCompletedQuizResult(...) 제거. getQuiz에서 옵션의 isAnswer 노출 제거. summitQuizAnswer(...)를 선택 옵션 ID 기반으로 재작성하고 정답/선택 상태 계산 후 CompletedQuizResponseDTO 반환. 에러 메시지 수정, 의존성 주입 추가.
유저 데일리미션 컨트롤러
.../domain/mission/user_daily_mission/presentation/UserDailyMissionController.java
GET 결과 조회 엔드포인트 제거(주석 처리). 답안 제출 엔드포인트를 summitAnswerToQuiz로 변경하고 반환 타입을 CompletedQuizResponseDTO로 변경.
유저 데일리미션 서비스
.../domain/mission/user_daily_mission/service/UserDailyMissionService.java
summitAnswer(...) 제거. 나머지 미션 관련 메서드 유지.

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)
Loading
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)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related issues

Possibly related PRs

  • Fix/38 #39 — 동일 영역(퀴즈/옵션, Admin/Service/DTO) 변경이 겹치는 PR.
  • Refactor : 응답 DTO 개선 #44 — QuizRequestDTO의 선택 필드 변경과 본 PR의 제출 로직 변경이 직접적으로 연관.
  • Feat/27 diary garden api #49 — AdminService/AdminController에서 퀴즈 옵션 생성 시그니처 변경과 검증 로직이 중첩.

Suggested labels

enhancement

Suggested reviewers

  • xoruddl

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/mission-refactoring

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@c5ln c5ln merged commit c2a5394 into develop Aug 25, 2025
8 of 11 checks passed
@c5ln c5ln deleted the refactor/mission-refactoring branch August 25, 2025 08:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 36b2741 and 22cf8b1.

📒 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: 정답 노출 범위 확인 필요

CompletedQuizOptionResponseDTOisAnswer가 포함되어 있어 정답 정보가 응답에 실립니다. 이 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 :spotlessCheck
src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java (1)

1-169: Spotless 포맷터가 CI 환경에서 데몬 연결 문제로 실행되지 않아, 로컬에서 아래 명령어가 정상 통과하는지 확인 부탁드립니다.

로컬 검증:

./gradlew spotlessApply && ./gradlew spotlessCheck

정상 통과 여부를 확인하신 후 결과를 공유해 주세요.

Comment on lines +5 to 13
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.*;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

미사용 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.

Suggested change
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.

Comment on lines +63 to 67
public static class CreateQuizOptionRequestDTO {

@NotNull private String optionText;

@NotNull private boolean isCorrect;

@NotNull private int optionOrder;

@NotNull private Long missionMasterId;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

원시타입에 @NotNull 무효 — 옵션 순서 검증 보강 및 타입 수정

optionOrderint 원시타입이라 @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).

Comment on lines +72 to +78
public static class CreateQuizRequestDTO {
private CreateMissionRequestDTO forCreateMission; //미션 마스터 생성을 위한 요청
private String quizQuestion;
private QuizType quizType;
private Long answerNumber;
private List<CreateQuizOptionRequestDTO> quizOptions;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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;
}

Comment on lines +4 to 12
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

미사용 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.

Suggested change
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.

Comment on lines +66 to +74
@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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

일관되지 않은 반환 타입 및 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.

Suggested change
@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.

Comment on lines +118 to +128
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("해당하는 선지를 찾을 수 없습니다.")
);
//퀴즈 정답 확인

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

소유권 검증 누락: 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.

Suggested change
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.

Comment on lines +125 to +127
QuizOptions selectedQuizOption = quizOptionsRepository.findById(request.getSelectedOptionId()).orElseThrow(
()-> new RuntimeException("해당하는 선지를 찾을 수 없습니다.")
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

선지 소속 검증 누락: 다른 퀴즈의 선지를 참조 가능

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.

Suggested change
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.

Comment on lines +129 to 130
boolean isCorrect = quiz.getAnswerNumber().equals(request.getSelectedOptionId());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

정답 판정 로직 버그: 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.

Suggested change
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.

Comment on lines 155 to 158
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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()).

Comment on lines +98 to 103
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));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

오타: 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.

Suggested change
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/java

Length 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.

Suggested change
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));
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 미션 API 리팩토링

1 participant