Skip to content

Conversation

@lejuho
Copy link
Collaborator

@lejuho lejuho commented Aug 20, 2025

close #38

Summary by CodeRabbit

  • New Features

    • 좋아요 추가/취소 기능 및 피드 좋아요 알림, 완료된 퀴즈 결과 조회 API(정답·선택 포함), 토큰 재발급 시 새 리프레시 토큰 동시 발급
  • Refactor

    • 가든 조회 경로를 /api/v1/gardens/{gardenId}로 통일, 다이어리 공개 API 정리, 도메인·패키지 재구성(회원/미션/콘텐츠/알림) 및 콘텐츠·이미지 처리 위치 변경
  • Bug Fixes

    • 좋아요 중복 방지(데이터 정합성 및 유니크 제약), 잘못된 요청에 대해 400 응답 제공(IllegalArgumentException 처리)

@lejuho lejuho requested review from c5ln and xoruddl August 20, 2025 09:36
@coderabbitai
Copy link

coderabbitai bot commented Aug 20, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

대규모 패키지 재구성(회원/미션/콘텐츠/알림 도메인), 좋아요(Like) 도메인 및 마이그레이션 추가, 퀴즈/유저데일리미션 흐름 확장, S3 → Cloudflare R2 전환, 일부 다이어리 API/DTO/리포지토리 삭제 및 테스트 경로/내용 대규모 갱신.

Changes

Cohort / File(s) Summary
Garden 모듈
src/main/java/.../domain/garden/garden/presentation/GardenController.java, .../garden/service/*, .../garden/domain/*
GardenController에 클래스 레벨 매핑(/api/v1/gardens) 추가, GET /{gardenId}만 노출. 패키지 이동 및 User 타입 import 갱신, unlock 내부 userLevel 타입 int→long 변경.
Like 도메인 및 마이그레이션
src/main/java/com/example/.../domain/social/like/..., src/main/resources/db/migration/V2__add_like_unique_constraint.sql
JPA 엔티티 Like, LikeRepository, LikeService 추가; 피드 좋아요에 알림 전송 로직 포함. 중복 제거 SQL 및 유니크 제약 마이그레이션 추가.
미션(일일/퀴즈) 리팩터링 및 추가
src/main/java/.../domain/mission/...
DailyMissionMasters → DailyMissionMaster로 리네임 및 리포지토리 재구성. Keyword, Quiz, QuizOptions, QuizRepository 및 UserDailyMission 엔티티/Repo/Service/컨트롤러 추가. 퀴즈 조회 및 완료 결과 API/DTO(QuizResponseDTO/CompletedQuizResponseDTO/DailyMissionResponseDTO 등) 추가.
멤버/인증/알림 리팩터링
src/main/java/com/example/.../domain/member/...
User, UserRepository, UserService, Auth, Notification 등 패키지 이동(도메인.member.*). User level/experience 타입 Integer→Long 변경, NotificationType 일부 상수 제거. 컨트롤러/서비스/리포지토리 import 일괄 갱신.
콘텐츠(아바타/이미지) 모듈 이동 & S3→R2
src/main/java/.../domain/content/..., src/main/java/.../global/infra/S3Uploader.java, src/main/java/.../global/config/AwsConfig.java
Avatar/DailyMissionImage/ImageUploader 등 패키지 이동. S3Uploader → global.infra로 이동, ImageUploader 타입 경로 변경. AwsConfig R2 엔드포인트/크레덴셜 사용, amazonS3 빈에 @primary, endpoint/path-style 설정. application.properties와 CI 환경변수(R2) 갱신.
Diary 도메인 축소
src/main/java/.../domain/social/diary/...
DiaryController, DiaryImageController, DiaryService 및 관련 DTO/Repo 다수 삭제(일부 엔티티 유지). 다이어리 관련 퍼블릭 API 제거.
키워드 모듈 이동 및 DTO 레코드화
src/main/java/.../domain/mission/keyword/...
KeywordResponse를 record로 재정의, TodayKeywordResponse 추가/이동. KeywordRepository 재구성 및 서비스/컨트롤러 경로 갱신. 관련 테스트 파일 이동/삭제.
테스트 정렬/보강
src/test/java/...
테스트 패키지/임포트 전반 갱신, User.level Long 반영, LikeServiceTest 추가, 다수 테스트(미션/퀴즈/어드민/가든 등) 리팩터링.
기타 경로/임포트 정리
.../domain/social/*, .../domain/garden/plant_masters/*, .../domain/content/avatar/*
AvatarPost/Bookmark/Comment/Feed/Follow/Guestbook 등에서 User/Notification 경로 갱신, 일부 엔티티에 @Builder.Default 추가, 불필요한 DTO(예: PostLikeResponse) 삭제 또는 재생성.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant LC as LikeController
  participant LS as LikeService
  participant UR as UserRepository
  participant LR as LikeRepository
  participant FR as FeedRepository
  participant NS as NotificationService

  C->>LC: POST /likes {userId, targetId, targetType}
  LC->>LS: addLike(userId, targetId, targetType)
  LS->>UR: findById(userId)
  UR-->>LS: User or not found
  alt user not found
    LS-->>LC: throw UserNotFoundException
  else
    LS->>LR: existsByUserAndTargetIdAndTargetType(...)
    alt already exists
      LS-->>LC: throw RuntimeException("이미 좋아요를 눌렀습니다.")
    else
      LS->>LR: save(Like)
      opt targetType == "feed"
        LS->>FR: findById(targetId)
        FR-->>LS: Feed
        alt receiver != user
          LS->>NS: sendNotification(FEED_LIKE, receiver, "/feeds/"+targetId)
        end
      end
      LS-->>LC: success
    end
  end
  LC-->>C: 200 OK
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant UMC as UserDailyMissionController
  participant UMS as UserDailyMissionService
  participant UDMR as UserDailyMissionRepository
  participant QR as QuizRepository
  participant QOR as QuizOptionsRepository

  C->>UMC: GET /missions/quiz/{userDailyMissionId}
  UMC->>UMS: getQuiz(userDailyMissionId)
  UMS->>UDMR: findById(userDailyMissionId)
  UDMR-->>UMS: UserDailyMission
  UMS->>QR: findByDailyMissionMaster_Id(missionId)
  QR-->>UMS: Quiz
  UMS->>QOR: findAllByQuizId(quizId)
  QOR-->>UMS: List<QuizOptions>
  UMS-->>UMC: QuizResponseDTO
  UMC-->>C: 200 OK
Loading

Estimated code review effort

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

Assessment against linked issues

Objective Addressed Explanation
충돌로 인한 코드 유실 복구 (브랜치 정리, 누락 로직 원복) [#38] 대규모 리팩터링/삭제·추가가 섞여 있어 "원복" 범위(어떤 로직을 복구해야 하는지)가 PR만으로 명확하지 않습니다.
관련 테스트 보강 및 정상 동작 검증 [#38] 여러 테스트가 패키지에 맞게 갱신되었고 LikeService 단위 테스트가 추가되었습니다.
빌드/배포 파이프라인 정상화 [#38] CI 환경변수 및 Gradle 명령이 변경되었으나 복구 목적(충돌 해결)과 직접 연관되는지 불명확합니다.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
src/main/java/com/example/cp_main_be/domain/social/diary/* 삭제 (DiaryController, DiaryService 등) #38 은 "충돌 복구" 목적이므로 다이어리 API 전면 삭제가 복구 범위인지 불명확 — 기능 제거는 복구 목적과 일치하지 않을 가능성이 높음.
src/main/java/com/example/cp_main_be/global/config/AwsConfig.java — R2 전환 및 @primary 추가 linked issue #38 에 빌드/복구 관련 내용은 있으나 S3→R2 인프라 전환은 충돌 복구 목표와 직접 연관되지 않을 수 있음.
다수의 패키지 재배치(예: user→member.user, avatar→content.avatar 등) 패키지 재구성 자체는 정리 작업으로 보이며, 충돌 "복구" 목적 외의 아키텍처 변경일 가능성이 있어 범위가 다를 수 있습니다.

Possibly related PRs

  • Fix/38 #39 — 거의 동일한 GardenController 변경(엔드포인트 축소, 클래스 레벨 매핑) 적용으로 코드 레벨 충돌 가능성 있음.
  • Feat/mission and admin api #35 — 이전에 GardenController에 수동 엔드포인트(물주기/햇빛/언락)를 추가한 PR로, 본 PR은 해당 변경을 제거/대체하므로 직접적인 연관/충돌 소지가 높음.

Suggested reviewers

  • xoruddl
  • c5ln

Poem

깡총깡총 커밋 쌓인 숲 속에서
패키지 옮기며 길을 정리했네 🌱
좋아요 한 번에 알림도 톡!
퀴즈는 풀고 미션은 척척 ✔️
R2 바람 불어 이미지 훨훨 — 토끼가 박수 쳐요 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/38

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

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/garden/presentation/GardenController.java (1)

17-18: 서비스 메서드 참조 및 경로 변경 확인

  • src/test/java/com/example/cp_main_be/domain/garden/service/GardenServiceTest.java
    – 제거·변경된 메서드명(waterGarden, sunlightGarden, unlockGarden, getBackground)을 여전히 호출하고 있어 테스트가 실패합니다.
    GardenService에 맞춰 새 메서드명으로 수정해야 합니다.
  • 외부 클라이언트(게이트웨이·프론트엔드) 및 통합 테스트에서 최종 엔드포인트 /api/v1/gardens 사용 여부를 검토·업데이트하세요.
  • @RequestMapping의 선행 슬래시는 이미 모든 컨트롤러에 일관 적용되어 있어 추가 작업이 필요 없습니다.
src/main/java/com/example/cp_main_be/domain/infra/s3/S3Uploader.java (1)

51-61: URL에서 Key 추출 로직, R2 엔드포인트 다양성에 취약 — URI 기반으로 보강 제안.

현재 구현은 amazonS3.getUrl(bucket, "")를 전제로 prefix 제거를 수행합니다. 엔드포인트/주소 스타일 변경(virtual-hosted-style vs path-style) 또는 CDN/커스텀 도메인 사용 시 실패할 수 있습니다.

다음처럼 URI 파싱 기반으로 보강해 주세요:

-  private String extractKeyFromUrl(String imageUrl) {
-    // URL에서 버킷 이름과 경로를 제외한 부분 추출
-    String bucketUrl = amazonS3.getUrl(bucket, "").toString();
-    // URL 인코딩된 문자열을 처리하기 위해 replaceAll 사용
-    String key = imageUrl.replaceFirst(bucketUrl, "");
-    // 경로의 첫 번째 슬래시 제거
-    if (key.startsWith("/")) {
-      key = key.substring(1);
-    }
-    return key;
-  }
+  private String extractKeyFromUrl(String imageUrl) {
+    try {
+      java.net.URI uri = java.net.URI.create(imageUrl);
+      String path = uri.getPath() == null ? "" : uri.getPath();
+      if (path.startsWith("/")) path = path.substring(1);
+      // path-style: "{bucket}/{key}" -> 버킷 prefix 제거
+      if (path.startsWith(bucket + "/")) {
+        return path.substring(bucket.length() + 1);
+      }
+      // virtual-host/custom-domain: path 자체가 key
+      return path;
+    } catch (Exception e) {
+      throw new IllegalArgumentException("유효하지 않은 이미지 URL입니다.");
+    }
+  }

추가로, AwsConfig에서 path-style을 강제(withPathStyleAccessEnabled(true))하면 본 로직의 안정성이 더 높아집니다.

🧹 Nitpick comments (15)
src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java (2)

41-49: repository.save 스텁이 null을 반환합니다 — 서비스 구현이 반환값을 사용할 경우 NPE 위험

Spring Data JPA의 save는 일반적으로 엔티티를 반환합니다. 현재 doAnswer에서 null을 반환하고 있어, 서비스 구현이 save 반환값을 사용하는 경우 NPE가 발생할 수 있습니다. 저장된 엔티티(인자로 받은 객체)를 반환하도록 수정하는 편이 안전합니다. 주석도 실제 의미에 맞게 정리하면 좋습니다.

아래처럼 변경을 제안합니다:

-    // Simulate the @PrePersist behavior
+    // Simulate JPA @PrePersist behavior on save
     doAnswer(
             invocation -> {
               User argUser = invocation.getArgument(0);
               argUser.setUuid(UUID.randomUUID()); // Set UUID on the passed object
-              return null; // saveUser is void, so return null
+              return argUser; // repository.save(...) normally returns the persisted entity
             })
         .when(userRepository)
         .save(any(User.class));

3-8: 일관성: never 정적 임포트 사용으로 표기 단순화 제안

다른 Mockito API를 정적 임포트로 사용하고 있으므로, never()도 동일하게 정적 임포트로 일관성을 맞추면 가독성이 좋아집니다.

아래 변경을 제안합니다:

 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.never;
-    verify(userRepository, org.mockito.Mockito.never()).delete(any(User.class));
+    verify(userRepository, never()).delete(any(User.class));
-    verify(userRepository, org.mockito.Mockito.never()).save(any(User.class));
+    verify(userRepository, never()).save(any(User.class));

Also applies to: 141-144, 209-212

src/main/java/com/example/cp_main_be/domain/social/diaryimage/domain/DiaryImageRepository.java (1)

10-11: 의도가 PK 조회가 아니라면 메서드명을 명확화(예: findByImageId)하세요

현재 findById(Long imageId)는 이름상 PK 조회처럼 보이지만, 파라미터명이 imageId라 비PK 필드 조회 의도가 섞여 보입니다.

  • PK 조회가 목적이면 커스텀 메서드를 제거하고 기본 findById(ID) 사용을 권장합니다.
  • 만약 DiaryImage 내부에 imageId라는 별도 컬럼으로 조회하려는 의도라면, 파생쿼리 규칙에 맞게 메서드를 findByImageId(...)로 명확화하세요.

예시(비PK 컬럼 조회가 의도인 경우):

-  Optional<com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage> findById(
-      Long imageId);
+  Optional<DiaryImage> findByImageId(Long imageId);

적용 전, 엔티티에 private Long imageId;와 해당 접근자(게터)가 실제로 존재하는지 반드시 확인이 필요합니다.

src/main/java/com/example/cp_main_be/domain/garden/presentation/GardenController.java (1)

24-28: 권한 체크/ID 유효성/에러 문서화 보완을 권장

  • 이 조회 API가 퍼블릭 공개가 아닌 경우, 인증/인가 체크(예: @PreAuthorize) 또는 Service 레벨 소유권 검증이 필요합니다.
  • PathVariable ID는 양수 제약을 추가하는 편이 안전합니다.
  • 404 Not Found 등 에러 응답 스펙을 Swagger에 명시하면 API 소비자 경험이 좋아집니다. 다만 현재 com.example...ApiResponse 클래스와 Swagger의 ApiResponse 어노테이션이 이름 충돌하므로 주의가 필요합니다.

다음 최소 변경으로 시작해 보세요.

  • 메서드 시그니처에 양수 검증 추가(해당 범위 내 diff):
-  public ResponseEntity<ApiResponse<GardenResponse>> getGarden(@PathVariable Long gardenId) {
+  public ResponseEntity<ApiResponse<GardenResponse>> getGarden(@PathVariable @jakarta.validation.constraints.Positive Long gardenId) {
  • 클래스 레벨에 메서드 검증 활성화와 필요한 import 추가(범위 밖이므로 예시 코드로 제시):

Java 코드(클래스 헤더 인근):

import org.springframework.validation.annotation.Validated;
// ...
@RestController
@RequiredArgsConstructor
@Validated
@RequestMapping("/api/v1/gardens")
public class GardenController {
  • Swagger 에러 응답 문서화(이름 충돌을 피하기 위해 FQN 사용 예시):
@Operation(summary = "텃밭 슬롯 조회", description = "텃밭 슬롯을 조회합니다")
// 필요 시 아래와 같이 명시
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "존재하지 않는 텃밭 ID")

인가가 필요하다면 예시:

import org.springframework.security.access.prepost.PreAuthorize;

@PreAuthorize("hasRole('USER')")
public ResponseEntity<ApiResponse<GardenResponse>> getGarden(...) { ... }

확인 요청:

  • 이 엔드포인트는 인증 없이 공개가 맞나요?
  • Service(findGardenById)가 미존재 ID에 대해 404로 매핑되는 예외를 던지도록 구성되어 있나요(예: ResponseStatusException/도메인 NotFound 예외 + @ControllerAdvice)? 필요 시 관련 핸들러 존재 여부도 점검이 필요합니다.
src/main/java/com/example/cp_main_be/global/config/AwsConfig.java (2)

25-27: region 하드코딩 제거: 프로퍼티로 오버라이드 가능하게.

R2는 region 개념이 약하지만, 하드코딩 대신 기본값이 'auto'인 프로퍼티 주입으로 유연성을 확보하는 것이 좋습니다.

다음 변경을 제안합니다:

   @Value("${cloudflare.r2.endpoint}")
   private String endpoint;
+
+  // R2는 리전 개념이 약함. 기본값 'auto'를 쓰되 필요 시 오버라이드 가능
+  @Value("${cloudflare.r2.region:auto}")
+  private String region;
 
-  // R2는 리전 개념이 약하므로 "auto"로 하드코딩하거나,
-  // properties에 cloudflare.r2.region 키를 추가한 뒤 @Value로 주입받아도 됩니다.
-  String region = "auto";

application.properties에 명시적으로 정의하고 싶다면 다음을 추가하는 것도 고려해 주세요:

  • cloudflare.r2.region=auto

Also applies to: 34-37


17-27: 구성 바인딩으로 전환 시 유지보수성 향상 (@ConfigurationProperties).

여러 @value 주입 대신 @ConfigurationProperties(prefix = "cloudflare.r2")로 묶으면 테스트/유지보수성이 크게 향상됩니다.

원하시면 R2Properties 클래스를 생성해 바인딩하는 스켈레톤을 드리겠습니다.

src/main/java/com/example/cp_main_be/domain/infra/s3/S3Uploader.java (3)

30-34: 업로드 예외 처리 보강 및 스트림 누수 방지.

putObjectAmazonServiceException/SdkClientException을 던질 수 있습니다. 현재는 IOException만 래핑 중이며, 입력 스트림 close도 보장되지 않습니다.

다음과 같이 보강하는 것을 권장합니다:

-    try {
-      amazonS3.putObject(bucket, fileName, file.getInputStream(), metadata);
-    } catch (IOException e) {
-      throw new IllegalArgumentException("파일 업로드에 실패했습니다.");
-    }
+    try (java.io.InputStream is = file.getInputStream()) {
+      amazonS3.putObject(bucket, fileName, is, metadata);
+    } catch (com.amazonaws.AmazonServiceException | com.amazonaws.SdkClientException e) {
+      throw new RuntimeException("파일 업로드에 실패했습니다: " + e.getMessage(), e);
+    } catch (IOException e) {
+      throw new IllegalArgumentException("파일 업로드에 실패했습니다.", e);
+    }

35-36: 반환 URL의 공개 접근성 검토 필요(사전 서명 URL 또는 퍼블릭 베이스 도메인).

amazonS3.getUrl은 단순 객체 URL을 반환합니다. 버킷이 private이거나 Cloudflare R2의 퍼블릭 설정/커스텀 도메인을 쓰지 않으면 접근 불가일 수 있습니다. 비공개 버킷이라면 presigned URL을 발급하거나, 퍼블릭 CDN/커스텀 도메인 베이스 URL을 프로퍼티화하여 조합해 반환하는 방식을 고려해 주세요.

원하시면 generatePresignedUrl 기반 구현 또는 cloudflare.r2.public-base-url 프로퍼티 도입 방안을 제안하겠습니다.


23-36: 테스트 추가 권장: 업로드/삭제 플로우 및 Key 추출 케이스.

이슈(#38) 체크리스트에 따라,

  • 업로드 후 반환된 URL을 delete에 그대로 넣었을 때 정상 삭제되는지,
  • path-style 및 virtual-host 스타일 URL 모두에서 extractKeyFromUrl가 정확히 동작하는지,
  • 예외 상황(잘못된 URL, 삭제 대상 없음 등) 처리

에 대한 단위/통합 테스트를 추가해 주세요.

테스트 템플릿(예: Mockito로 AmazonS3 모킹 + URL 케이스 파라미터라이즈)을 드릴 수 있습니다. 원하시면 알려주세요.

Also applies to: 38-49

src/main/java/com/example/cp_main_be/domain/social/like/domain/Like.java (2)

30-31: String targetType 대신 Enum 전환을 고려해 주세요.

"DIARY", "AVATAR_POST", "feed" 등 문자열이 혼재되어 오타/도메인 불일치 위험이 큽니다. Enum(예: LikeTargetType) + @Enumerated(EnumType.STRING)으로 정적 타이핑을 권장합니다. 서비스/테스트 단에도 동일 Enum을 사용하면 정합성이 좋아집니다.


14-16: JPA 기본 생성자는 PROTECTED 접근 제어를 권장합니다.

외부에서의 무분별한 인스턴스화를 막을 수 있습니다.

적용 예:

-import lombok.NoArgsConstructor;
+import lombok.NoArgsConstructor;
+import lombok.AccessLevel;
 ...
-@NoArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
src/main/java/com/example/cp_main_be/domain/social/like/service/LikeService.java (3)

41-44: 도메인 용어 정합성: 예외 메시지 '일기' → '피드'로 정정 제안

feed 조회 실패인데 메시지가 "일기를 찾을 수 없습니다."로 표시됩니다.

-              .orElseThrow(() -> new IllegalArgumentException("일기를 찾을 수 없습니다."));
+              .orElseThrow(() -> new IllegalArgumentException("피드를 찾을 수 없습니다."));

21-25: 하드코딩된 문자열 'feed'는 상수화하거나 Enum으로 치환하세요.

매직 스트링은 추후 변경/오타에 취약합니다. 최소 상수화, 가능하면 Enum 통합을 권장합니다.

 public class LikeService {
 
   private final LikeRepository likeRepository;
   private final UserRepository userRepository;
   private final FeedRepository feedRepository;
   private final NotificationService notificationService;
+  private static final String TARGET_FEED = "feed";
 ...
-    if ("feed".equalsIgnoreCase(targetType)) {
+    if (TARGET_FEED.equalsIgnoreCase(targetType)) {

Also applies to: 39-39


33-34: RuntimeException 남발 대신 도메인 예외로 구체화하세요.

DuplicateLikeException, LikeNotFoundException 등으로 교체하면 핸들링/로깅/상태코드 매핑이 명확해집니다.

Also applies to: 62-63

src/test/java/com/example/cp_main_be/domain/social/like/service/LikeServiceTest.java (1)

1-20: 알림(Feed) 시나리오도 테스트에 포함해 주세요.

서비스는 targetType이 "feed"일 때 FeedRepository/NotificationService를 사용합니다. 해당 경로를 커버하는 테스트가 없어서 회귀를 못 잡을 수 있습니다.

아래 패치로 의존성 모킹과 케이스 2건을 추가할 수 있습니다.

@@
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.verify;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
 
 import com.example.cp_main_be.domain.social.like.domain.Like;
 import com.example.cp_main_be.domain.social.like.domain.repository.LikeRepository;
 import com.example.cp_main_be.domain.user.domain.User;
 import com.example.cp_main_be.domain.user.domain.repository.UserRepository;
 import com.example.cp_main_be.global.exception.UserNotFoundException;
+import com.example.cp_main_be.domain.notification.domain.NotificationType;
+import com.example.cp_main_be.domain.notification.service.NotificationService;
+import com.example.cp_main_be.domain.social.feed.domain.Feed;
+import com.example.cp_main_be.domain.social.feed.domain.repository.FeedRepository;
 import java.util.Optional;
@@
   @Mock private LikeRepository likeRepository;
   @Mock private UserRepository userRepository;
+  @Mock private FeedRepository feedRepository;
+  @Mock private NotificationService notificationService;

그리고 클래스 마지막에 테스트 2건 추가:

@@
-}
+  @DisplayName("좋아요 추가(feed) - 소유자와 다른 사용자가 좋아요 시 알림 전송")
+  @Test
+  void addLike_feed_sendsNotification() {
+    // given
+    Long userId = 1L;
+    Long targetId = 10L;
+    String targetType = "feed";
+    User sender = User.builder().id(userId).username("sender").build();
+    User receiver = User.builder().id(2L).username("owner").build();
+
+    Feed feed = org.mockito.Mockito.mock(Feed.class);
+    given(feed.getUser()).willReturn(receiver);
+
+    given(userRepository.findById(userId)).willReturn(Optional.of(sender));
+    given(likeRepository.existsByUserAndTargetIdAndTargetType(sender, targetId, targetType))
+        .willReturn(false);
+    given(feedRepository.findById(targetId)).willReturn(Optional.of(feed));
+
+    // when
+    likeService.addLike(userId, targetId, targetType);
+
+    // then
+    verify(likeRepository).save(any(Like.class));
+    verify(notificationService)
+        .send(receiver, sender, NotificationType.FEED_LIKE, "/feeds/" + targetId);
+  }
+
+  @DisplayName("좋아요 추가(feed) - 자기 자신의 피드 좋아요 시 알림 미전송")
+  @Test
+  void addLike_feed_selfLike_noNotification() {
+    // given
+    Long userId = 1L;
+    Long targetId = 10L;
+    String targetType = "feed";
+    User senderAndOwner = User.builder().id(userId).username("same").build();
+
+    Feed feed = org.mockito.Mockito.mock(Feed.class);
+    given(feed.getUser()).willReturn(senderAndOwner);
+
+    given(userRepository.findById(userId)).willReturn(Optional.of(senderAndOwner));
+    given(likeRepository.existsByUserAndTargetIdAndTargetType(senderAndOwner, targetId, targetType))
+        .willReturn(false);
+    given(feedRepository.findById(targetId)).willReturn(Optional.of(feed));
+
+    // when
+    likeService.addLike(userId, targetId, targetType);
+
+    // then
+    verify(likeRepository).save(any(Like.class));
+    verify(notificationService, org.mockito.Mockito.never())
+        .send(any(), any(), any(), any());
+  }
+}
📜 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 1c74d5e and ca72468.

📒 Files selected for processing (30)
  • src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/domain/Garden.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/presentation/GardenController.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/service/GardenService.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/infra/s3/S3Uploader.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/keyword/domain/Keyword.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/keyword/domain/repository/KeywordRepository.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/keyword/dto/response/KeywordResponse.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/keyword/dto/response/TodayKeywordResponse.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/keyword/presentation/KeywordController.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/keyword/service/KeywordService.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/notification/domain/NotificationType.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/avatarpost/dto/response/PostLikeResponse.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/diary/domain/repository/DiaryRepository.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/diary/dto/request/DiaryWriteRequest.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/diary/dto/response/DiaryIdResponse.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/diary/dto/response/DiaryResponse.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/diary/presentation/DiaryController.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/diary/presentation/DiaryImageController.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/diaryimage/domain/DiaryImageRepository.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/like/domain/Like.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/like/domain/repository/LikeRepository.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/like/service/LikeService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/global/config/AwsConfig.java (1 hunks)
  • src/main/resources/application.properties (1 hunks)
  • src/test/java/com/example/cp_main_be/domain/keyword/service/KeywordServiceTest.java (0 hunks)
  • src/test/java/com/example/cp_main_be/domain/social/avatarpost/service/AvatarPostServiceTest.java (0 hunks)
  • src/test/java/com/example/cp_main_be/domain/social/like/service/LikeServiceTest.java (1 hunks)
  • src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java (2 hunks)
💤 Files with no reviewable changes (20)
  • src/main/java/com/example/cp_main_be/domain/keyword/domain/repository/KeywordRepository.java
  • src/main/java/com/example/cp_main_be/domain/social/diary/presentation/DiaryController.java
  • src/main/java/com/example/cp_main_be/domain/garden/domain/Garden.java
  • src/main/java/com/example/cp_main_be/domain/notification/domain/NotificationType.java
  • src/test/java/com/example/cp_main_be/domain/keyword/service/KeywordServiceTest.java
  • src/main/java/com/example/cp_main_be/domain/keyword/presentation/KeywordController.java
  • src/main/java/com/example/cp_main_be/domain/social/diary/domain/repository/DiaryRepository.java
  • src/main/java/com/example/cp_main_be/domain/keyword/dto/response/KeywordResponse.java
  • src/main/java/com/example/cp_main_be/domain/social/diary/dto/request/DiaryWriteRequest.java
  • src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java
  • src/main/java/com/example/cp_main_be/domain/keyword/domain/Keyword.java
  • src/main/java/com/example/cp_main_be/domain/social/diary/dto/response/DiaryIdResponse.java
  • src/main/java/com/example/cp_main_be/domain/keyword/service/KeywordService.java
  • src/main/java/com/example/cp_main_be/domain/garden/service/GardenService.java
  • src/test/java/com/example/cp_main_be/domain/social/avatarpost/service/AvatarPostServiceTest.java
  • src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java
  • src/main/java/com/example/cp_main_be/domain/social/diary/dto/response/DiaryResponse.java
  • src/main/java/com/example/cp_main_be/domain/social/avatarpost/dto/response/PostLikeResponse.java
  • src/main/java/com/example/cp_main_be/domain/social/diary/presentation/DiaryImageController.java
  • src/main/java/com/example/cp_main_be/domain/keyword/dto/response/TodayKeywordResponse.java
🧰 Additional context used
🧬 Code Graph Analysis (4)
src/test/java/com/example/cp_main_be/domain/social/like/service/LikeServiceTest.java (1)
src/main/java/com/example/cp_main_be/global/exception/UserNotFoundException.java (1)
  • UserNotFoundException (3-7)
src/main/java/com/example/cp_main_be/domain/social/like/domain/Like.java (5)
src/main/java/com/example/cp_main_be/domain/social/avatarpost/domain/AvatarPost.java (1)
  • Entity (10-51)
src/main/java/com/example/cp_main_be/domain/social/bookmark/domain/Bookmark.java (1)
  • Entity (9-44)
src/main/java/com/example/cp_main_be/domain/social/comment/domain/Comment.java (1)
  • Entity (13-58)
src/main/java/com/example/cp_main_be/domain/user/domain/User.java (1)
  • Entity (16-108)
src/main/java/com/example/cp_main_be/domain/social/diary/domain/Diary.java (1)
  • Entity (10-85)
src/main/java/com/example/cp_main_be/domain/garden/presentation/GardenController.java (2)
src/main/java/com/example/cp_main_be/domain/social/avatarpost/presentation/AvatarPostController.java (1)
  • RestController (17-34)
src/main/java/com/example/cp_main_be/domain/user/presentation/UserController.java (1)
  • RestController (19-86)
src/main/java/com/example/cp_main_be/domain/social/like/service/LikeService.java (3)
src/main/java/com/example/cp_main_be/global/exception/UserNotFoundException.java (1)
  • UserNotFoundException (3-7)
src/main/java/com/example/cp_main_be/domain/social/comment/service/CommentService.java (1)
  • Service (17-83)
src/main/java/com/example/cp_main_be/domain/notification/service/NotificationService.java (1)
  • Service (31-216)
🪛 GitHub Actions: Spring Boot CI/CD with AWS
src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java

[error] 67-67: incompatible types: long cannot be converted to Integer

🔇 Additional comments (12)
src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java (2)

5-7: 와일드카드 정적 임포트 제거, 가독성 측면에서 적절합니다

필요한 항목(doAnswer, doThrow, verify)만 명시적으로 임포트한 점 좋습니다.


67-69: UserServiceTest: .level(1L).level(1)로 수정 필요

테스트 빌더의 level 파라미터는 Integer(또는 int)를 받기 때문에, 1L(long) 대신 정수 리터럴 1을 사용해야 합니다. 다른 테스트에서는 이미 정수 리터럴을 사용하고 있으므로, 이 한 곳만 변경하시면 CI 컴파일 오류가 해결됩니다.

  • 수정 대상
    • 파일: src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java
    • 위치: 67번째 줄
-            .level(1L)
+            .level(1)
src/main/java/com/example/cp_main_be/domain/garden/presentation/GardenController.java (1)

10-13: 웹 어노테이션 임포트 정리 LGTM

REST 전용으로 축소된 컨트롤러 목적에 맞게 필요한 어노테이션만 깔끔하게 임포트되어 있습니다.

src/main/resources/application.properties (3)

19-19: 속성 위치 변경은 무해함 (의도 확인 차원에서 OK).

jwt.refresh-token-expiration-milliseconds를 파일 하단으로 이동한 것으로 보입니다. 동작에는 영향이 없고, 가독성 목적이라면 괜찮습니다.

변경 의도가 단순 정리 목적이라면 체크리스트 상 “재현/수정 확인”과는 무관하므로, 해당 이슈(#38)의 테스트 추가 여부만 별도로 확인 부탁드립니다.


15-15: fastapi.server.url 이 실제로 사용되고 있습니다

  • src/main/resources/application.properties (15행): fastapi.server.url=${FASTAPI_URL}
  • src/main/java/com/example/cp_main_be/domain/image/service/ImageProcessingService.java (25행): @Value("${fastapi.server.url}") private String fastapiServerUrl;
  • src/test/java/com/example/cp_main_be/domain/image/service/ImageProcessingServiceTest.java (39행): registry.add("fastapi.server.url", …);

AI 요약과 달리 해당 속성은 애플리케이션과 테스트 코드에서 참조 중입니다.
• 속성을 계속 유지할 예정이라면 현 상태를 유지해도 좋습니다.
• 삭제할 예정이라면 코드 및 테스트에서의 모든 참조를 함께 제거해 주세요.


11-14: 레거시 AWS 키 레퍼런스 미발견 — Cloudflare R2 키만 사용 중

rg -nP 'cloud\.aws\.' 검색 결과, 코드베이스 전역에 더 이상 cloud.aws.* 프로퍼티가 없습니다.
rg -nP 'cloudflare\.r2\.(endpoint|bucket|access-key|secret-key)' 검색 결과, src/main/resources/application.properties, 테스트 리소스, S3Uploader.java, AwsConfig.java 등에서 모두 cloudflare.r2.* 키만 정상 참조 중입니다.
@Bean public AmazonS3 중복 정의도 발견되지 않아, 최신 @Primary 우선 주입 이슈가 없습니다.

위 검증 결과 추가 조치가 필요하지 않습니다.

src/main/java/com/example/cp_main_be/global/config/AwsConfig.java (1)

29-31: AmazonS3 Bean 우선 주입(@primary) 적용 적절 — 중복 정의 없음 확인

전체 Java 파일을 대상으로 @Bean … public AmazonS3 정의를 검색한 결과, 해당 빈이 추가로 선언된 위치는 없습니다.
안심하고 머지 진행해 주세요.

src/main/java/com/example/cp_main_be/domain/infra/s3/S3Uploader.java (1)

20-21: 버킷 프로퍼티 키 전환 LGTM.

cloud.aws.s3.bucket -> cloudflare.r2.bucket로의 전환이 application.properties와 일치합니다.

src/main/java/com/example/cp_main_be/domain/social/like/domain/repository/LikeRepository.java (1)

8-12: LGTM – 기본 쿼리 메서드 구성이 적절합니다.

중복 검사(existsBy)와 단건 조회(findBy) 모두 서비스 요구사항에 부합합니다.

src/main/java/com/example/cp_main_be/domain/social/like/service/LikeService.java (1)

39-50: 좋아요 수 증감 처리 필요 여부 확인

Feed/Diary 등 엔티티에 likeCount 필드가 있다면 add/remove 시 증가/감소가 필요합니다. 현재는 Notification만 발송합니다. 도메인 요구사항을 재확인해 주세요.

원한다면 likeCount 증감 포함 버전으로 패치를 제안하겠습니다.

src/test/java/com/example/cp_main_be/domain/social/like/service/LikeServiceTest.java (2)

29-47: 기본 성공 케이스 테스트는 적절합니다.

중복/사용자 없음/삭제 플로우까지 커버되어 단위 동작 보장은 충분합니다.


35-36: targetType 값의 도메인 일관성 확인("DIARY" vs "feed")

현재 테스트는 "DIARY"를 사용하지만 서비스의 알림 경로는 "feed"에서만 동작합니다. 도메인 표준 용어에 맞게 테스트 상수도 정리하는 것을 권장합니다. (기존 케이스는 유지하고, feed 케이스를 추가하는 것으로도 충분)

Also applies to: 55-56, 75-76, 90-93, 111-116, 131-138

Comment on lines 6 to 13
public interface DiaryImageRepository
extends JpaRepository<
com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage, Integer> {

Optional<DiaryImage> findById(Long imageId);
Optional<com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage> findById(
Long imageId);

void deleteById(Long id);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

ID 타입(Long/Integer) 불일치 + JpaRepository 기본 메서드 중복 정의로 인한 혼선/버그 가능성

  • Line 8: JpaRepository<..., Integer>로 선언되어 있는데, Line 10–11과 Line 13은 Long을 사용합니다. 엔티티의 PK 타입과 Repository 제네릭 ID 타입이 불일치하면, 기본 제공 메서드(findById(ID), deleteById(ID))와의 오버로드/오버샤도잉으로 혼선을 부르고 버그로 이어질 수 있습니다.
  • Spring Data JPA는 이미 findById(ID)/deleteById(ID)를 제공합니다. 동일 이름의 시그니처를 Long으로 재정의/오버로드하는 것은 유지보수성과 안정성 측면에서 좋지 않습니다.

권장 수정:

  • DiaryImage 엔티티의 @id 타입에 맞춰 제네릭 ID 타입을 통일하세요.
  • PK 조회가 목적이라면 커스텀 findById(Long)/deleteById(Long) 선언을 제거하고, JpaRepository 기본 메서드를 그대로 사용하세요.
  • FQCN 대신 동일 패키지의 간단한 타입명을 사용하는 것이 가독성에 유리합니다.

Option A: DiaryImage의 PK가 Long인 경우(가장 일반적)

- public interface DiaryImageRepository
-     extends JpaRepository<
-         com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage, Integer> {
-
-   Optional<com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage> findById(
-       Long imageId);
-
-   void deleteById(Long id);
- }
+ public interface DiaryImageRepository extends JpaRepository<DiaryImage, Long> {
+ }

Option B: DiaryImage의 PK가 Integer인 경우

- public interface DiaryImageRepository
-     extends JpaRepository<
-         com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage, Integer> {
-
-   Optional<com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage> findById(
-       Long imageId);
-
-   void deleteById(Long id);
- }
+ public interface DiaryImageRepository
+     extends JpaRepository<DiaryImage, Integer> {
+   // 주의: 기본 메서드와 시그니처가 충돌하지 않도록 커스텀 선언을 제거합니다.
+ }

문제 진단을 위해 아래 스크립트로 DiaryImage의 PK 타입과 호출부를 점검해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== DiaryImage 엔티티 파일 위치 =="
files=$(fd -a 'DiaryImage.java' || true)
echo "$files"

if [ -n "${files:-}" ]; then
  echo
  echo "== DiaryImage @Id 필드 주변 =="
  echo "$files" | xargs -I{} rg -n -C2 -P '(class\s+DiaryImage\b|@Id\b)' {}
else
  echo "DiaryImage.java not found"
fi

echo
echo "== DiaryImageRepository 사용처(호출부) =="
rg -n -C2 -P '\bDiaryImageRepository\b' --type=java || true

echo
echo "== findById/deleteById 호출 =="
rg -n -C2 -P '\.findById\s*\(|\.deleteById\s*\(' --type=java || true

Length of output: 80507


DiaryImageRepository의 제네릭 ID 타입을 Long으로 통일하고 불필요한 커스텀 메서드를 제거하세요

엔티티 DiaryImage의 PK는 Long id인데, 리포지토리 제네릭에 Integer를 사용하고 있어 스프링 데이터 JPA 기본 메서드(findById, deleteById)와 시그니처가 충돌합니다. 아래와 같이 수정해 주세요:

  • 파일: src/main/java/com/example/cp_main_be/domain/social/diaryimage/domain/DiaryImageRepository.java
-public interface DiaryImageRepository
-    extends JpaRepository<
-        com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage, Integer> {
-
-  Optional<com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage> findById(
-      Long imageId);
-
-  void deleteById(Long id);
-}
+public interface DiaryImageRepository extends JpaRepository<DiaryImage, Long> {
+}
  • extends JpaRepository<DiaryImage, Integer>extends JpaRepository<DiaryImage, Long>
  • 중복 선언된 findById(Long)deleteById(Long) 삭제
  • FQCN 대신 동일 패키지의 DiaryImage 간단명 사용
📝 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 interface DiaryImageRepository
extends JpaRepository<
com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage, Integer> {
Optional<DiaryImage> findById(Long imageId);
Optional<com.example.cp_main_be.domain.social.diaryimage.domain.DiaryImage> findById(
Long imageId);
void deleteById(Long id);
public interface DiaryImageRepository extends JpaRepository<DiaryImage, Long> {
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/social/diaryimage/domain/DiaryImageRepository.java
around lines 6 to 13, the repository incorrectly uses JpaRepository<DiaryImage,
Integer> while the DiaryImage entity PK is Long, and it redundantly re-declares
findById(Long) and deleteById(Long) with FQCNs; change the interface to extend
JpaRepository<DiaryImage, Long>, remove the custom findById and deleteById
method declarations, and replace FQCN references with the simple DiaryImage type
(ensure the DiaryImage import is present).

Comment on lines 12 to 13
@Table(name = "likes")
@Getter
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DB 유니크 제약으로 중복 좋아요를 확실히 방지하고 컬럼명 명시하세요.

현재 existsBy → save 흐름만으로는 동시 요청에서 중복 레코드가 저장될 수 있습니다. DB 레벨 unique 제약(사용자+타겟식별자+타겟타입)으로 원자성을 보장하는 것이 안전합니다. 또한 컬럼명을 명시해 스키마 일관성(스네이크 케이스)도 맞추는 것을 권장합니다.

아래처럼 적용 제안드립니다:

-@Table(name = "likes")
+@Table(
+    name = "likes",
+    uniqueConstraints =
+        @UniqueConstraint(
+            name = "uk_likes_user_target",
+            columnNames = {"user_id", "target_id", "target_type"}))
 ...
-  @Column(nullable = false)
+  @Column(name = "target_id", nullable = false)
   private Long targetId;
 
-  @Column(nullable = false)
+  @Column(name = "target_type", nullable = false)
   private String targetType; // 예: "DIARY", "AVATAR_POST"
 
-  @Column(nullable = false)
+  @Column(name = "created_at", nullable = false)
   private LocalDateTime createdAt;

유니크 제약은 운영 DB에도 반영되어야 합니다. 마이그레이션 도구(Flyway/Liquibase)를 사용 중이면 아래 스크립트로 미반영 여부를 점검해 주세요.

Also applies to: 27-34


🏁 Script executed:

#!/bin/bash
# 'likes' 테이블 생성 및 unique 제약 존재 여부 검색
rg -n -i -C2 'create table.*likes|unique.*likes|uk_likes_user_target|target_id|target_type'

Length of output: 554


운영 DB 유니크 제약 및 컬럼명 명시 누락 확인 — 마이그레이션 추가 필요

rg 결과, migration 파일에서 likes 테이블 생성이나 uk_likes_user_target unique 제약 정의를 찾지 못했습니다.
운영 DB에도 아래 사항을 반드시 반영해주세요:

• src/main/java/com/example/cp_main_be/domain/social/like/domain/Like.java

  • @table 어노테이션에 uniqueConstraints 추가 (user_id, target_id, target_type)
  • 각 필드에 snake_case 컬럼명 명시
    - @Table(name = "likes")
    + @Table(
    +     name = "likes",
    +     uniqueConstraints =
    +         @UniqueConstraint(
    +             name = "uk_likes_user_target",
    +             columnNames = {"user_id", "target_id", "target_type"}))
    
    - @Column(nullable = false)
    + @Column(name = "user_id", nullable = false)
      private Long userId;
    
    - @Column(nullable = false)
    + @Column(name = "target_id", nullable = false)
      private Long targetId;
    
    - @Column(nullable = false)
    + @Column(name = "target_type", nullable = false)
      private String targetType;
    
    - @Column(nullable = false)
    + @Column(name = "created_at", nullable = false)
      private LocalDateTime createdAt;

• 마이그레이션 스크립트(Flyway/Liquibase)에 unique 제약 반영
예: V__add_like_unique_constraint.sql

ALTER TABLE likes
  ADD CONSTRAINT uk_likes_user_target
  UNIQUE (user_id, target_id, target_type);

위 변경이 운영 환경에도 배포되도록 migration 파일을 추가·적용해 주세요.

🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/domain/social/like/domain/Like.java
around lines 12-13, the @Table annotation and entity fields are missing DB-level
unique constraint and explicit snake_case column names; update the @Table to
include uniqueConstraints for (user_id, target_id, target_type) with the name
uk_likes_user_target, and annotate each entity field with @Column(name =
"snake_case_name") for userId, targetId, targetType (and any other fields
persisted) so JPA maps to the correct column names; additionally add a
Flyway/Liquibase migration (e.g., V__add_like_unique_constraint.sql) that
executes ALTER TABLE likes ADD CONSTRAINT uk_likes_user_target UNIQUE (user_id,
target_id, target_type) to ensure the constraint exists in production.

Comment on lines 32 to 38
if (likeRepository.existsByUserAndTargetIdAndTargetType(user, targetId, targetType)) {
throw new RuntimeException("이미 좋아요를 눌렀습니다."); // TODO: Custom Exception
}

Like like = Like.builder().user(user).targetId(targetId).targetType(targetType).build();
likeRepository.save(like);

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

중복 좋아요 동시성 레이스 가능성: exists 후 save는 원자적이지 않습니다.

두 요청이 동시에 들어오면 둘 다 existsBy=false 판단 후 save를 시도해 중복 레코드가 저장될 수 있습니다. DB 유니크 제약(+ 예외 처리)로 보완하세요.

아래처럼 save를 try-catch로 감싸 DataIntegrityViolationException을 중복 시그널로 취급하는 패턴을 제안합니다. (유니크 제약 추가 전제)

+import org.springframework.dao.DataIntegrityViolationException;
 ...
     Like like = Like.builder().user(user).targetId(targetId).targetType(targetType).build();
-    likeRepository.save(like);
+    try {
+      likeRepository.save(like);
+    } catch (DataIntegrityViolationException e) {
+      // 동시성 상황에서의 중복 삽입 방어
+      throw new RuntimeException("이미 좋아요를 눌렀습니다.");
+    }

참고: existsBy는 빠른 단락(UX 메세지)용으로 유지하되, 최종 방어는 DB 유니크 제약과 예외 처리로 하는 구성이 안전합니다.

🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/social/like/service/LikeService.java
around lines 32 to 38, the exists-then-save sequence is not atomic and can race,
so add a DB-level unique constraint on (user_id, target_id, target_type) and
modify the save path to catch the DB uniqueness violation; keep the
existsByUser... check for fast UX feedback but still wrap
likeRepository.save(...) in a try-catch that catches
DataIntegrityViolationException (or the JPA-specific constraint exception),
translate that into a domain-specific DuplicateLikeException (instead of
RuntimeException) and return the appropriate response, and only rethrow other
exceptions. Ensure the repository/save flow no longer relies on existsBy for
correctness but only for early feedback.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/main/java/com/example/cp_main_be/domain/keyword/dto/response/KeywordResponse.java (1)

5-5: 필드 명명 통일성 제안: date → createdAt 고려

Service에서 Keyword.createdAt을 이 DTO의 date 필드로 매핑하고 있어 의미상 혼동 여지가 있습니다. 외부 API 계약이 허용한다면 createdAt으로 명확화하는 것을 권장합니다. 계약 변경이 어렵다면 JavaDoc/Swagger 스키마 설명으로 의미를 보완하는 것도 방법입니다.

src/main/java/com/example/cp_main_be/domain/keyword/service/KeywordService.java (2)

26-28: JDK 16+라면 Collectors.toList() → toList()로 간결화 가능

Java 16 이상이라면 불필요한 Collectors 의존 없이 종결 연산자로 대체할 수 있습니다. 불변 리스트가 반환되어 의도치 않은 외부 변경을 방지하는 장점도 있습니다.

다음 변경을 고려해 주세요:

-    return keywordRepository.findAll().stream()
-        .map(keyword -> new KeywordResponse(keyword.getCreatedAt(), keyword.getKeyword()))
-        .collect(Collectors.toList());
+    return keywordRepository.findAll().stream()
+        .map(k -> new KeywordResponse(k.getCreatedAt(), k.getKeyword()))
+        .toList();

26-28: 정렬/페이징 요구가 있다면 Repository 단계로 위임 권장

findAll()은 정렬이 비결정적일 수 있습니다. 클라이언트가 최신순/생성순을 기대한다면 Repository에서 정렬/페이징까지 처리하도록 변경하는 것을 권장합니다.

예시(참고용):

  • Spring Data 정렬 사용:
    • 서비스:
      import org.springframework.data.domain.Sort;
      return keywordRepository.findAll(Sort.by(Sort.Direction.DESC, "createdAt")).stream()
          .map(k -> new KeywordResponse(k.getCreatedAt(), k.getKeyword()))
          .toList();
    • 혹은 파생 쿼리 메서드: List findAllByOrderByCreatedAtDesc();
  • 데이터가 커질 수 있다면 Pageable을 사용한 페이지네이션 API 설계 고려.
📜 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 ca72468 and c240914.

📒 Files selected for processing (3)
  • src/main/java/com/example/cp_main_be/domain/keyword/dto/response/KeywordResponse.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/keyword/dto/response/TodayKeywordResponse.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/keyword/service/KeywordService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/example/cp_main_be/domain/keyword/dto/response/TodayKeywordResponse.java
🔇 Additional comments (3)
src/main/java/com/example/cp_main_be/domain/keyword/dto/response/KeywordResponse.java (2)

5-5: 레코드(record) 전환 깔끔합니다. 불변 DTO로 보일러플레이트 제거 LGTM

값 객체 성격의 DTO에 record 적용으로 생성자/접근자/equals/hashCode/toString 자동 생성되어 유지보수성 향상되었습니다.


5-5: Jackson Java Record 직렬화 지원 환경 확인 완료
build.gradle에서 Spring Boot 3.5.3(내장 Jackson 2.15.x) 및 Java 17 사용이 확인되었으며, Jackson 2.12+부터 Java Record 직렬화를 기본 지원하므로 별도의 파라미터명 모듈이나 확장 설정이 필요 없습니다.

src/main/java/com/example/cp_main_be/domain/keyword/service/KeywordService.java (1)

26-28: 스트림 기반 리팩토링 간결하고 명확합니다

중간 변수 제거로 가독성이 좋아졌고, DTO 생성 로직도 직관적입니다.

Comment on lines +26 to +28
return keywordRepository.findAll().stream()
.map(keyword -> new KeywordResponse(keyword.getCreatedAt(), keyword.getKeyword()))
.collect(Collectors.toList());
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

레코드 전환에 따른 접근자(getDate/getKeyword) 잔존 사용 여부 점검

DTO가 record로 바뀌면 접근자는 date()/keyword()로 변경됩니다. 기존 테스트/컨트롤러/문서에서 getDate()/getKeyword() 호출이 남아있다면 컴파일/런타임 오류가 발생할 수 있으니 확인 부탁드립니다.

다음 스크립트로 잠재 잔존 호출을 탐색할 수 있습니다(정확한 타입 분해는 아니므로 맥락을 함께 확인해 주세요):


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== record 접근자 전환 전/후 메서드 호출 탐색 =="
rg -n -C2 --type=java '\bgetDate\s*\(' || true
rg -n -C2 --type=java '\bgetKeyword\s*\(' || true

echo
echo "== 새로운 접근자 호출 사용 여부 =="
rg -n -C2 --type=java '\bdate\s*\(' || true
rg -n -C2 --type=java '\bkeyword\s*\(' || true

Length of output: 2369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== getCreatedAt() 호출 탐색 =="
rg -n -C2 --type=java '\bgetCreatedAt\s*\(' || true

echo
echo "== getKeywordDate() 호출 탐색 =="
rg -n -C2 --type=java '\bgetKeywordDate\s*\(' || true

echo
echo "== getId() 호출 탐색 =="
rg -n -C2 --type=java '\bgetId\s*\(' || true

Length of output: 36025


KeywordResponse record 접근자(getDate()/getKeyword()) 수정 필요

AdminServiceTest에서 여전히 result.getKeyword()를 호출하고 있어 컴파일 오류가 발생합니다. record의 접근자는 keyword()이므로 아래와 같이 수정해주세요.

– src/test/java/com/example/cp_main_be/domain/admin/service/AdminServiceTest.java:132
Before: assertThat(result.getKeyword()).isEqualTo("테스트 키워드");
After: assertThat(result.keyword()).isEqualTo("테스트 키워드");

🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/keyword/service/KeywordService.java
around lines 26 to 28, the returned KeywordResponse is fine but tests call Java
record accessors incorrectly; update usages in tests to use record accessor
names (keyword() and createdAt() or date() as defined) instead of
getKeyword()/getDate(). Specifically change AdminServiceTest at
src/test/java/com/example/cp_main_be/domain/admin/service/AdminServiceTest.java
line ~132 from assertThat(result.getKeyword()).isEqualTo("테스트 키워드"); to
assertThat(result.keyword()).isEqualTo("테스트 키워드"); (and similarly replace any
getDate()/getCreatedAt() calls with the record's actual accessor name).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (41)
src/main/java/com/example/cp_main_be/domain/mission/keyword/presentation/KeywordController.java (1)

1-32: 점검 결과 요약 및 권장 조치

  • 구 패키지 참조 없음
    com.example.cp_main_be.domain.keyword 잔존 참조가 발견되지 않아 패키지 리네임이 정상 적용되었습니다.
  • Keyword 엔티티 위치 확인
    src/main/java/com/example/cp_main_be/domain/mission/keyword/domain/Keyword.java 파일 1개만 존재합니다.
  • getTodayKeyword() 구현 확인
    src/main/java/com/example/cp_main_be/domain/mission/keyword/service/KeywordService.java 클래스에서 정상 구현되어 있습니다.
  • 빌드 스크립트(Java 버전) 설정 미발견
    → Gradle(build.gradle/build.gradle.kts) 또는 Maven(pom.xml)에서 sourceCompatibility/targetCompatibility 또는 컴파일 플러그인 설정이 없습니다.
    → Java 16 이상(레코드 지원)으로 명시적으로 설정해주세요.
  • 테스트 코드 부재
    mission/keyword 도메인 관련 테스트(*Keyword*Test.java)가 발견되지 않습니다. 주요 서비스·컨트롤러에 대한 단위/통합 테스트 추가를 권장합니다.
  • 매핑 중복 없음
    /api/v1/keywords RequestMapping은 컨트롤러 단 한 곳에만 선언되어 있습니다.

위 중 Java 버전 설정은 필수 반영 후 빌드 환경을 재확인해주시고, 테스트 코드 보강은 선택 사항이지만 안정성 확보를 위해 권장드립니다.

src/main/java/com/example/cp_main_be/domain/garden/image/DailyMissionImage.java (1)

7-12: JPA 엔티티에 클래스 레벨 @Setter 사용 지양 — 식별자(id) 세터 노출은 위험합니다

엔티티 전체에 @Setter를 부여하면 setId까지 노출되어 데이터 무결성 리스크가 큽니다. 클래스 레벨 @Setter를 제거하고 필요한 필드에만 부여해 주세요.

권장 변경 예시:

 @Entity
 @Getter
-@Setter
 @NoArgsConstructor
 @AllArgsConstructor
 @Builder
 public class DailyMissionImage {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   @Column(name = "image_url")
+  @Setter
   private String imageUrl;
 }

Also applies to: 17-19

src/main/java/com/example/cp_main_be/domain/member/notification/domain/repository/EmitterRepository.java (3)

39-46: ConcurrentHashMap 순회 중 remove 호출 패턴 개선 필요

동일 맵에 대해 forEach 순회 중 remove를 호출하는 패턴은 일부 키가 누락되거나 비결정적 동작을 유발할 수 있습니다. ConcurrentHashMap에서는 entrySet()/keySet()의 removeIf가 더 안전하고 의도가 명확합니다.

다음으로 교체하세요:

-  public void deleteAllEmitterStartWithId(String userId) {
-    emitters.forEach(
-        (key, emitter) -> {
-          if (key.startsWith(userId)) {
-            emitters.remove(key);
-          }
-        });
-  }
+  public void deleteAllEmitterStartWithId(String userId) {
+    emitters.keySet().removeIf(key -> key.startsWith(userId));
+  }

삭제 대상이 많을 때 성능과 가독성도 개선됩니다.


48-55: eventCache에도 동일한 동시성 패턴 개선 적용

위와 동일한 이유로 eventCache에도 removeIf 적용이 필요합니다.

-  public void deleteAllEventCacheStartWithId(String userId) {
-    eventCache.forEach(
-        (key, emitter) -> {
-          if (key.startsWith(userId)) {
-            eventCache.remove(key);
-          }
-        });
-  }
+  public void deleteAllEventCacheStartWithId(String userId) {
+    eventCache.keySet().removeIf(key -> key.startsWith(userId));
+  }

23-27: 접두사 충돌 위험: userId + "_" 구분자 적용 필요
makeTimeIncludeIduserId + "_" + timestamp 형태임에도 불구하고, 단순 .startsWith(userId) 사용 시 예를 들어 사용자 ID "1"이 "10", "11" 등과 매칭될 수 있습니다.
userId + "_" 형태로 접두사를 한정하여 충돌 가능성을 제거하세요.

검토할 위치

  • src/main/java/com/example/cp_main_be/domain/member/notification/domain/repository/EmitterRepository.java
    • findAllEmitterStartWithByUserId (23–27줄)
    • findAllEventCacheStartWithByUserId (29–33줄)
    • emitters.forEach 삭제 로직 (40–44줄)
    • eventCache.forEach 삭제 로직 (49–53줄)

변경 예시

- .filter(entry -> entry.getKey().startsWith(userId))
+ .filter(entry -> entry.getKey().startsWith(userId + "_"))
- if (key.startsWith(userId)) {
+ if (key.startsWith(userId + "_")) {
src/main/java/com/example/cp_main_be/domain/garden/image/service/ImageProcessingService.java (2)

45-61: 3xx 상태를 성공으로 처리할 수 있는 여지 — 비 2xx 응답은 모두 오류로 매핑하세요.
현재 HttpStatusCode::isError는 4xx/5xx만 매칭합니다. 3xx가 오면 bodyToMono(byte[])로 진행되어 예측 불가 동작이 됩니다.

다음과 같이 2xx만 허용하도록 수정해 주세요.

-              .onStatus(
-                  HttpStatusCode::isError,
+              .onStatus(
+                  status -> !status.is2xxSuccessful(),
                   clientResponse ->
                       clientResponse
                           .bodyToMono(String.class)
                           .flatMap(
                               errorBody -> {
                                 log.error(
                                     "FastAPI server error. Status: {}, Body: {}",
                                     clientResponse.statusCode(),
                                     errorBody);
                                 return Mono.error(
                                     new CustomApiException(ErrorCode.AI_AVATAR_FAILED));
                               }))

41-44: 멀티파트 전송 시 filename 누락 가능성 — MultipartBodyBuilder 사용 권장.
BodyInserters.fromMultipartData("image", imageFile.getResource())만으로는 filename 헤더가 누락돼 서버 파싱 문제가 날 수 있습니다.

-              .contentType(MediaType.MULTIPART_FORM_DATA)
-              .body(BodyInserters.fromMultipartData("image", imageFile.getResource()))
+              .contentType(MediaType.MULTIPART_FORM_DATA)
+              .body(BodyInserters.fromMultipartData(
+                  new org.springframework.util.LinkedMultiValueMap<String, Object>() {{
+                    org.springframework.http.client.MultipartBodyBuilder builder =
+                        new org.springframework.http.client.MultipartBodyBuilder();
+                    builder.part("image", imageFile.getResource())
+                        .filename(imageFile.getOriginalFilename())
+                        .contentType(MediaType.parseMediaType(
+                            imageFile.getContentType() != null ? imageFile.getContentType() : MediaType.APPLICATION_OCTET_STREAM_VALUE));
+                    addAll(builder.build());
+                  }}
+              ))

대안: 간결히 하려면 MultipartBodyBuilder를 별도 로컬 변수로 만들어 .body(BodyInserters.fromMultipartData(builder.build())) 적용.

src/test/java/com/example/cp_main_be/domain/image/service/ImageProcessingServiceTest.java (1)

45-65: 검증(assert)이 없어 성공/실패를 식별할 수 없습니다. 기대 결과를 검증해 주세요.
현재 테스트는 메서드만 호출하고 결과를 체크하지 않습니다. 응답 바이트 배열을 비교하도록 보강을 권장합니다. 또한 DisplayName의 "ApiResponse" 문구는 실제 반환 타입(byte[])과 불일치합니다.

   @Test
-  @DisplayName("FastAPI 서버에 이미지 처리 요청 성공 시, 성공 ApiResponse를 반환한다")
+  @DisplayName("FastAPI 서버에 이미지 처리 요청 성공 시, 생성된 바이트 배열을 반환한다")
   void processImageWithAi_fastApi_success() throws Exception {
@@
-    byte[] response = imageProcessingService.processImageWithAi(mockImageFile);
+    byte[] response = imageProcessingService.processImageWithAi(mockImageFile);
+    // then
+    org.assertj.core.api.Assertions.assertThat(response).isEqualTo(processedImageBytes);
   }

추가로 4xx/5xx 케이스와 타임아웃 케이스에 대해 CustomApiException이 발생하는지도 별도 테스트로 보강해 주세요.

src/main/java/com/example/cp_main_be/global/infra/S3Uploader.java (2)

51-61: 키 추출 로직에 정규식 버그와 주석/구현 불일치 — 잘못된 삭제 가능성.
replaceFirst(bucketUrl, "")는 첫 번째 인자를 정규식으로 처리합니다. URL의 '.' 등 메타문자 때문에 예기치 않은 치환이 발생할 수 있습니다. 또한 주석은 replaceAll이라 쓰여 있으나 실제는 replaceFirst입니다.

다음과 같이 정규식 이스케이프 또는 prefix 기반 substring으로 안전하게 수정해 주세요.

+import java.util.regex.Pattern;
@@
   private String extractKeyFromUrl(String imageUrl) {
     // URL에서 버킷 이름과 경로를 제외한 부분 추출
     String bucketUrl = amazonS3.getUrl(bucket, "").toString();
-    // URL 인코딩된 문자열을 처리하기 위해 replaceAll 사용
-    String key = imageUrl.replaceFirst(bucketUrl, "");
+    // URL 인코딩된 문자열을 고려하되, 정규식 메타문자 이슈 방지
+    String key = imageUrl;
+    if (key.startsWith(bucketUrl)) {
+      key = key.substring(bucketUrl.length());
+    } else {
+      // 안전망: regex 기반 치환 (정확 매칭)
+      key = key.replaceFirst(Pattern.quote(bucketUrl), "");
+    }

가능하면 URI로 파싱하여 호스트/패스 기준으로 추출하는 것도 권장됩니다.


25-29: 원본 파일명 사용 시 안전성 이슈 — 파일명 정규화/살균화 권장.
getOriginalFilename()은 사용자 입력을 그대로 담습니다. 공백/경로 구분자/제어문자 제거가 필요합니다.

-    String fileName = path + "/" + UUID.randomUUID().toString() + "_" + file.getOriginalFilename();
+    String safeOriginal = java.util.Optional.ofNullable(file.getOriginalFilename())
+        .map(fn -> fn.replaceAll("[\\r\\n\\\\/\\t\\p{Cntrl}]+", "_"))
+        .map(fn -> fn.length() > 120 ? fn.substring(fn.length() - 120) : fn)
+        .orElse("file");
+    String fileName = path + "/" + UUID.randomUUID() + "_" + safeOriginal;
src/main/java/com/example/cp_main_be/domain/member/user/dto/response/UserResponse.java (1)

15-16: 응답 DTO에서 토큰(Access/Refresh) 노출은 보안 리스크입니다.

Access/Refresh Token을 JSON 바디로 반환하면 XSS/로그/3rd-party 전달로 인해 유출 가능성이 커집니다. Refresh Token은 특히 HttpOnly+Secure+SameSite 쿠키 사용을 권장합니다.

다음과 같이 UserResponse에서 토큰 필드를 제거하고, 별도 AuthTokensResponse DTO로 분리하는 것을 제안합니다.

   private UUID uuid;
-  private String accessToken;
-  private String refreshToken;

별도 DTO(새 파일) 예시:

package com.example.cp_main_be.domain.member.auth.dto.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class AuthTokensResponse {
  private final String accessToken;
  // refreshToken은 기본적으로 HttpOnly 쿠키로만 발급하도록 권장.
}

서비스/컨트롤러에서는:

  • UserResponse: 사용자 프로필/상태만 반환
  • 토큰 발급: AccessToken은 필요시 바디로, RefreshToken은 HttpOnly 쿠키로 설정해 반환

필요하시면 컨트롤러/필터 수정 패치도 제공하겠습니다.

src/main/java/com/example/cp_main_be/domain/social/guestbook/service/GuestbookService.java (1)

37-49: 1일 1회 제한 TOCTOU 레이스 컨디션: DB 유니크 제약 + 예외 처리로 방어 필요

현재는 "조회 후 저장" 패턴이라 동시 요청(예: 동일 writer/owner가 거의 동시에 쓰기)을 둘 다 통과시켜 중복 저장될 수 있습니다. DB 레벨 유니크 제약과 저장 시 예외 처리로 강하게 막아주세요. 또한 존재 여부 조회는 Optional 대신 existsBy...로 변경하면 불필요한 엔티티 로딩을 줄일 수 있습니다.

적용 예시(서비스 측):

+import org.springframework.dao.DataIntegrityViolationException;
+// import com.example.cp_main_be.global.exception.GuestbookDailyLimitExceededException; // 예: 커스텀 예외

   // 1일 1회 제한 로직
-  LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
-  LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
+  LocalDate today = LocalDate.now(clock);
+  LocalDateTime startOfDay = today.atStartOfDay();
+  LocalDateTime endOfDay = today.atTime(LocalTime.MAX);

-  if (guestbookRepository
-      .findByWriterAndOwnerAndCreatedAtBetween(writer, owner, startOfDay, endOfDay)
-      .isPresent()) {
-    throw new RuntimeException("해당 사용자에게는 하루에 한 번만 방명록을 작성할 수 있습니다."); // TODO: Custom Exception
-  }
+  if (guestbookRepository.existsByWriterAndOwnerAndCreatedAtBetween(writer, owner, startOfDay, endOfDay)) {
+    throw new GuestbookDailyLimitExceededException();
+  }

   Guestbook guestbook =
       Guestbook.builder().writer(writer).owner(owner).content(request.getContent()).build();
-  guestbookRepository.save(guestbook);
+  try {
+    guestbookRepository.save(guestbook);
+  } catch (DataIntegrityViolationException e) {
+    // DB 유니크 제약으로 인한 동시성 충돌 방어
+    throw new GuestbookDailyLimitExceededException();
+  }

레포지토리 보완(existsBy 추가):

 public interface GuestbookRepository extends JpaRepository<Guestbook, Long> {
   Optional<Guestbook> findByWriterAndOwnerAndCreatedAtBetween(
       User writer, User owner, LocalDateTime startOfDay, LocalDateTime endOfDay);
+
+  boolean existsByWriterAndOwnerAndCreatedAtBetween(
+      User writer, User owner, LocalDateTime startOfDay, LocalDateTime endOfDay);
 }

DB 유니크 제약(엔티티 예시): createdAt의 날짜 단위로 유니크 제약을 구성하기 어렵다면 writeDate(LocalDate) 컬럼을 두고 (writer_id, owner_id, write_date)에 유니크 인덱스를 추가하는 방식을 권장합니다.

// Guestbook 엔티티 예시
@Table(
  name = "guestbook",
  uniqueConstraints = @UniqueConstraint(
    name = "ux_guestbook_writer_owner_date",
    columnNames = {"writer_id", "owner_id", "write_date"}
  )
)
@Entity
public class Guestbook {
  // ...
  @Column(name = "write_date", nullable = false)
  private LocalDate writeDate;

  @PrePersist
  void prePersist() {
    if (this.writeDate == null) this.writeDate = LocalDate.now(clock); // Clock 주입 권장
  }
}
src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java (1)

77-88: QuizOptions 객체 영속화 및 관계 설정 누락

현재 createQuizOption 메서드는 QuizOptions를 빌드만 하고 save()나 미션과의 연관관계 설정이 빠져 있어, DB에 저장되지 않습니다.

조치 사항:

  • src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java (77–88행)
    • QuizOptions 객체 생성 후 dailyMissionMaster와의 연관관계 설정
    • quizOptionsRepository.save(...) 호출 추가
  • QuizOptionsRepository 주입 (필드·생성자) 및 import 추가

예시 diff:

 public QuizOptions createQuizOption(AdminRequestDTO.CreateQuizRequestDTO requestDTO) {
   DailyMissionMasters dailyMissionMaster =
       dailyMissionMastersRepository
           .findById(requestDTO.getMissionMasterId())
           .orElseThrow(() -> new IllegalStateException("해당 ID를 가진 미션이 존재하지 않습니다."));

-  return QuizOptions.builder()
-      .optionText(requestDTO.getOptionText())
-      .optionOrder(requestDTO.getOptionOrder())
-      .isCorrect(requestDTO.isCorrect())
-      .build();
+  QuizOptions option = QuizOptions.builder()
+      .optionText(requestDTO.getOptionText())
+      .optionOrder(requestDTO.getOptionOrder())
+      .isCorrect(requestDTO.isCorrect())
+      .build();
+  // 연관관계 설정 (필드/메서드명은 실제 엔티티에 맞춰 조정)
+  option.setDailyMissionMaster(dailyMissionMaster);
+  return quizOptionsRepository.save(option);
 }

필요한 필드 및 생성자 예시:

// AdminService 클래스 필드 추가
private final QuizOptionsRepository quizOptionsRepository;

// 생성자에 주입
public AdminService(…, QuizOptionsRepository quizOptionsRepository) {
  …
  this.quizOptionsRepository = quizOptionsRepository;
}
src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java (1)

27-33: UserDailyMissionService 테스트: 올바른 리포지토리 목(mock) 사용 필요
현재 getDailyMissions() 내에서 호출되는 것은

userDailyMissionRepository.findAllByUserId(userId);

뿐이며, dailyMissionMastersRepository는 전혀 사용되지 않습니다. 따라서 테스트에서 아래와 같이 수정해 주세요.

  • File: src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java
    @Mock private DailyMissionMastersRepository dailyMissionMastersRepository;
    @Mock private UserDailyMissionRepository userDailyMissionRepository;
    dailyMissionMastersRepository 관련 stubbing/verification 제거
    userDailyMissionRepository.findAllByUserId(userId) 반환값을 적절히 stub 처리
  • (선택) 다른 메서드 테스트 시 필요에 따라 quizRepository, quizOptionsRepository 등에 대한 목 추가 고려
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java (3)

60-69: 원시 타입(primitive)에 @NotNull은 무효 — 래퍼/제약으로 수정 필요

boolean, int 에 @NotNull 은 효과가 없습니다. 요청 DTO에서 필수 값을 강제하려면 래퍼 타입(Boolean, Integer)로 바꾸고 적절한 제약을 추가하세요. 예: optionOrder는 순서를 나타내므로 @min(1) 또는 @positive 가 적합합니다.

-  public static class CreateQuizRequestDTO {
-    @NotNull private String optionText;
-    @NotNull private boolean isCorrect;
-    @NotNull private int optionOrder;
-    @NotNull private Long missionMasterId;
-  }
+  public static class CreateQuizRequestDTO {
+    @NotNull private String optionText;
+    @NotNull private Boolean isCorrect;
+    @jakarta.validation.constraints.Min(1) private Integer optionOrder;
+    @NotNull private Long missionMasterId;
+  }

추가로 optionText 는 @notblank 가 더 적절할 수 있습니다(공백만 전송 방지).


13-16: 요청 DTO에 @NoArgsConstructor 누락 — Jackson 바인딩 실패 위험

각 내부 DTO가 @builder 만 있고 기본 생성자가 없어(롬복이 빌더용 생성자를 생성) JSON 역직렬화가 실패할 수 있습니다. @NoArgsConstructor 를 추가해 주세요. 필요 시 @AllArgsConstructor, @Jacksonized 도 고려 바랍니다.

 import lombok.Builder;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.NoArgsConstructor;

   public static class CreateMissionRequestDTO {
-  @Getter
-  @Setter
-  @Builder
+  @Getter
+  @Setter
+  @Builder
+  @NoArgsConstructor
   public static class CreateMissionRequestDTO {
@@
-  @Getter
-  @Setter
-  @Builder
+  @Getter
+  @Setter
+  @Builder
+  @NoArgsConstructor
   public static class UpdateMissionRequestDTO {
@@
-  @Getter
-  @Setter
-  @Builder
+  @Getter
+  @Setter
+  @Builder
+  @NoArgsConstructor
   public static class CreateKeywordRequestDTO {
@@
-  @Getter
-  @Setter
-  @Builder
+  @Getter
+  @Setter
+  @Builder
+  @NoArgsConstructor
   public static class ChangeUserStatusRequestDTO {
@@
-  @Getter
-  @Setter
-  @Builder
+  @Getter
+  @Setter
+  @Builder
+  @NoArgsConstructor
   public static class CreateQuizRequestDTO {
@@
-  @Getter
-  @Setter
-  @Builder
+  @Getter
+  @Setter
+  @Builder
+  @NoArgsConstructor
   public static class CreatePlantMasterRequestDTO {
@@
-  @Getter
-  @Setter
-  @Builder
+  @Getter
+  @Setter
+  @Builder
+  @NoArgsConstructor
   public static class UpdatePlantMasterRequestDTO {

Also applies to: 30-33, 42-45, 50-53, 71-74, 81-84


1-90: 테스트 코드 내 구 패키지 네임스페이스 참조 수정 필요

아래 테스트 파일에서 여전히 com.example.cp_main_be.domain.daily_mission_masters.service 패키지를 사용하고 있습니다. 리팩토링된 새로운 패키지 경로로 업데이트하세요.

  • src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java: 패키지 선언 및 import 경로 수정
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (1)

41-41: QuizOptionsRepository 파생 쿼리 메서드명 수정 필요

QuizOptions 엔티티에는 quizId 필드가 없고 @ManyToOne(fetch = LAZY) @JoinColumn(name = "quiz_id") private Quiz quiz;만 정의되어 있어,
findAllByQuizId(...) 호출 시 JPA 파생 쿼리 해석 오류가 발생합니다.

조치 사항:

  • src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/repository/QuizOptionsRepository.java
    - List<QuizOptions> findAllByQuizId(Long quizId);
    + List<QuizOptions> findAllByQuiz_Id(Long quizId);
  • src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java
    - quizOptionsRepository.findAllByQuizId(quiz.getId());
    + quizOptionsRepository.findAllByQuiz_Id(quiz.getId());
  • 테스트 코드(QuizServiceTest, UserDailyMissionServiceTest 등) 내 findAllByQuizId 호출부 및 Mockito 검증부(given/verify)도 동일하게 findAllByQuiz_Id로 변경
src/main/java/com/example/cp_main_be/domain/social/bookmark/domain/Bookmark.java (1)

13-14: 중복 @builder 사용으로 컴파일 실패 가능

클래스 레벨(@line 13)과 생성자 레벨(@line 39)에 동시에 @builder가 선언되어 있어 Lombok이 빌더 생성 시 충돌합니다. 하나만 남겨야 합니다.

클래스 레벨 빌더를 유지하고, 생성자 레벨의 @builder를 제거하는 것을 권장합니다:

-  @Builder
   public Bookmark(User user, AvatarPost post) {
     this.user = user;
     this.avatarPost = post;
   }

(대안) 만약 생성자 기반 빌더를 선호한다면 클래스 레벨 @builder를 제거하고 생성자 레벨만 유지하세요. 두 곳 모두에 @builder는 허용되지 않습니다.

Also applies to: 39-41

src/main/java/com/example/cp_main_be/domain/reports/domain/Reports.java (3)

26-29: ManyToOne에 CascadeType.ALL 적용은 위험합니다 — ReportReason 전파 삭제/수정을 유발할 수 있습니다.

보고서 삭제 시 원인(ReportReason) 엔티티까지 전파 삭제될 수 있어 데이터 무결성을 해칩니다. 일반적으로 ManyToOne에는 cascade를 두지 않거나 최소화(PERSIST)합니다.

권장 변경(diff):

-  @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+  @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "report_reason_id")
   private ReportReason reason;

47-49: ManyToOne 관계에서 CascadeType.ALL 제거 필요
ManyToOne에 CascadeType.ALL이 적용될 경우 자식(Entity) 삭제 시 연관된 부모(Entity)가 함께 삭제될 수 있어 치명적입니다. 아래 모든 ManyToOne 매핑에서 cascade = CascadeType.ALL 옵션을 제거하세요.

• src/main/java/com/example/cp_main_be/domain/mission/user_daily_missions/domain/UserDailyMissions.java

-  @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+  @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "mission_master_id")
   private DailyMissionMasters dailyMissionMasters;

• src/main/java/com/example/cp_main_be/domain/reports/domain/Reports.java (신고 사유)

-  @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+  @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "report_reason_id")
   private ReportReason reason;

• src/main/java/com/example/cp_main_be/domain/reports/domain/Reports.java (사용자)

-  @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+  @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "user_id")
   private User user;

23-25: Enum 매핑 일관성 검토: @Enumerated(EnumType.STRING) 지정 필요

현재 모든 enum 필드가 기본 ORDINAL 방식으로 저장되어, enum 정의 변경 시 데이터 무결성 문제가 발생할 수 있습니다. 아래 필드에 @Enumerated(EnumType.STRING)을 추가해주세요:

  • src/main/java/com/example/cp_main_be/domain/reports/domain/Reports.java
    • Lines 23-25: targetType
    • Lines 34-36: status
  • src/main/java/com/example/cp_main_be/domain/reports/domain/ReportReason.java
    • Line 22: reasonType
  • src/main/java/com/example/cp_main_be/domain/mission/daily_mission_masters/domain/DailyMissionMasters.java
    • Line 26: missionType
  • src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/Quiz.java
    • Line 23: quizType
  • src/main/java/com/example/cp_main_be/domain/member/notification/domain/Notification.java
    • Line 34: notificationType

권장 변경(diff 예시):

--- a/src/main/java/com/example/cp_main_be/domain/reports/domain/Reports.java
@@ -21,7 +21,9 @@ public class Reports {
   @Column(name = "target_type")
-  private TargetType targetType;
+  @Enumerated(EnumType.STRING)
+  @Column(name = "target_type")
+  private TargetType targetType;

   @Column(name = "report_status")
-  private ReportStatus status;
+  @Enumerated(EnumType.STRING)
+  @Column(name = "report_status")
+  private ReportStatus status;
src/main/java/com/example/cp_main_be/domain/member/notification/presentation/NotificationController.java (3)

58-65: SecurityContextHolder에서 UUID를 추출하는 방식 대신 @AuthenticationPrincipal 주입을 사용해 일관화하세요.

현재 컨트롤러 내에서 인증 정보를 가져오는 방식이 혼재되어 있으며, 캐스팅 실패 위험 및 테스트 난이도를 높입니다. subscribe/getNotifications는 @AuthenticationPrincipal을 이미 사용 중입니다.

권장 변경(diff):

-  public ResponseEntity<ApiResponse<Void>> registerNotificationToken(
-      @RequestBody @Valid NotificationTokenRequest request) {
-    String userUuid =
-        (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
-    User user = userService.findUserByUuid(UUID.fromString(userUuid));
-    notificationService.registerOrUpdateDeviceToken(user.getId(), request);
+  public ResponseEntity<ApiResponse<Void>> registerNotificationToken(
+      @AuthenticationPrincipal User user, @RequestBody @Valid NotificationTokenRequest request) {
+    notificationService.registerOrUpdateDeviceToken(user.getId(), request);
     return ResponseEntity.ok(ApiResponse.success(null));
   }

69-76: 알림 설정 API도 @AuthenticationPrincipal을 사용하도록 통일하세요.

권장 변경(diff):

-  public ResponseEntity<ApiResponse<Void>> updateNotificationSettings(
-      @RequestBody @Valid NotificationSettingsRequest request) {
-    String userUuid =
-        (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
-    User user = userService.findUserByUuid(UUID.fromString(userUuid));
-    notificationService.updateNotificationSettings(user.getId(), request);
+  public ResponseEntity<ApiResponse<Void>> updateNotificationSettings(
+      @AuthenticationPrincipal User user, @RequestBody @Valid NotificationSettingsRequest request) {
+    notificationService.updateNotificationSettings(user.getId(), request);
     return ResponseEntity.ok(ApiResponse.success(null));
   }

49-53: 알림 읽음 처리 서비스에 소유권 검증 추가 필요
현재 NotificationService.readNotification(Long notificationId) 메서드는 알림 ID만으로 조회 후 바로 읽음 처리를 하고 있어, 인증된 사용자의 소유 여부를 검증하지 않습니다. 이로 인해 다른 사용자의 알림을 임의로 읽음 처리할 수 있는 보안·권한 문제가 발생합니다.

수정 제안:

  • NotificationService.readNotification 시그니처를
    public void readNotification(Long notificationId, Long userId) 등으로 변경
  • notificationRepository.findByIdAndReceiverId(notificationId, userId) 또는 조회 후 notification.getReceiver().getId() == userId 검증 로직 추가
  • 미일치 시 AccessDeniedException 등 적절한 예외 던지기

점검 위치:

  • src/main/java/com/example/cp_main_be/domain/member/notification/presentation/NotificationController.java:51
  • src/main/java/com/example/cp_main_be/domain/member/notification/service/NotificationService.java:120
src/main/java/com/example/cp_main_be/domain/social/follow/service/FollowService.java (1)

25-37: 자기 자신 팔로우 방지 및 커스텀 예외로 도메인 에러 명확화가 필요합니다

현재 자기 자신을 팔로우하는 요청을 차단하지 않습니다. 데이터 정합성 이슈로 이어질 수 있어 선제 차단이 안전합니다. 또한 RuntimeException 대신 도메인 예외로 의도를 드러내세요.

적용 예:

   public void followUser(Long followerId, Long followingId) {
     User follower =
         userRepository
             .findById(followerId)
             .orElseThrow(() -> new UserNotFoundException("팔로워 사용자를 찾을 수 없습니다."));
     User following =
         userRepository
             .findById(followingId)
             .orElseThrow(() -> new UserNotFoundException("팔로잉할 사용자를 찾을 수 없습니다."));

+    if (follower.getId().equals(following.getId())) {
+      throw new IllegalArgumentException("자기 자신은 팔로우할 수 없습니다."); // TODO: Custom Exception e.g., SelfFollowNotAllowedException
+    }
src/main/java/com/example/cp_main_be/domain/member/auth/presentation/AuthController.java (1)

25-33: Authorization 헤더 파싱이 취약합니다 — "Bearer " 검증 없이 substring(7)을 호출합니다

"Bearer " 접두사가 없을 때 StringIndexOutOfBoundsException으로 500이 발생합니다. 접두사 검증 후 400 응답을 반환하도록 보완하세요.

적용 예:

   @PostMapping("/refresh")
   public ResponseEntity<ApiResponse<TokenRefreshResponse>> refreshAccessToken(
       @RequestHeader("Authorization") String refreshToken) {
     try {
-      TokenRefreshResponse response = authService.refreshAccessToken(refreshToken.substring(7));
+      if (refreshToken == null || !refreshToken.startsWith("Bearer ")) {
+        return ResponseEntity.badRequest()
+            .body(ApiResponse.fail("INVALID_AUTHORIZATION_HEADER", "Authorization 헤더 형식이 올바르지 않습니다."));
+      }
+      String rawRefreshToken = refreshToken.substring(7);
+      TokenRefreshResponse response = authService.refreshAccessToken(rawRefreshToken);
       return ResponseEntity.ok(ApiResponse.success(response));
     } catch (RuntimeException e) {
       // 리프레시 토큰 만료 시, 새로운 익명 계정 생성
       TokenRefreshResponse response = authService.createNewAnonymousAccount();
       return ResponseEntity.ok(ApiResponse.success(response));
     }
   }

참고: ApiResponse.fail(...) 시그니처는 프로젝트 구현에 맞춰 조정이 필요합니다.

src/main/java/com/example/cp_main_be/domain/member/user/domain/repository/UserRepository.java (1)

10-13: DB 유니크 제약 보장 필요

엔티티와 마이그레이션 양쪽에서 username·uuid 컬럼에 유니크 제약이 확실히 적용되어야 합니다.
지금까지 확인된 내용:

  • User.java
    • uuid 필드는 @Column(unique = true) 적용됨
    • username 필드는 @Column(nullable = false)만 적용되어 있음
  • 마이그레이션 스크립트(.sql, Liquibase XML 등)에서는 두 컬럼에 대한 유니크 제약을 찾지 못함

조치 사항:

  • src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java
     ­ @Column(nullable = false)
    + @Column(nullable = false, unique = true)
       private String username;
  • 운영 DB 스키마용 마이그레이션 파일에 아래와 같이 유니크 인덱스/제약 추가
    ALTER TABLE users
      ADD CONSTRAINT uq_users_username UNIQUE (username),
      ADD CONSTRAINT uq_users_uuid     UNIQUE (uuid);
  • (또는) Liquibase/ Flyway XML에 uniqueConstraint 또는 createIndex(unique="true") 설정 추가

위 변경 후 엔티티 자동 DDL 사용 여부(JPA ddl-auto)와 실제 운영 DB에 제약이 반영됐는지 꼭 검증해 주세요.

src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java (5)

51-59: PathVariable 누락으로 런타임 바인딩 실패

@PutMapping("/missions/daily/{id}")인데 파라미터 id에 @PathVariable이 없어 400/500을 유발합니다.

-  public ResponseEntity<ApiResponse<AdminResponseDTO.DailyMissionMastersResDTO>> updateDailyMission(
-      @RequestBody AdminRequestDTO.UpdateMissionRequestDTO requestDTO, Long id) {
+  public ResponseEntity<ApiResponse<AdminResponseDTO.DailyMissionMastersResDTO>> updateDailyMission(
+      @RequestBody AdminRequestDTO.UpdateMissionRequestDTO requestDTO,
+      @PathVariable("id") Long id) {

86-92: RequestBody 누락: 퀴즈 옵션 생성 요청 본문 미바인딩

createQuizOption의 requestDTO는 @RequestBody가 필요합니다.

-  public ResponseEntity<ApiResponse<QuizOptions>> createQuizOption(
-      AdminRequestDTO.CreateQuizRequestDTO requestDTO) {
+  public ResponseEntity<ApiResponse<QuizOptions>> createQuizOption(
+      @RequestBody AdminRequestDTO.CreateQuizRequestDTO requestDTO) {

96-101: RequestBody 누락: 식물 등록 요청 본문 미바인딩

createNewPlant의 requestDTO도 @RequestBody가 필요합니다.

-  public ResponseEntity<ApiResponse<AdminResponseDTO.PlantMasterResDTO>> createNewPlant(
-      AdminRequestDTO.CreatePlantMasterRequestDTO requestDTO) {
+  public ResponseEntity<ApiResponse<AdminResponseDTO.PlantMasterResDTO>> createNewPlant(
+      @RequestBody AdminRequestDTO.CreatePlantMasterRequestDTO requestDTO) {

103-111: RequestBody 누락: 식물 수정 요청 본문 미바인딩

updatePlantMasters의 requestDTO에도 @RequestBody 추가가 필요합니다.

-  public ResponseEntity<ApiResponse<AdminResponseDTO.PlantMasterResDTO>> updatePlantMasters(
-      @PathVariable(name = "id") Long plantId,
-      AdminRequestDTO.UpdatePlantMasterRequestDTO requestDTO) {
+  public ResponseEntity<ApiResponse<AdminResponseDTO.PlantMasterResDTO>> updatePlantMasters(
+      @PathVariable(name = "id") Long plantId,
+      @RequestBody AdminRequestDTO.UpdatePlantMasterRequestDTO requestDTO) {

77-84: @RequestBody 어노테이션 추가 및 서비스 메서드명 오타 수정 필요

아래 이슈를 해결해야 API가 정상 동작합니다:

• AdminController.java (79–81번 줄)
– ChangeUserStatusRequestDTO 파라미터에 @RequestBody가 빠져 있어 JSON 바인딩이 실패합니다.
• AdminController.java (82번 줄)
– adminService.chageUserStatus 호출은 정의된 메서드명이지만 오타로 보이므로 changeUserStatus로 일관성 있게 수정하세요.
• AdminService.java (71번 줄)
– chageUserStatus 메서드명을 changeUserStatus로 변경해 주세요.

--- a/src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java
+++ b/src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java
@@ -79,7 +79,7 @@ public ResponseEntity<ApiResponse<User>> changeUserStatus(
-      AdminRequestDTO.ChangeUserStatusRequestDTO request) {
+      @RequestBody AdminRequestDTO.ChangeUserStatusRequestDTO request) {
@@ -81,7 +81,7 @@ public ResponseEntity<ApiResponse<User>> changeUserStatus(
-    User user = adminService.chageUserStatus(userId, request);
+    User user = adminService.changeUserStatus(userId, request);
--- a/src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java
+++ b/src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java
@@ -71,7 +71,7 @@ public class AdminService {
-  public User chageUserStatus(Long userId, AdminRequestDTO.ChangeUserStatusRequestDTO requestDTO) {
+  public User changeUserStatus(Long userId, AdminRequestDTO.ChangeUserStatusRequestDTO requestDTO) {
src/main/java/com/example/cp_main_be/domain/mission/daily_mission_masters/domain/DailyMissionMasters.java (1)

63-65: 부분 업데이트 로직 버그: isActive null 체크 대상이 잘못되었습니다

현재는 엔티티의 현재 값(this.isActive)을 기준으로 업데이트 여부를 결정하고 있어, 요청 DTO에 isActive가 와도 엔티티 값이 null이면 갱신이 누락됩니다. 요청 DTO를 기준으로 판단해야 합니다.

아래와 같이 수정 제안합니다.

-    if (this.isActive != null) {
-      this.isActive = requestDTO.getIsActive();
-    }
+    if (requestDTO.getIsActive() != null) {
+      this.isActive = requestDTO.getIsActive();
+    }
src/main/java/com/example/cp_main_be/domain/member/user/service/CustomUserDetailsService.java (1)

19-25: 잘못된 UUID 입력 시 500이 아닌 UsernameNotFoundException으로 매핑 필요

UUID 파싱 실패(IllegalArgumentException)가 그대로 전파되면 인증 흐름에서 500을 유발할 수 있습니다. UsernameNotFoundException으로 변환하여 일관된 실패 응답을 보장하세요.

아래와 같이 try/catch로 감싸 UUID 파싱 오류를 변환하고, 파싱된 UUID를 레포지토리 조회에 사용하도록 제안합니다.

   @Override
-  public UserDetails loadUserByUsername(String uuid) throws UsernameNotFoundException {
-    User user =
-        userRepository
-            .findByUuid(UUID.fromString(uuid))
-            .orElseThrow(() -> new UsernameNotFoundException("User not found with uuid: " + uuid));
+  public UserDetails loadUserByUsername(String uuid) throws UsernameNotFoundException {
+    UUID parsedUuid;
+    try {
+      parsedUuid = UUID.fromString(uuid);
+    } catch (IllegalArgumentException e) {
+      throw new UsernameNotFoundException("Invalid UUID format: " + uuid);
+    }
+    User user =
+        userRepository
+            .findByUuid(parsedUuid)
+            .orElseThrow(() -> new UsernameNotFoundException("User not found with uuid: " + uuid));
src/main/java/com/example/cp_main_be/domain/mission/user_daily_missions/presentation/UserDailyMissionController.java (1)

27-31: @AuthenticationPrincipal 타입 불일치 가능성 (런타임 캐스팅 오류 위험)

현재 파라미터 타입이 도메인 User인데, CustomUserDetailsService는 스프링 UserDetails(User)를 반환합니다. 일치하지 않으면 ClassCastException이 발생할 수 있습니다.

두 가지 방향 중 하나로 정합성 맞추는 것을 권장합니다.

  • 컨트롤러 측 단순화(추천):
-  public ResponseEntity<ApiResponse<DailyMissionResponseDTO>> getDailyMissions(
-      @AuthenticationPrincipal User user) {
-    DailyMissionResponseDTO response = userDailyMissionService.getDailyMissions(user.getId());
+  public ResponseEntity<ApiResponse<DailyMissionResponseDTO>> getDailyMissions(
+      @AuthenticationPrincipal(expression = "username") String uuid) {
+    DailyMissionResponseDTO response = userDailyMissionService.getDailyMissionsByUuid(uuid);
     return ResponseEntity.ok(ApiResponse.success(response));
   }

서비스에 getDailyMissionsByUuid(String uuid) 추가가 필요합니다.

  • 혹은 인증 주체를 커스텀 UserDetails(LoginUser)에 도메인 User를 보관하도록 변경하고, @AuthenticationPrincipal LoginUser principal 사용.
    원하시는 쪽 말씀 주시면 일괄 리팩토링 제안 드리겠습니다.
src/main/java/com/example/cp_main_be/domain/social/follow/presentation/FollowController.java (1)

38-42: unfollow 로직에도 동일한 안전한 UUID 해석 적용 필요

follow와 동일하게 principal 캐스팅 및 UUID 파싱의 런타임 예외 가능성이 있습니다. 동일한 헬퍼를 사용하세요.

-    String followerUuid =
-        (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
-    User follower = userService.findUserByUuid(UUID.fromString(followerUuid));
+    java.util.UUID followerUuid = resolveCurrentUserUuid();
+    User follower = userService.findUserByUuid(followerUuid);
src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/Quiz.java (1)

22-24: Enum 매핑에 @Enumerated(EnumType.STRING) 누락 — 데이터 호환성 이슈 위험

기본 ORDINAL 매핑은 enum 순서 변경 시 데이터 붕괴 위험이 큽니다. STRING 매핑으로 명시해 주세요.

-  @Column(name = "quiz_type")
-  private QuizType quizType;
+  @Enumerated(EnumType.STRING)
+  @Column(name = "quiz_type", nullable = false)
+  private QuizType quizType;
src/main/java/com/example/cp_main_be/domain/mission/user_daily_missions/domain/UserDailyMissions.java (1)

34-36: ManyToOne에 cascade = ALL은 매우 위험합니다 (부모 삭제/전파 위험)

자식(UserDailyMissions) → 부모(DailyMissionMasters) 방향의 ManyToOne에 ALL 캐스케이드는 삭제 전파 등으로 상위 엔티티가 의도치 않게 제거/변경될 수 있습니다. 일반적으로 ManyToOne에는 cascade를 두지 않습니다.

-  @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+  @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "mission_master_id")
-  DailyMissionMasters dailyMissionMasters;
+  private DailyMissionMasters dailyMissionMasters;

추가로 필드 접근 제어를 private으로 맞춰 캡슐화를 강화했습니다.

src/main/java/com/example/cp_main_be/domain/member/notification/service/NotificationService.java (2)

84-86: SSE 재연결 시 NPE 가능성 — null/blank 안전성 보강 필요

hasLostDatalastEventId가 null인 경우 NPE를 유발합니다. 헤더 미전달/빈 문자열 시 안전하게 동작하도록 수정하세요.

-  private boolean hasLostData(String lastEventId) {
-    return !lastEventId.isEmpty();
-  }
+  private boolean hasLostData(String lastEventId) {
+    return lastEventId != null && !lastEventId.isBlank();
+  }

66-71: 이벤트 캐시 키를 eventId로 저장하도록 수정 필요

send() 메서드에서 캐시 저장 시 emitterId(key)가 아닌 eventId를 사용해야 sendLostData 복원 로직이 정상 작동합니다.

  • 대상 파일/위치
    • src/main/java/com/example/cp_main_be/domain/member/notification/service/NotificationService.java
    • send 메서드, 66–71라인

  • 수정 예시

 emitters.forEach((key, emitter) -> {
-  emitterRepository.saveEventCache(key, notification);
+  emitterRepository.saveEventCache(eventId, notification);
   sendNotification(emitter, eventId, key, NotificationResponse.from(notification));
 });
  • 변경 후 검증
    findAllEventCacheStartWithByUserId(String.valueOf(userId))가 반환하는 Map의 key가 eventId(userId_timestamp)로 시작하는지 확인

Comment on lines 4 to 12
import com.example.cp_main_be.domain.garden.plant_masters.domain.PlantMasters;
import com.example.cp_main_be.domain.garden.plant_masters.domain.repository.PlantMasterRepository;
import com.example.cp_main_be.domain.member.user.domain.User;
import com.example.cp_main_be.domain.member.user.service.UserService;
import com.example.cp_main_be.domain.mission.daily_keywords.domain.DailyKeywords;
import com.example.cp_main_be.domain.mission.daily_keywords.domain.repository.DailyKeywordsRepository;
import com.example.cp_main_be.domain.mission.daily_mission_masters.domain.DailyMissionMasters;
import com.example.cp_main_be.domain.mission.daily_mission_masters.domain.repository.DailyMissionMastersRepository;
import com.example.cp_main_be.domain.mission.quiz.domain.QuizOptions;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

남은 구 패키지 경로 유입 여부 점검 권장

리팩토링 후 잔존 import가 없는지 레포 전역 점검을 권장합니다. 아래 스크립트로 구 네임스페이스 사용 흔적을 확인해주세요(출력 없으면 정상).


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[CHK] old plant_masters path"
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.plant_masters\b' || true

echo "[CHK] old daily_keywords path"
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.daily_keywords\b' || true

echo "[CHK] old daily_mission_masters path"
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.daily_mission_masters\b' || true

echo "[CHK] old quiz path (non-mission)"
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.quiz\.domain\b' || true

echo "[CHK] old user path"
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.user\.domain\.User\b' || true

Length of output: 1183


구 네임스페이스 잔존 테스트 패키지 경로 수정 필요

리팩토링 후 테스트 코드에 아래 구 패키지 경로가 여전히 남아 있습니다. 테스트 패키지 선언 및 import 경로를 새 네임스페이스(com.example.cp_main_be.domain.mission.daily_mission_masters.service)로 업데이트해주세요.

• src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java:1 (package 선언)

🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java
around lines 4 to 12, the project still references old test-package paths from
the pre-refactor namespace; update the test file
src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java
by changing its package declaration and any import statements that reference the
old package to the new namespace
com.example.cp_main_be.domain.mission.daily_mission_masters.service, ensuring
all imports point to classes in
com.example.cp_main_be.domain.mission.daily_mission_masters.* (and adjust any
test code references accordingly) so the test package and imports match the
refactored code structure.

Comment on lines +5 to +8
import com.example.cp_main_be.domain.member.notification.domain.NotificationType;
import com.example.cp_main_be.domain.member.notification.service.NotificationService;
import com.example.cp_main_be.domain.member.user.domain.User;
import com.example.cp_main_be.domain.member.user.domain.repository.UserRepository;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

임포트 경로 재배치 일관성 확인 OK. 도메인 예외 활용(니트피크) 및 레거시 패키지 참조 점검 권장

  • member.* 네임스페이스로의 이전이 잘 반영되었습니다. 컨트롤러/다른 서비스들에서도 동일한 변경이 누락되지 않았는지 한번에 점검해 주세요.
  • 니트피크: 아바타 미조회 시 IllegalArgumentException 대신 AvatarNotFoundException 같은 도메인 예외가 있으면 교체를 고려해 주세요(로그/전역 예외처리 일관성 향상).

레거시 패키지 경로 잔존 여부 점검:

도메인 예외 예시(외부 변경 필요 코드 예시):

public class AvatarNotFoundException extends RuntimeException {
  public AvatarNotFoundException(String message) { super(message); }
}

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 레거시 Notification/User 패키지 경로
rg -n -C2 -P '\bcom\.example\.cp_main_be\.domain\.notification\.' || true
rg -n -C2 -P '\bcom\.example\.cp_main_be\.domain\.user\.' || true

# import 라인만 별도 확인
rg -n -C0 -P 'import\s+com\.example\.cp_main_be\.domain\.notification\.' || true
rg -n -C0 -P 'import\s+com\.example\.cp_main_be\.domain\.user\.' || true

Length of output: 1041


테스트 코드 레거시 패키지 참조 수정 필요

프로덕션 코드의 import 경로는 잘 변경되었으나, 테스트 코드에서 여전히 옛 패키지를 참조하고 있어 CI/컴파일 오류가 발생할 수 있습니다. 아래 파일들의 패키지 경로를 일관되게 업데이트해주세요.

  • src/test/java/com/example/cp_main_be/domain/notification/service/NotificationServiceTest.java
  • src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java

각 파일에서

- package com.example.cp_main_be.domain.notification.service;
+ package com.example.cp_main_be.domain.member.notification.service;

- import com.example.cp_main_be.domain.user.*;
+ import com.example.cp_main_be.domain.member.user.*;

와 같이 com.example.cp_main_be.domain.notificationcom.example.cp_main_be.domain.member.notification,
com.example.cp_main_be.domain.usercom.example.cp_main_be.domain.member.user 로 변경하세요.

추가 권장(선택):
AvatarService에서 조회 실패 시 IllegalArgumentException 대신

public class AvatarNotFoundException extends RuntimeException {
  public AvatarNotFoundException(String message) { super(message); }
}

같은 도메인 예외를 사용하면 로그 및 전역 예외처리의 일관성이 향상됩니다.

🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/domain/avatar/service/AvatarService.java
lines 5-8 and the two test files listed, update legacy test imports: in
src/test/java/com/example/cp_main_be/domain/notification/service/NotificationServiceTest.java
replace any import starting with com.example.cp_main_be.domain.notification.* to
com.example.cp_main_be.domain.member.notification.*; in
src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java
replace com.example.cp_main_be.domain.user.* to
com.example.cp_main_be.domain.member.user.* so test package paths match
production; optionally, in AvatarService replace thrown IllegalArgumentException
for missing avatar with a custom domain exception AvatarNotFoundException
(create the class extending RuntimeException with a message constructor) and
update references/imports accordingly.

Comment on lines +23 to +28
@Operation(summary = "텃밭 슬롯 조회", description = "텃밭 슬롯을 조회합니다")
@GetMapping("/{gardenId}")
public ResponseEntity<ApiResponse<GardenResponse>> getGarden(@PathVariable Long gardenId) {
GardenResponse gardenResponse = gardenService.findGardenById(gardenId);
return ResponseEntity.ok(ApiResponse.success(gardenResponse));
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

404 매핑 확인: 서비스의 IllegalArgumentException이 404로 변환되는지 검증 필요

현재 Service에서 IllegalArgumentException을 던지는 형태라, 글로벌 예외 핸들러가 404(Not Found)로 매핑하지 않으면 400/500으로 처리될 수 있습니다. 도메인 NotFound 예외 또는 ResponseStatusException(HttpStatus.NOT_FOUND) 사용 여부를 확인해주세요.

테스트/동작 확인을 위해 다음 스크립트로 예외 핸들러에서 IllegalArgumentException을 어떻게 매핑하는지 탐색해보세요.

필요하시면 ControllerAdvice 기반 전역 예외 처리(404 매핑) 스켈레톤 코드를 생성해드릴게요.


🏁 Script executed:

#!/bin/bash
# IllegalArgumentException 매핑 여부/방식 확인
rg -nP -C3 '(@ControllerAdvice|@RestControllerAdvice)' 
rg -nP -C2 'ExceptionHandler\s*\(\s*IllegalArgumentException' 
rg -nP -C2 'ResponseStatusException\(' 

Length of output: 976


#!/bin/bash

글로벌 예외 처리기 내용 확인

sed -n '1,200p' src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java


🏁 Script executed:

#!/bin/bash
# 글로벌 예외 처리기 구현 내용 확인
sed -n '1,200p' src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java

Length of output: 1867


IllegalArgumentException에 대한 404 매핑을 추가해주세요
현재 GardenService#findGardenByIdIllegalArgumentException을 던질 경우, 글로벌 핸들러의 @ExceptionHandler(RuntimeException.class)에 걸려 500 오류로 응답됩니다. Not Found 상황은 404로 반환되어야 하므로, 아래 중 하나를 선택해 반영해주세요.

• 도메인 전용 NotFound 예외(GardenNotFoundException 등)를 정의하고 @ExceptionHandler 또는 @ResponseStatus(HttpStatus.NOT_FOUND)를 사용
• 서비스에서 ResponseStatusException(HttpStatus.NOT_FOUND, "...")를 던지도록 변경
GlobalExceptionHandlerIllegalArgumentException 전용 핸들러 추가

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<Object>> handleIllegalArgument(IllegalArgumentException e) {
  ApiResponse<Object> resp = ApiResponse.failure("NOT_FOUND", e.getMessage());
  return new ResponseEntity<>(resp, HttpStatus.NOT_FOUND);
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/garden/garden/presentation/GardenController.java
around lines 23 to 28, the controller lets GardenService#findGardenById throw
IllegalArgumentException which is currently handled by the generic
RuntimeException handler and returns 500; add a specific handler for
IllegalArgumentException in your GlobalExceptionHandler that maps it to a 404
response: create an @ExceptionHandler(IllegalArgumentException.class) method
that builds an ApiResponse.failure with a NOT_FOUND code/message and returns it
with HttpStatus.NOT_FOUND so Not Found cases from the service return 404 instead
of 500.

Comment on lines 10 to 16
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class GardenService {

private final GardenRepository gardenRepository;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

중복 @service 빈 이름 충돌 가능성 매우 높음

기존 패키지(com.example.cp_main_be.domain.garden.service)에 동일한 클래스명 GardenService(@service)가 존재합니다. 두 클래스 모두 기본 빈 이름이 gardenService로 등록되어 애플리케이션 시작 시 BeanDefinitionOverride 예외가 발생할 수 있습니다. 최소 한쪽에 명시적 빈 이름을 부여하거나 클래스명을 분리(예: GardenQueryService)하세요.

다음과 같이 명시적 빈 이름을 부여하는 즉시 대응을 권장합니다.

-@Service
+@Service("gardenQueryService")
 @RequiredArgsConstructor
 @Transactional(readOnly = true)
 public class GardenService {

대안:

  • 리드 전용 조회 전담 클래스로 리네이밍(GardenQueryService) 후 컨트롤러 의존성도 함께 변경.
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java
around lines 10 to 16, there is a high risk of Spring bean name collision
because another class with the same simple name (GardenService) exists in
com.example.cp_main_be.domain.garden.service; fix by either assigning an
explicit bean name to this service (e.g., @Service("gardenReadService") or
similar) or renaming the class to a distinct name like GardenQueryService and
update all dependent injections (controllers/services) accordingly so the bean
names no longer collide.

@@ -1,8 +1,8 @@
package com.example.cp_main_be.domain.daily_mission_masters.domain;
package com.example.cp_main_be.domain.mission.daily_mission_masters.domain;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

레거시 패키지 참조 잔존 여부 점검 제안

광범위한 네임스페이스 이동이 있었으므로, 남아있는 구 패키지 참조를 한번에 점검하시길 권장합니다.

다음 스크립트로 레거시 패키지 참조를 검색해 주세요. 발견되면 동일한 새로운 경로로 치환하면 됩니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[SEARCH] legacy package usages"
# daily_mission_masters 이전 경로
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.daily_mission_masters\b' || true
# user_daily_missions 이전 경로
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.user_daily_missions\b' || true
# quiz 이전 경로
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.quiz\b' || true
# notification 이전 경로
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.notification\b' || true
# user 도메인 이전 경로
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.user\.domain\.User\b' || true

Length of output: 2079


테스트 패키지의 레거시 참조 제거 필요
아래 테스트 파일들에서 구 패키지 선언(com.example.cp_main_be.domain.{daily_mission_masters, user_daily_missions, quiz, notification})이 남아 있습니다. 새로운 네임스페이스(com.example.cp_main_be.domain.mission…)로 일괄 수정해주세요.

  • src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java
  • src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java
  • src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java
  • src/test/java/com/example/cp_main_be/domain/notification/service/NotificationServiceTest.java

예시 변경(diff):

--- a/src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java
+++ b/src/test/java/com/example/cp_main_be/domain/mission/daily_mission_masters/service/DailyMissionServiceTest.java
@@ -1 +1 @@
-package com.example.cp_main_be.domain.daily_mission_masters.service;
+package com.example.cp_main_be.domain.mission.daily_mission_masters.service;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In the listed test files
(src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java,
src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java,
src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java,
src/test/java/com/example/cp_main_be/domain/notification/service/NotificationServiceTest.java)
update the package declarations from the old namespaces
(com.example.cp_main_be.domain.daily_mission_masters,
com.example.cp_main_be.domain.user_daily_missions,
com.example.cp_main_be.domain.quiz, com.example.cp_main_be.domain.notification)
to the new mission-rooted namespaces
(com.example.cp_main_be.domain.mission.daily_mission_masters,
com.example.cp_main_be.domain.mission.user_daily_missions,
com.example.cp_main_be.domain.mission.quiz,
com.example.cp_main_be.domain.mission.notification) and then adjust any import
statements or references inside each test to use the new package paths so
imports compile and tests run.

Comment on lines 1 to 6
package com.example.cp_main_be.domain.mission.daily_mission_masters.domain.repository;

import com.example.cp_main_be.domain.mission.daily_mission_masters.domain.DailyMissionMasters;
import org.springframework.data.jpa.repository.JpaRepository;

public interface DailyMissionMastersRepository extends JpaRepository<DailyMissionMasters, Long> {}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

구 경로 참조 잔존 여부 점검 권장

모듈 이동으로 인해 기존 임포트가 남아 있을 수 있습니다. 아래 스크립트로 구 패키지 경로 참조를 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# 이전 경로 참조가 남아있는지 탐색
rg -nP -C2 'com\.example\.cp_main_be\.domain\.daily_mission_masters\.domain(\.repository)?\.DailyMissionMasters' --type=java

Length of output: 126


추가 확인을 위해 전체 코드베이스에서 이전 패키지 경로와 디렉토리명이 남아있는지 아래 스크립트로 재검증해 주세요.


🏁 Script executed:

#!/bin/bash
# 이전 패키지 경로 직접 참조 검사
rg -nP 'com\.example\.cp_main_be\.domain\.daily_mission_masters' --type=java -C2

# 디렉토리명 혹은 클래스명에 남은 ‘daily_mission_masters’ 키워드 검사
rg -n 'daily_mission_masters' --type java -C2

Length of output: 16085


테스트 패키지 경로 및 디렉토리명 재구성 필요

다음 테스트 클래스가 여전히
domain/daily_mission_masters/service 디렉토리와
package com.example.cp_main_be.domain.daily_mission_masters.service 를 사용하고 있습니다.

아래와 같이 수정해주세요:

  • 파일 이동
    • src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java
    → src/test/java/com/example/cp_main_be/domain/mission/daily_mission_masters/service/DailyMissionServiceTest.java
  • 패키지 선언 변경
    package com.example.cp_main_be.domain.daily_mission_masters.service;
    package com.example.cp_main_be.domain.mission.daily_mission_masters.service;
🤖 Prompt for AI Agents
In
src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java
(move to
src/test/java/com/example/cp_main_be/domain/mission/daily_mission_masters/service/)
update the package declaration from package
com.example.cp_main_be.domain.daily_mission_masters.service; to package
com.example.cp_main_be.domain.mission.daily_mission_masters.service; and
relocate the file into the new directory structure so the package path and
filesystem match; run tests to confirm imports and references are resolved.

Comment on lines 1 to 4
package com.example.cp_main_be.domain.mission.quiz.dto;

import com.example.cp_main_be.domain.quiz.domain.QuizOptions;
import com.example.cp_main_be.domain.quiz.enums.QuizType;
import com.example.cp_main_be.domain.mission.quiz.domain.QuizOptions;
import com.example.cp_main_be.domain.mission.quiz.enums.QuizType;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DTO에서 도메인 엔티티 직접 노출 최소화 및 필드 불변성 권장

  • 현재 List<QuizOptions>로 도메인 엔티티를 API DTO에 직접 노출하고 있습니다. 응답 전용 QuizOptionResponseDTO로 탈거하는 것을 권장합니다.
  • DTO 필드를 private final로 두고 @Builder로만 생성되도록 하면 불변성과 의도를 강화할 수 있습니다.

레거시 퀴즈 패키지 경로 잔존 여부 점검:

참고 구현 예시(외부 변경 필요 코드 예시):

@Getter
@Builder
public class QuizResponseDTO {
  private final String quizQuestion;
  private final QuizType quizType;
  private final Long missionId;
  private final List<QuizOptionResponseDTO> quizOptions;
  private final Boolean isCompleted;

  @Getter
  @Builder
  public static class QuizOptionResponseDTO {
    private final Long id;
    private final String text;
    private final Boolean isAnswer;
  }
}

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 과거 quiz 네임스페이스 경로
rg -n -C2 -P '\bcom\.example\.cp_main_be\.domain\.quiz\.' || true

Length of output: 430


DTO에서 도메인 엔티티 직접 노출 최소화 및 필드 불변성 강화

  • 현재 List<QuizOptions>로 도메인 엔티티를 직접 노출하고 있습니다. 응답 전용 QuizOptionResponseDTO로 변환해 노출을 최소화하세요.
  • DTO 필드를 private final로 선언하고 @Builder를 통해서만 생성하여 객체 불변성을 보장하는 것이 좋습니다.
  • 테스트 코드에서 아직 구(legacy) 패키지인 com.example.cp_main_be.domain.quiz를 참조하고 있습니다.
    • 위치: src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java
    • 레거시 패키지 참조를 모두 제거하고, 테스트 대상 클래스가 새 패키지(domain.mission.quiz)를 사용하도록 수정해야 합니다.

참고 구현 예시:

@Getter
@Builder
public class QuizResponseDTO {
  private final String quizQuestion;
  private final QuizType quizType;
  private final Long missionId;
  private final List<QuizOptionResponseDTO> quizOptions;
  private final Boolean isCompleted;

  @Getter
  @Builder
  public static class QuizOptionResponseDTO {
    private final Long id;
    private final String text;
    private final Boolean isAnswer;
  }
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/QuizResponseDTO.java
around lines 1-4, replace the direct exposure of the domain entity
List<QuizOptions> with a response-only List<QuizOptionResponseDTO>, make all DTO
fields private final, and add a @Builder (and @Getter) so instances are
immutable and only created via the builder; add an inner static
QuizOptionResponseDTO (or separate class) with private final id/text/isAnswer
and its own @Builder/@Getter, and map domain QuizOptions ->
QuizOptionResponseDTO in the place that builds/returns this DTO. Also update
tests in
src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java to
remove references to the legacy package com.example.cp_main_be.domain.quiz and
import/use classes from com.example.cp_main_be.domain.mission.quiz instead.

Comment on lines +3 to +5
import com.example.cp_main_be.domain.member.user.domain.User;
import com.example.cp_main_be.domain.member.user.domain.repository.UserRepository;
import com.example.cp_main_be.domain.member.user.service.UserService;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

레거시 패키지 잔존 여부 점검 스크립트

domain.user.*domain.member.user.* 리팩터링이 광범위하여, 잔존 import가 없는지 레포 전반 점검을 권장합니다. 아래 스크립트로 빠르게 확인 가능합니다.


🏁 Script executed:

#!/bin/bash
# 레거시 패키지 참조 검색
set -euo pipefail
rg -nP -C2 '\bcom\.example\.cp_main_be\.domain\.user\b' --glob '!**/build/**' --glob '!**/out/**'
echo "----"
echo "위 출력이 없으면 레거시 import는 제거된 상태입니다."

Length of output: 552


테스트 코드 내 레거시 패키지 참조 발견
스크립트 실행 결과, 아래 테스트 파일에서만 com.example.cp_main_be.domain.user.service를 여전히 참조하고 있습니다. 패키지 경로와 import 문을 domain.member.user.service로 변경해주세요.

• src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java

변경 후 동일 스크립트를 재실행해 다른 잔여 레거시 참조가 없는지 재확인 부탁드립니다.

🤖 Prompt for AI Agents
In src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java
(update imports and package references), change any imports or package
declarations referencing com.example.cp_main_be.domain.user.service to
com.example.cp_main_be.domain.member.user.service and update any other legacy
domain.user.* references to domain.member.user.* accordingly; after making these
replacements run the provided verification script to ensure no remaining legacy
references exist.

@@ -1,7 +1,7 @@
package com.example.cp_main_be.domain.social.follow.domain.repository;

import com.example.cp_main_be.domain.member.user.domain.User;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

레거시 import 잔존 여부 점검 스크립트

패키지 리팩토링 후에도 이전 경로(com.example.cp_main_be.domain.user...) 참조가 남아있지 않은지 확인을 권장합니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[검색] 이전 User 패키지 참조"
rg -nC2 -g 'src/**' -P 'com\.example\.cp_main_be\.domain\.user\.domain\.User'

echo
echo "[검색] 이전 Notification 패키지 참조"
rg -nC2 -g 'src/**' -P 'com\.example\.cp_main_be\.domain\.notification\.(?!service\.NotificationServiceTest)'

echo
echo "[검색] 테스트 내 패키지/경로 불일치 가능성"
rg -nC1 -g 'src/test/**' -P '^package\s+com\.example\.cp_main_be\.domain\.notification\.service;'

Length of output: 148


아래와 같이 rg 명령 뒤에 || true를 추가해 첫 실패 시 스크립트가 종료되지 않도록 수정했습니다. 전체 검색이 수행되도록 다시 실행해주세요.


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "[검색] 이전 User 패키지 참조"
rg -nC2 -g 'src/**' -P 'com\.example\.cp_main_be\.domain\.user\.domain\.User' || true

echo
echo "[검색] 이전 Notification 패키지 참조"
rg -nC2 -g 'src/**' -P 'com\.example\.cp_main_be\.domain\.notification\.(?!service\.NotificationServiceTest)' || true

echo
echo "[검색] 테스트 내 패키지/경로 불일치 가능성"
rg -nC1 -g 'src/test/**' -P '^package\s+com\.example\.cp_main_be\.domain\.notification\.service;' || true

Length of output: 1159


레거시 패키지 참조 점검 및 수정 요청

– User 레거시 패키지 참조: 없음
– Notification 레거시 패키지 참조 (테스트 코드):
• 파일: src/test/java/com/example/cp_main_be/domain/notification/service/NotificationServiceTest.java
• 1행: package com.example.cp_main_be.domain.notification.service;
– 아직 이전 패키지 경로를 사용 중입니다.
• import 구문에서도 동일한 경로가 참조되고 있을 수 있습니다.

위 테스트 코드의 패키지 선언과 import 경로를 리팩토링된 패키지 구조에 맞춰 수정해주세요.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (11)
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java (2)

42-45: 이미지 저장 시 영속성 전파 누락 가능성: PERSIST/MERGE 전파와 LAZY 권장

서비스에서 new DailyMissionImage(...)를 세팅만 하고 별도 save를 하지 않는 흐름이 있다면, 현재 매핑(@OnetoOne, cascade 미지정)에서는 TransientPropertyValueException이 발생할 수 있습니다. 또한 OneToOne 기본 fetch는 EAGER이므로 불필요한 로딩이 생길 수 있습니다.

-  @OneToOne
+  @OneToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
   @JoinColumn(name = "image_id")
   private DailyMissionImage dailyMissionImage;

추가 확인: UserDailyMissionService.uploadPictureForDailyMission(...)와 같이 새로운 DailyMissionImage를 세팅만 하는 코드 경로가 있다면 반드시 위 전파 설정이 필요합니다.


42-45: DailyMissionImage 연관관계에 Cascade 설정 추가 필요

UserDailyMissionService에서

userDailyMission.setDailyMissionImage(DailyMissionImage.builder()…​.build());

호출 시 DailyMissionImage 엔티티가 영속화되지 않아 flush 단계에서 org.hibernate.TransientObjectException이 발생할 수 있습니다. 아래 두 가지 대응 중 하나를 적용해 주세요:

• UserDailyMission 엔티티에 CascadeType.PERSIST(또는 ALL) 추가
파일: src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java

- @OneToOne
+ @OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "image_id")
private DailyMissionImage dailyMissionImage;

• 또는 서비스 레이어에서 DailyMissionImageRepository.save(...) 로 명시적 저장

위 변경 후 UserDailyMission 저장 시 TransientObjectException 예외가 발생하지 않는지 확인 부탁드립니다.

src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/domain/DailyMissionMaster.java (1)

63-65: isActive 업데이트 조건 역전 버그: requestDTO 기준으로 null 체크해야 합니다

현재 조건은 this.isActive 기준으로 체크하고 있어 업데이트가 의도대로 동작하지 않습니다. requestDTO.getIsActive()가 null이 아닐 때만 반영하도록 수정해야 합니다.

다음과 같이 수정하세요:

-    if (this.isActive != null) {
-      this.isActive = requestDTO.getIsActive();
-    }
+    if (requestDTO.getIsActive() != null) {
+      this.isActive = requestDTO.getIsActive();
+    }
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java (1)

67-67: 메서드명 오타 수정: summitAnswersubmitAnswer

비즈니스 의미상 ‘제출’ 기능이므로 메서드명을 아래 위치 전반에 걸쳐 일관되게 수정해 주세요.

수정이 필요한 위치:

  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java
    • 64행: 메서드 시그니처 (public Boolean summitAnswer → submitAnswer)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java
    • 67행: 호출부
  • src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java
    • 193, 232, 262행: 테스트 내 호출부

Controller 변경 예시:

-    Boolean result = userDailyMissionService.summitAnswer(request, userDailyMissionId);
+    Boolean result = userDailyMissionService.submitAnswer(request, userDailyMissionId);
src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java (5)

79-83: @RequestBody 누락으로 요청 본문 바인딩 실패

ChangeUserStatusRequestDTO는 본문 JSON으로 받을 가능성이 높습니다. @RequestBody를 추가하세요.

-  public ResponseEntity<ApiResponse<User>> changeUserStatus(
-      @PathVariable(name = "userId") Long userId,
-      AdminRequestDTO.ChangeUserStatusRequestDTO request) {
+  public ResponseEntity<ApiResponse<User>> changeUserStatus(
+      @PathVariable(name = "userId") Long userId,
+      @RequestBody AdminRequestDTO.ChangeUserStatusRequestDTO request) {

추가로 @jakarta.validation.Valid 적용도 고려해 주세요.


88-91: @RequestBody 누락 (퀴즈 선지 생성)

JSON 본문 바인딩을 위해 @RequestBody 필요합니다.

-  public ResponseEntity<ApiResponse<QuizOptions>> createQuizOption(
-      AdminRequestDTO.CreateQuizRequestDTO requestDTO) {
+  public ResponseEntity<ApiResponse<QuizOptions>> createQuizOption(
+      @RequestBody AdminRequestDTO.CreateQuizRequestDTO requestDTO) {

96-101: @RequestBody 누락 (식물 생성)

동일하게 @RequestBody 추가가 필요합니다.

-  public ResponseEntity<ApiResponse<AdminResponseDTO.PlantMasterResDTO>> createNewPlant(
-      AdminRequestDTO.CreatePlantMasterRequestDTO requestDTO) {
+  public ResponseEntity<ApiResponse<AdminResponseDTO.PlantMasterResDTO>> createNewPlant(
+      @RequestBody AdminRequestDTO.CreatePlantMasterRequestDTO requestDTO) {

105-111: @RequestBody 누락 (식물 수정)

JSON 본문을 받도록 수정하세요.

-  public ResponseEntity<ApiResponse<AdminResponseDTO.PlantMasterResDTO>> updatePlantMasters(
-      @PathVariable(name = "id") Long plantId,
-      AdminRequestDTO.UpdatePlantMasterRequestDTO requestDTO) {
+  public ResponseEntity<ApiResponse<AdminResponseDTO.PlantMasterResDTO>> updatePlantMasters(
+      @PathVariable(name = "id") Long plantId,
+      @RequestBody AdminRequestDTO.UpdatePlantMasterRequestDTO requestDTO) {

81-83: 메서드명 오타 수정 필요: chageUserStatuschangeUserStatus

서비스, 컨트롤러, 테스트 전반에서 일관된 메서드명으로 변경해주세요:

  • src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java
    • 메서드 시그니처:
    public User chageUserStatus(...)changeUserStatus(...)
  • src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java
    • 호출부:
    adminService.chageUserStatus(userId, request)adminService.changeUserStatus(userId, request)
  • src/test/java/com/example/cp_main_be/domain/admin/service/AdminServiceTest.java
    • 테스트 코드 호출부:
    adminService.chageUserStatus(userId, requestDTO)adminService.changeUserStatus(userId, requestDTO)

IDE의 Rename Refactoring 기능을 활용하면 연관 호출부까지 안전하게 일괄 변경할 수 있습니다.

src/test/java/com/example/cp_main_be/domain/admin/service/AdminServiceTest.java (1)

187-198: 테스트가 서비스의 버그(퀴즈 옵션 비영속/무연관)를 놓치고 있습니다

AdminService.createQuizOption은 현재 QuizOptions를 빌드만 하고 저장/연관 설정을 하지 않습니다. 테스트도 저장 호출과 Quiz 연관을 검증하지 않아 회귀를 놓칩니다. 저장 호출 및 연관을 검증하도록 보강해주세요.

권장 보강 포인트:

  • QuizRepository, QuizOptionsRepository를 Mock 추가
  • missionMasterId에 해당하는 Quiz 로딩(or 생성) 검증
  • quizOptionsRepository.save(...) 호출 검증
  • 반환 객체의 quiz 연관 검증

원한다면 서비스/테스트 양쪽에 맞춘 패치 초안을 제공하겠습니다.

src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java (1)

77-88: 버그: QuizOptions가 저장되지 않고 Quiz/미션과의 연관도 설정되지 않습니다

현재 createQuizOption은 DailyMissionMaster 조회 후 QuizOptions를 빌드만 하고 반환합니다. 이러면 DB에 반영되지 않고, 어떤 Quiz에 속한 옵션인지 정보도 없습니다. 실사용 시 아무 효과가 없고 이후 조회 흐름(quizOptionsRepository.findAllByQuizId)과도 불일치합니다.

아래처럼 퀴즈 연관을 설정하고 저장하도록 수정하세요(퀴즈가 없을 경우 예외 처리 또는 생성 정책 중 하나를 택해야 합니다).

   public QuizOptions createQuizOption(AdminRequestDTO.CreateQuizRequestDTO requestDTO) {
-    DailyMissionMaster dailyMissionMaster =
-        dailyMissionMasterRepository
-            .findById(requestDTO.getMissionMasterId())
-            .orElseThrow(() -> new IllegalStateException("해당 ID를 가진 미션이 존재하지 않습니다."));
-
-    return QuizOptions.builder() // QuizOptions 퀴즈의 선지
-        .optionText(requestDTO.getOptionText())
-        .optionOrder(requestDTO.getOptionOrder())
-        .isCorrect(requestDTO.isCorrect())
-        .build();
+    DailyMissionMaster dailyMissionMaster =
+        dailyMissionMasterRepository
+            .findById(requestDTO.getMissionMasterId())
+            .orElseThrow(() -> new IllegalStateException("해당 ID를 가진 미션이 존재하지 않습니다."));
+
+    var quiz =
+        quizRepository
+            .findByDailyMissionMaster_Id(dailyMissionMaster.getId())
+            .orElseThrow(() -> new IllegalStateException("퀴즈가 존재하지 않습니다."));
+
+    QuizOptions quizOption =
+        QuizOptions.builder()
+            .quiz(quiz)
+            .optionText(requestDTO.getOptionText())
+            .optionOrder(requestDTO.getOptionOrder())
+            .isCorrect(requestDTO.isCorrect())
+            .build();
+
+    return quizOptionsRepository.save(quizOption);
   }

위 변경을 위해 클래스 상단에 다음 의존성과 임포트를 추가해야 합니다.

추가 필드(RequiredArgsConstructor로 주입):

  • private final QuizRepository quizRepository;
  • private final QuizOptionsRepository quizOptionsRepository;

임포트:

  • com.example.cp_main_be.domain.mission.quiz.domain.repository.QuizRepository
  • com.example.cp_main_be.domain.mission.quiz.domain.repository.QuizOptionsRepository
🧹 Nitpick comments (25)
src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/Quiz.java (1)

25-27: OneToOne 유일성 제약 명시 제안

DB 레벨에서 mission_master_id의 유일성을 보장하려면 unique=true를 명시하는 것이 안전합니다. 현재 @OnetoOne이라도 DB에 유니크 인덱스가 없으면 중복 레코드가 들어갈 수 있습니다.

다음과 같이 변경을 권장합니다:

-  @JoinColumn(name = "mission_master_id")
+  @JoinColumn(name = "mission_master_id", unique = true)
src/main/java/com/example/cp_main_be/domain/member/user/service/UserService.java (1)

45-52: updateAvatar/updateNickname에 save 추가: 동작 안전성↑, 트랜잭션 보장 시에는 생략 가능

클래스 레벨 @transactional 하에서 엔티티 변경은 더티 체킹으로 flush되므로 save는 필수는 아닙니다. 다만 과거에 트랜잭션 경계 문제가 있었다면 save 유지가 안전합니다. 일관성/성능을 위해 명시 save를 제거하는 방향도 고려할 수 있습니다(선호도의 영역).

트랜잭션이 보장된다는 전제에서 아래와 같이 단순화할 수 있습니다:

   public void updateAvatar(Long userId, String newAvatarUrl) {
@@
     user.updateProfile(null, newAvatarUrl);
-    userRepository.save(user);
   }
@@
   public void updateNickname(Long userId, String newNickname) {
@@
     user.updateProfile(newNickname, null);
-    userRepository.save(user);
   }

Also applies to: 54-61

src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/repository/UserDailyMissionRepository.java (1)

8-8: 파생 쿼리 메서드 명시성 개선 제안: findAllByUser_Id

엔티티에 user(연관관계) 필드가 있고 별도의 userId 원시 필드가 없다면, Spring Data 관례상 findAllByUser_Id가 더 명시적이며 오탐을 줄입니다. 현재 이름(findAllByUserId)도 동작할 수 있으나, 버전/도메인 변경 시 취약합니다.

-  List<UserDailyMission> findAllByUserId(Long userId);
+  List<UserDailyMission> findAllByUser_Id(Long userId);
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java (1)

11-19: 이름 일관성 개선 제안: DailyMissionMastersResDTO → DailyMissionMasterResDTO

도메인 모델이 단수형(DailyMissionMaster)으로 통일되었으므로 DTO 명도 단수형으로 맞추는 것이 가독성/탐색성에 유리합니다. 영향 범위가 넓을 수 있으니 후속 리팩터로 고려해 주세요.

src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/repository/QuizRepository.java (1)

9-9: DailyMissionMaster 당 Quiz 1건 가정 시 DB 제약 또는 매핑 강화 권장

비즈니스가 “미션 마스터 1개 ↔ 퀴즈 1개”를 가정한다면:

  • Quiz 테이블의 mission_master_id에 유니크 인덱스를 추가하거나,
  • 엔티티 매핑을 @OnetoOne으로 명시하고 unique=true 제약을 걸어 데이터 무결성을 보장하세요.

현재 메서드는 Optional 하나만 반환하므로, 데이터 레벨 제약이 없으면 중복 데이터가 생겼을 때 예기치 않은 동작이 발생할 수 있습니다.

src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (5)

4-4: 미사용 import 정리

DailyMissionMasterRepository를 사용하지 않습니다. import를 제거해 주세요.

-import com.example.cp_main_be.domain.mission.daily_mission_master.domain.repository.DailyMissionMasterRepository;

23-23: 미사용 의존성 필드 제거

주입만 되고 사용되지 않는 필드는 유지 비용만 늘립니다. 아래처럼 제거해 주세요.

   private final UserDailyMissionRepository userDailyMissionRepository;
-  private final DailyMissionMasterRepository dailyMissionMasterRepository;
   private final QuizRepository quizRepository;
   private final QuizOptionsRepository quizOptionsRepository;

27-27: 읽기 전용 트랜잭션으로 지정하여 성능 최적화

조회용 메서드는 readOnly=true로 지정하면 플러시/더티체킹 비용을 줄일 수 있습니다.

-  public QuizResponseDTO getQuiz(Long userDailyMissionId) {
+  @Transactional(readOnly = true)
+  public QuizResponseDTO getQuiz(Long userDailyMissionId) {

29-39: 예외 타입 정교화 제안

RuntimeException 대신 도메인별 NotFound 예외(또는 ResponseStatusException(HttpStatus.NOT_FOUND))를 사용하면 컨트롤러 단 예외 매핑과 로깅이 일관됩니다. 메시지는 유지하되 예외 타입을 구분하는 것을 권장합니다.


43-48: 엔티티 직접 노출 지양 권장

QuizResponseDTO에 JPA 엔티티(QuizOptions) 리스트를 그대로 싣는 경우, 지연로딩/N+1/순환참조 등 리스크가 있습니다. Option 전용 응답 DTO로 매핑하여 반환하는 방식을 검토해 주세요.

src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java (1)

25-26: 불리언 필드 네이밍 정합성(선택)

boolean isCompleted 필드에 @Setter를 사용하면 Lombok이 setCompleted(...)를 생성하여 필드명/접두어가 혼재합니다. 직렬화/바인딩 단계에서 혼동을 줄이려면 필드명을 completed로 변경하는 것을 고려해 주세요. 현재 코드도 동작에는 문제 없으나 일관성 측면에서 제안드립니다.

src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/domain/DailyMissionMaster.java (1)

68-79: DTO/메서드 네이밍 일관성 제안 (기능에는 영향 없음)

엔티티가 DailyMissionMaster로 정리되었는데, 매핑 메서드/DTO 이름이 DailyMissionMastersResDTO로 남아 있어 혼동 여지가 있습니다. 추후 리팩터링 시 toDailyMissionMasterResDTO 혹은 DailyMissionMasterResDTO 등으로 정리하면 가독성이 올라갑니다.

src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/dto/response/DailyMissionResponseDTO.java (1)

22-28: DailyMissionResponseDTO에 isCompleted 필드 매핑 오버로드 추가 권장드립니다

검증 결과, UserDailyMissionService(src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java:36–38) 에서 UserDailyMission 리스트를
DailyMissionMaster 리스트로 변환한 뒤
DailyMissionResponseDTO.from(List<DailyMissionMaster>)만 호출하고 있어
isCompleted 값이 누락되고 있습니다.

해결을 위해 아래를 권장합니다:

  • MissionSummaryDTO에 from(UserDailyMission) 오버로드 추가
  • DailyMissionResponseDTO에 fromUserDailyMissions(List<UserDailyMission>) 오버로드 추가
  • UserDailyMissionService에서 기존 변환 로직을 제거하고 새로운 오버로드를 호출하도록 수정

예시 코드:

// src/.../dto/response/DailyMissionResponseDTO.java

@Getter @Builder
public static class MissionSummaryDTO {
  Long missionId;
  String missionTitle;
  String missionDescription;
  Boolean isCompleted;

  public static MissionSummaryDTO from(DailyMissionMaster m) {
    return MissionSummaryDTO.builder()
        .missionId(m.getId())
        .missionTitle(m.getTitle())
        .missionDescription(m.getDescription())
        .build();
  }

  // 추가: UserDailyMission → DTO 오버로드
  public static MissionSummaryDTO from(UserDailyMission udm) {
    var m = udm.getDailyMissionMaster();
    return MissionSummaryDTO.builder()
        .missionId(m.getId())
        .missionTitle(m.getTitle())
        .missionDescription(m.getDescription())
        .isCompleted(udm.getIsCompleted())
        .build();
  }
}

// DailyMissionResponseDTO 클래스 내부에 추가
public static DailyMissionResponseDTO fromUserDailyMissions(List<UserDailyMission> udms) {
  List<MissionSummaryDTO> dtos = udms.stream()
      .map(MissionSummaryDTO::from)
      .toList();
  return DailyMissionResponseDTO.builder()
      .todayMissions(dtos)
      .build();
}
// src/.../service/UserDailyMissionService.java

// 변경 전:
dailyMissions.stream()
    .map(UserDailyMission::getDailyMissionMaster)
    .toList();
return DailyMissionResponseDTO.from(dailyMissionMasters);

// 변경 후:
return DailyMissionResponseDTO.fromUserDailyMissions(userDailyMissions);
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java (1)

65-67: 요청 유효성 검사 적용 제안 (@Valid)

요청 DTO에 제약이 있다면 컨트롤러에서 @Valid로 검증을 트리거하는 편이 안전합니다.

간단 적용(별도 import 없이 FQN 사용):

-      @PathVariable(name = "userDailyMissionId") Long userDailyMissionId,
-      @RequestBody QuizRequestDTO request) {
+      @PathVariable(name = "userDailyMissionId") Long userDailyMissionId,
+      @RequestBody @jakarta.validation.Valid QuizRequestDTO request) {
src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java (1)

41-49: 네이밍 일관성 제안: create/updateDailyMissionMasters → …MissionMaster (단수형)

엔티티 명칭이 단수형으로 정리된 만큼 서비스/메서드 명도 단수형으로 맞추면 가독성이 개선됩니다. 기능 영향은 없어 우선순위는 낮습니다.

src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java (4)

30-33: 사용되지 않는 목 제거 권장

dailyMissionMasterRepository 목은 본 테스트에서 사용되지 않습니다. 유지하면 테스트 잡음이 늘어나므로 제거를 권장합니다.

-import com.example.cp_main_be.domain.mission.daily_mission_master.domain.repository.DailyMissionMasterRepository;
...
-  @Mock private DailyMissionMasterRepository dailyMissionMasterRepository;
-
   @Mock private UserDailyMissionRepository userDailyMissionRepository;

65-67: 주석 최신화 제안

현재 테스트는 userDailyMissionRepository.findAllByUserId(userId)를 사용합니다. 주석의 dailyMissionMastersRepository.findAllById(userId) 부분은 혼동을 줄 수 있어 갱신을 권장합니다.

-    // dailyMissionMastersRepository.findAllById(userId) 호출 시 mockMissions를 반환하도록 설정
+    // userDailyMissionRepository.findAllByUserId(userId) 호출 시 mockMissions를 반환하도록 설정

24-26: 테스트 클래스/패키지 명 정합성 제안

테스트 클래스명과 패키지가 리팩터링 전 명칭(daily_mission_masters)을 유지하고 있습니다. 실제 대상 서비스(UserDailyMissionService)에 맞게 파일 경로나 클래스명을 정리하면 추후 유지보수에 유리합니다.


77-86: isCompleted 검증 추가 고려

DTO에 isCompleted가 포함된다면 해당 값에 대한 검증(assertions)도 추가해 시나리오 커버리지를 높이길 권장합니다. (현재 DTO 매핑 상태에 따라 바로 추가가 어려울 수 있으니, 매핑 보완과 함께 진행하세요.)

src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java (1)

114-116: 미호출 검증은 null 인수로 times(0)보다 verifyNoInteractions/never()가 안전합니다

현재 검증은 null 인자로 호출되지 않았음을 확인하는 수준이라, 실제로는 다른 인자로 호출된 경우를 잡아내지 못합니다. 해당 시나리오에서는 레포지토리가 전혀 호출되지 않아야 하므로 미호출 자체를 검증하세요.

아래처럼 교체를 권장합니다.

-    verify(quizRepository, times(0)).findByDailyMissionMaster_Id(null);
-    verify(quizOptionsRepository, times(0)).findAllByQuizId(null);
+    verifyNoInteractions(quizRepository);
+    verifyNoInteractions(quizOptionsRepository);

추가: 상단에 다음 정적 임포트를 추가해야 합니다.

import static org.mockito.Mockito.verifyNoInteractions;
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (3)

41-45: Optional 처리 간소화: orElseThrow로 직접 예외를 던지세요

null 반환 후 수동 체크 대신 orElseThrow가 간결하고 NPE 리스크를 줄입니다.

-    UserDailyMission mission = userDailyMissionRepository.findById(dailyMissionId).orElse(null);
-    if (mission == null) throw new RuntimeException("미션을 찾을 수 없습니다.");
-    userDailyMissionRepository.delete(mission);
+    UserDailyMission mission =
+        userDailyMissionRepository
+            .findById(dailyMissionId)
+            .orElseThrow(() -> new RuntimeException("미션을 찾을 수 없습니다."));
+    userDailyMissionRepository.delete(mission);

55-59: 불필요한 동일성 검사 제거: 업로드 성공 시 완료 처리만 하면 됩니다

setDailyMissionImage 직후 equals 검사는 항상 true이므로 제거해도 됩니다.

-    userDailyMission.setDailyMissionImage(DailyMissionImage.builder().imageUrl(imageUrl).build());
-
-    if (userDailyMission.getDailyMissionImage().getImageUrl().equals(imageUrl)) {
-      userDailyMission.setCompleted(true);
-    }
+    userDailyMission.setDailyMissionImage(DailyMissionImage.builder().imageUrl(imageUrl).build());
+    userDailyMission.setCompleted(true);

64-80: 오탈자: summitAnswer → submitAnswer로의 명명 개선 제안

public API지만, 가능하면 올바른 철자로 변경해 가독성과 검색성을 높이길 권장합니다. 컨트롤러/테스트/호출부 일괄 변경이 필요합니다.

원하시면 리네임에 따른 호출부/테스트 일괄 패치 제안 드리겠습니다.

src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java (2)

231-236: 오답 케이스에서도 외부 의존 호출 검증을 추가하세요

정답 케이스처럼 레포지토리 호출 여부를 검증하면 회귀 방지가 강화됩니다.

   // Then
   assertThat(result).isFalse();
+  verify(userDailyMissionRepository, times(1)).findById(userDailyMissionId);
+  verify(quizRepository, times(1)).findByDailyMissionMaster_Id(dailyMissionMasterId);
+  verify(quizOptionsRepository, times(1)).findAllByQuizId(quizId);

1-1: 테스트 패키지 네이밍(단수형) 통일 권장
프로덕션 패키지(mission.user_daily_mission, mission.daily_mission_master)는 단수형을 사용하는 반면, 테스트 패키지에는 복수형(user_daily_missions, daily_mission_masters)이 남아 있습니다. 기능상 문제는 없지만, 일관성을 위해 테스트 경로와 package 선언을 단수형으로 변경할 것을 권장드립니다.

점검된 대상:

  • src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java
  • src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java

제안 작업:

  • 각 파일을 아래 경로로 이동
    • …/user_daily_missions → …/user_daily_mission
    • …/daily_mission_masters → …/daily_mission_master
  • 최상단 package com.example.cp_main_be.domain.XXX; 선언을 단수형으로 수정
  • 필요 시 IDE 리팩토링 기능으로 관련 import 경로도 일괄 갱신
📜 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 7875116 and 5134033.

📒 Files selected for processing (26)
  • src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java (1 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 (3 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/garden/domain/Garden.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/member/user/dto/response/UserResponse.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/member/user/service/UserService.java (3 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/MissionType.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/domain/DailyMissionMaster.java (3 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/domain/repository/DailyMissionMasterRepository.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/dto/response/DailyMissionResponseDTO.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/Quiz.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/repository/QuizRepository.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java (3 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/repository/UserDailyMissionRepository.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (2 hunks)
  • src/test/java/com/example/cp_main_be/domain/admin/service/AdminServiceTest.java (8 hunks)
  • src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java (5 hunks)
  • src/test/java/com/example/cp_main_be/domain/garden/service/GardenServiceTest.java (4 hunks)
  • src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java (7 hunks)
  • src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java (2 hunks)
  • src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/test/java/com/example/cp_main_be/domain/garden/service/GardenServiceTest.java
  • src/main/java/com/example/cp_main_be/domain/garden/garden/domain/Garden.java
  • src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java
  • src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java
  • src/test/java/com/example/cp_main_be/domain/user/service/UserServiceTest.java
  • src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/domain/repository/DailyMissionMasterRepository.java (1)
src/main/java/com/example/cp_main_be/domain/daily_mission_masters/domain/repository/DailyMissionMastersRepository.java (1)
  • DailyMissionMastersRepository (6-8)
src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/repository/QuizRepository.java (1)
src/main/java/com/example/cp_main_be/domain/quiz/domain/repository/QuizRepository.java (1)
  • QuizRepository (8-11)
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/repository/UserDailyMissionRepository.java (1)
src/main/java/com/example/cp_main_be/domain/user_daily_missions/repository/UserDailyMissionRepository.java (1)
  • UserDailyMissionRepository (9-11)
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (2)
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (1)
  • Service (21-81)
src/main/java/com/example/cp_main_be/domain/quiz/service/QuizService.java (1)
  • Service (20-50)
src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/domain/DailyMissionMaster.java (2)
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminRequestDTO.java (1)
  • AdminRequestDTO (11-90)
src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java (1)
  • AdminResponseDTO (8-33)
🔇 Additional comments (16)
src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/MissionType.java (2)

3-7: 새 MissionType 정의 및 값 구성 LGTM

PHOTO/QUIZ/DIARY 구성 및 가시성(public) 선언 모두 적절합니다. 도메인 리팩토링 방향과도 일치합니다.


1-1: 패키지 경로 변경 후 레거시 참조 및 ORDINAL 매핑 점검 완료

스크립트 실행 결과, 다음과 같이 모두 정상입니다. 추가 조치는 필요하지 않습니다.

  • 레거시 패키지 참조: 0건
  • MissionType.java 중복 정의: 1건
  • JPA EnumType.ORDINAL 사용: 0건
src/main/java/com/example/cp_main_be/domain/member/user/dto/response/UserResponse.java (2)

1-1: 패키지 이동 검증 완료 — 기존 참조 없음 확인됨

rg 검색 결과 com.example.cp_main_be.domain.user.dto.response.UserResponse 경로를 참조하는 구 패키지 호출이 발견되지 않았습니다. 모든 호출부가 새 경로로 정리된 것을 확인했습니다.


22-23: level/experience 타입 일관성 확인 완료
코드베이스 상에서 DTO와 엔티티 모두 Long으로 변경되었으며, 레거시 Integer 사용 흔적은 없습니다.

확인된 사항:

  • User 엔티티(domain/member/user/domain/User.java)의 level/experience 필드가 모두 Long입니다.
  • 코드 전반에서 Integer level 또는 Integer experience 사용은 0건으로 확인되었습니다.

추가 검토 요청:

  • DB 스키마의 해당 컬럼이 BIGINT로 정의되어 있는지 확인
  • 빌더/매퍼 구현부 및 단위 테스트에서 기대 타입(Long)이 올바르게 적용되어 있는지 점검
src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/Quiz.java (1)

27-27: DailyMissionMaster 클래스의 양방향 매핑(mappedBy) 검증 필요

Quiz 쪽 필드명이 dailyMissionMastersdailyMissionMaster로 변경되었으므로, 반대편인 DailyMissionMaster 클래스에 설정된
@OneToOne(mappedBy="dailyMissionMaster") 어노테이션이 있는지 직접 확인해 주세요.
불일치 시 런타임 매핑 오류나 의도치 않은 조인 테이블/컬럼 생성이 발생할 수 있습니다.

  • 확인 대상: DailyMissionMaster 클래스 파일 내 @OneToOne(mappedBy="dailyMissionMaster") 설정 여부
src/main/java/com/example/cp_main_be/domain/member/user/service/UserService.java (1)

1-7: 구 패키지 경로 참조 없음 — 머지 승인

rg 검사 결과, com.example.cp_main_be.domain.user 패키지 경로로의 모든 참조가 제거되었고, member.user 리팩터링이 일관되게 적용되었습니다. 감사합니다.

src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/repository/UserDailyMissionRepository.java (1)

7-9: 새 UserDailyMissionRepository 추가 LGTM

JpaRepository 확장 및 사용자별 조회 메서드 선언이 목적에 부합합니다.

src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/domain/repository/DailyMissionMasterRepository.java (1)

6-6: LGTM — 새 리포지토리 정의가 리네이밍된 도메인과 일치합니다.

JpaRepository 확장은 적절하며, 패키지 이동/엔티티 단수화와 정합성이 맞습니다.

src/main/java/com/example/cp_main_be/domain/admin/dto/AdminResponseDTO.java (1)

3-3: LGTM — MissionType import 경로 변경이 새로운 패키지 구조와 일치합니다.

컴파일 타임 의존성 정합성 측면에서 문제 없습니다.

src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/repository/QuizRepository.java (1)

7-10: LGTM — 파생 쿼리 메서드가 새 연관 필드명(dailyMissionMaster)과 정확히 매칭됩니다.

이전 findByDailyMissionMasters_Id → findByDailyMissionMaster_Id로 올바르게 수정되었습니다.

src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (1)

27-49: 테스트 커버리지 확인 완료
QuizServiceTest에서 getQuiz의 정상 케이스(Quiz/Options 존재)와 예외 케이스(존재하지 않는 userDailyMissionId, Quiz 미존재) 테스트가 이미 구현되어 있어, 추가 보강이 필요 없습니다.

src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/domain/DailyMissionMaster.java (1)

17-17: 클래스명/패키지 정리 LGTM

DailyMissionMaster로의 리네임 및 패키지 이동이 일관성 있게 반영되었습니다.

src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/dto/response/DailyMissionResponseDTO.java (2)

1-3: 패키지/임포트 이동 LGTM

DailyMissionMaster 기반으로의 의존성 정리가 일관적입니다.


32-37: 빌드 JDK 버전 확인 필요
스크립트로 자동 감지를 시도했으나, build.gradle·pom.xml·gradle.properties에서 Java 버전 정보를 찾을 수 없었습니다. 프로젝트의 빌드 설정에서 target/sourceCompatibility 또는 maven-compiler-pluginrelease 설정을 직접 확인해 주세요.

• 대상 파일:
src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/dto/response/DailyMissionResponseDTO.java (32–37행)
• JDK 16 미만 지원 시 대체 코드:

-    List<DailyMissionResponseDTO.MissionSummaryDTO> dailyMissionResponseDTOS =
-        dailyMissionMasters.stream().map(MissionSummaryDTO::from).toList();
+    List<DailyMissionResponseDTO.MissionSummaryDTO> dailyMissionResponseDTOS =
+        dailyMissionMasters.stream()
+            .map(MissionSummaryDTO::from)
+            .collect(java.util.stream.Collectors.toList());
src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java (2)

33-45: createDailyMissionMasters 구현은 리팩터링 방향과 일치합니다

엔티티 생성 → save의 기본 플로우가 새 DailyMissionMaster 네임스페이스에 맞게 잘 반영되었습니다.


47-58: 업데이트는 JPA 더티체킹 가정으로 적절합니다

findById → 엔티티 필드 update(requestDTO) → 반환의 흐름이 트랜잭션 범위 내에서 유효합니다. 명시적 save가 필요하지 않습니다.


public interface QuizRepository extends JpaRepository<Quiz, Long> {

Optional<Quiz> findByDailyMissionMaster_Id(Long missionMasterId);
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

레거시 메서드/타입 잔존 여부 점검 제안

기존 명칭(DailyMissionMasters, findByDailyMissionMasters_Id)을 참조하는 코드가 남아있지 않은지 빠르게 확인해 주세요. 아래 스크립트로 확인 가능합니다.


🏁 Script executed:

#!/bin/bash
# 레거시 명칭/메서드 사용 여부 검색
rg -nP -C2 '(DailyMissionMasters\b|findByDailyMissionMasters_Id\b|dailyMissionMasters\b)'

Length of output: 6882


레거시 명칭(DailyMissionMasters) 잔존 확인 및 통일 필요

rg 검사 결과 아래 경로에서 여전히 레거시 명칭이 사용되고 있습니다. 메서드·DTO 명칭을 singular 형태(DailyMissionMaster)로 통일하거나, 적절한 네이밍으로 수정해주세요.

  • 테스트
    • src/test/java/com/example/cp_main_be/domain/admin/service/AdminServiceTest.java (66, 89, 109):
      • adminService.createDailyMissionMasters(...)
      • adminService.updateDailyMissionMasters(...)
  • 컨트롤러
    • src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java (45, 55):
      • createDailyMissionMasters(...)
      • updateDailyMissionMasters(...)
      • AdminResponseDTO.DailyMissionMastersResDTO
  • 서비스
    • src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java (33, 47):
      • createDailyMissionMasters(...)
      • updateDailyMissionMasters(...)
  • DTO
    • src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/dto/response/DailyMissionResponseDTO.java (32):
      • from(List<DailyMissionMaster> dailyMissionMasters)
  • 유저 미션 서비스
    • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (35):
      • List<DailyMissionMaster> dailyMissionMasters

또한, 만약 findByDailyMissionMasters_Id 호출이 남아 있는지도 점검 후 제거/대체 바랍니다.

🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/quiz/domain/repository/QuizRepository.java
around line 9, the codebase still uses legacy plural naming
("DailyMissionMasters"); change API to use the singular form consistently:
rename any plural references (method names, DTOs, params, variables) to
DailyMissionMaster (e.g., ensure repository method reads
findByDailyMissionMaster_Id and not a plural variant), then update all call
sites listed in the review (tests, controller, service, DTOs, user mission
service) to the singular name; additionally search the repo for any occurrences
of findByDailyMissionMasters_Id and either remove or replace them with the
correct singular method and update imports and tests accordingly so compilation
passes.

Comment on lines 7 to 9
public interface UserDailyMissionRepository extends JpaRepository<UserDailyMission, Long> {
List<UserDailyMission> findAllByUserId(Long userId);
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

엔티티 필드 구조에 따른 메서드명 적합성 검증 스크립트

엔티티에 실제로 userId 필드가 있는지, 아니면 ManyToOne user 연관만 있는지 확인해 주세요. 후자라면 위 제안대로 _ 구분자를 쓰는 것이 안전합니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) UserDailyMission 엔티티 정의 및 user/userId 필드 확인"
rg -nP --type=java -C3 'class\s+UserDailyMission\b|@\w+To\w+\(.*\)\s*private\s+User\s+user\b|\b(Long|Integer)\s+userId\b'

echo
echo "2) 현 리포지토리 메서드 호출부 검색(호출 시그니처 일치 확인)"
rg -nP --type=java -C2 '\buserDailyMissionRepository\.(findAllByUserId|findAllByUser_Id)\s*\(' || true

Length of output: 48668


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 엔티티의 필드 및 어노테이션 확인
sed -n '1,200p' src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java

Length of output: 1322


UserDailyMissionRepository 메서드명 수정 필요: findAllByUserId → findAllByUser_Id

엔티티에 userId 필드가 없고 @ManyToOne으로만 user를 매핑하고 있으므로, JPA가 프로퍼티 경로(user.id)를 인식하도록 메서드명을 변경해야 합니다.

수정할 위치:

  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/repository/UserDailyMissionRepository.java
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (호출부)
  • 관련 테스트 파일(예: UserDailyMissionServiceTest.java, DailyMissionServiceTest.java)에서 모의 설정(given) 및 호출을 모두 업데이트

예상 변경(diff):

 public interface UserDailyMissionRepository extends JpaRepository<UserDailyMission, Long> {
-  List<UserDailyMission> findAllByUserId(Long userId);
+  List<UserDailyMission> findAllByUser_Id(Long userId);
 }
📝 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 interface UserDailyMissionRepository extends JpaRepository<UserDailyMission, Long> {
List<UserDailyMission> findAllByUserId(Long userId);
}
public interface UserDailyMissionRepository extends JpaRepository<UserDailyMission, Long> {
List<UserDailyMission> findAllByUser_Id(Long userId);
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/repository/UserDailyMissionRepository.java
around lines 7-9, rename the repository method from findAllByUserId(Long userId)
to findAllByUser_Id(Long userId) so JPA recognizes the nested property path;
update the call sites in
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java
to call findAllByUser_Id(...) instead of findAllByUserId(...); and update
related tests (e.g., UserDailyMissionServiceTest.java,
DailyMissionServiceTest.java) to adjust mock setups and verifications to use the
new method name (given/when/verify) so compilation and mocking align with the
repository change.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/QuizResponseDTO.java (3)

5-6: List 필드 불변성 강화 및 빌더 사용성 개선: @Singular 사용 권장

List는 여전히 가변입니다. Lombok @Singular를 사용하면 빌더가 방어적 복사 + unmodifiable 컬렉션을 만들어 불변성을 보강하고, builder().quizOption(...) 식의 가독성도 개선됩니다.

아래 패치를 제안합니다.

 import com.example.cp_main_be.domain.mission.quiz.enums.QuizType;
 import java.util.List;
 import lombok.Builder;
 import lombok.Getter;
+import lombok.Singular;

 ...

-  private final List<QuizOptionResponseDTO> quizOptions;
+  @Singular("quizOption")
+  private final List<QuizOptionResponseDTO> quizOptions;

Also applies to: 14-14


15-15: Boolean isCompleted → boolean completed로 이름/타입 정리 권장

  • Boolean은 불필요한 tri-state(null) 가능성을 열어둡니다. 단순 상태값이면 boolean이 적절합니다.
  • 필드명을 isCompleted로 두면 Lombok/Bean 규약에서 getter명이 getIsCompleted()가 되어 혼동을 줍니다. completed로 두면 isCompleted()가 생성되어 자연스럽습니다.

API 호환 유지가 필요하면 @JsonProperty("isCompleted")로 외부 필드명을 보전할 수 있습니다.

-import lombok.Getter;
+import lombok.Getter;
+import com.fasterxml.jackson.annotation.JsonProperty;

 ...

-  private final Boolean isCompleted;
+  @JsonProperty("isCompleted")
+  private final boolean completed;

참고: 이 변경은 서비스/컨트롤러/테스트에서의 필드 접근(빌더, 직렬화 기대값)도 함께 정리해야 합니다.


11-14: 필수 필드에 @NonNull 부여로 빌더 단계에서 조기 실패 유도

응답에 반드시 있어야 하는 값(문항/타입/미션아이디/옵션 등)은 @NonNull을 부여해 실수로 누락 시 빌더 단계에서 즉시 예외가 나도록 하는 것이 안전합니다.

 import java.util.List;
 import lombok.Builder;
 import lombok.Getter;
+import lombok.NonNull;

 ...

-  private final String quizQuestion;
-  private final QuizType quizType;
-  private final Long missionId;
-  private final List<QuizOptionResponseDTO> quizOptions;
+  @NonNull private final String quizQuestion;
+  @NonNull private final QuizType quizType;
+  @NonNull private final Long missionId;
+  @NonNull private final List<QuizOptionResponseDTO> quizOptions;
src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java (1)

33-39: 에러 응답 래퍼가 혼재(ApiResponse vs ErrorResponse)되어 API 일관성이 떨어집니다

현재 CustomApiException은 ErrorResponse, 그 외는 ApiResponse를 사용합니다. 클라이언트 입장에서 실패 응답 포맷이 둘로 갈리면 파싱/처리 비용이 증가합니다. 장기적으로 실패 응답 포맷을 하나로 통일하는 것을 권장합니다(예: 모두 ApiResponse.failure(...)로 통일하거나, 모두 ErrorResponse로 통일).

📜 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 5134033 and ac9aa6b.

📒 Files selected for processing (3)
  • src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/QuizResponseDTO.java (1 hunks)
  • src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java (1 hunks)
  • src/main/resources/db/migration/V2__add_like_unique_constraint.sql (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/resources/db/migration/V2__add_like_unique_constraint.sql
🔇 Additional comments (1)
src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/QuizResponseDTO.java (1)

8-16: 불변 DTO(+Builder) 및 옵션 DTO 분리 적용 — 좋습니다

  • 필드 final + @Builder 조합으로 의도와 불변성이 명확합니다.
  • 도메인 엔티티 대신 QuizOptionResponseDTO를 노출하도록 분리된 점도 적절합니다.

Comment on lines 41 to 45
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<Object>> handleIllegalArgument(IllegalArgumentException e) {
ApiResponse<Object> resp = ApiResponse.failure("NOT_FOUND", e.getMessage());
return new ResponseEntity<>(resp, HttpStatus.NOT_FOUND);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

IllegalArgumentException을 404로 전역 매핑하는 것은 위험합니다 — 400(BAD_REQUEST)로 변경하고, 404는 NotFound 전용 예외로 처리하세요

IllegalArgumentException은 대개 클라이언트의 잘못된 입력(400)에 해당합니다. 전역으로 404에 매핑하면, 입력 검증 실패/파라미터 오류 등도 모두 404로 내려가 HTTP 시맨틱이 훼손되고, 디버깅과 모니터링이 어려워집니다. 리소스 미존재는 도메인 전용 NotFound 예외(예: NoSuchElementException, ResourceNotFoundException, 또는 CustomApiException(ErrorCode.NOT_FOUND))로 분리해 404로 응답하는 것이 안전합니다. 또한 스택트레이스 로깅과 변수명 일관성도 함께 반영을 권장합니다.

아래처럼 수정을 권장합니다:

-  @ExceptionHandler(IllegalArgumentException.class)
-  public ResponseEntity<ApiResponse<Object>> handleIllegalArgument(IllegalArgumentException e) {
-    ApiResponse<Object> resp = ApiResponse.failure("NOT_FOUND", e.getMessage());
-    return new ResponseEntity<>(resp, HttpStatus.NOT_FOUND);
-  }
+  @ExceptionHandler(IllegalArgumentException.class)
+  public ResponseEntity<ApiResponse<Object>> handleIllegalArgumentException(IllegalArgumentException e) {
+    logger.warn("IllegalArgumentException handled: {}", e.getMessage(), e);
+    ApiResponse<Object> response = ApiResponse.failure("BAD_REQUEST", e.getMessage());
+    return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
+  }

404 매핑은 별도의 NotFound 전용 예외 핸들러를 추가해 주세요(예: 서비스에서 해당 예외를 던지도록 변경):

// 필요 시 상단에 import 추가: import java.util.NoSuchElementException;

@ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<ApiResponse<Object>> handleNoSuchElement(NoSuchElementException e) {
  logger.warn("NoSuchElementException handled: {}", e.getMessage(), e);
  ApiResponse<Object> response = ApiResponse.failure("NOT_FOUND", e.getMessage());
  return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
}

필요하시면 LikeService 등에서 발생시키는 예외 타입을 일괄 정리(IllegalArgumentException → NoSuchElementException 또는 CustomApiException(ErrorCode.NOT_FOUND))하는 패치를 함께 제안드릴게요.


💡 Verification agent

🧩 Analysis chain

전역 영향도 점검: IllegalArgumentException 사용처를 확인해 주세요

해당 핸들러는 애플리케이션 전역에 영향을 줍니다. 기존에 입력 검증/파라미터 오류에도 IllegalArgumentException을 쓰고 있다면, 현재 구현은 모두 404로 응답하게 됩니다. 아래 스크립트로 사용처를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "==== throw new IllegalArgumentException 사용처 (컨텍스트 포함) ===="
rg -nP --type=java -C3 'throw\s+new\s+IllegalArgumentException\s*\('

echo
echo "==== IllegalArgumentException 참조 전역 검색 (선언/던짐/핸들링 포함, 컨텍스트 포함) ===="
rg -nP --type=java -C3 '\bIllegalArgumentException\b'

echo
echo "==== (참고) NoSuchElementException 던지는 곳 ===="
rg -nP --type=java -C3 'throw\s+new\s+NoSuchElementException\s*\('

Length of output: 12858


전역 예외 매핑 재검토 필요: IllegalArgumentException의 범용 처리로 잘못된 HTTP 상태코드 반환

현재 전역 핸들러는 모든 IllegalArgumentException을 404 NOT_FOUND로 응답하도록 되어 있습니다. 그러나 코드베이스 전반에서 IllegalArgumentException이 다음과 같은 다양한 상황에서 사용되므로, 일괄적으로 404로 매핑하면 오히려 잘못된 응답을 내보낼 우려가 있습니다.

점검된 주요 사용처:

  • src/main/java/com/example/cp_main_be/global/infra/S3Uploader.java (파일 업로드 실패 → IllegalArgumentException("파일 업로드에 실패했습니다."))
  • src/main/java/com/example/cp_main_be/domain/garden/garden/service/GardenService.java (리소스 미존재 시)
  • src/main/java/com/example/cp_main_be/domain/member/user/service/UserService.java
  • src/main/java/com/example/cp_main_be/domain/member/notification/service/NotificationService.java
  • src/main/java/com/example/cp_main_be/domain/social/avatarpost/service/AvatarPostService.java
  • src/main/java/com/example/cp_main_be/domain/social/comment/service/CommentService.java
  • src/main/java/com/example/cp_main_be/domain/admin/service/AdminService.java
  • src/main/java/com/example/cp_main_be/domain/social/like/service/LikeService.java
  • src/main/java/com/example/cp_main_be/domain/avatar/service/AvatarService.java
  • 각종 테스트 코드 (assertThrows(IllegalArgumentException.class, …))

제안 사항:

  • 리소스 미존재 전용 예외(예: ResourceNotFoundException)를 정의하여 orElseThrow에 사용
  • 파라미터 검증 실패인프라 오류는 각각 BadRequestException, InternalServerErrorException 등으로 구분
  • 전역 핸들러에서 각 예외별로 별도의 @ExceptionHandler를 추가하여, 상황에 맞는 HTTP 상태코드를 반환

이렇게 분리하면, 잘못된 404 응답 문제를 해결하고 API 소비자가 오류 원인을 정확히 파악할 수 있습니다.

🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java
around lines 41-45, the global handler currently maps all
IllegalArgumentException to 404 NOT_FOUND; change this by introducing specific
exception types (e.g., ResourceNotFoundException for missing resources,
BadRequestException for parameter/validation errors, and
InternalServerErrorException for infra failures), update service/infra code to
throw those specific exceptions (or wrap IllegalArgumentException where
semantically appropriate), and add separate @ExceptionHandler methods in this
class mapping each new exception to the correct HTTP status (NOT_FOUND,
BAD_REQUEST, INTERNAL_SERVER_ERROR); remove or repurpose the blanket
IllegalArgumentException handler (e.g., treat it as BAD_REQUEST) so 404 is only
returned for ResourceNotFoundException.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/ci-cd.yml (1)

101-107: 필수: API_TOKEN 시크릿 및 프로퍼티 매핑 검증 및 보완

  • REPLICATE_API_TOKEN 참조는 코드·워크플로우 전역에서 모두 제거된 것을 확인했습니다.
  • 테스트용 src/test/resources/application.properties에는 replicate.api.token: ${API_TOKEN} 매핑이 있으나,
    실제 src/main/resources/application*.{properties,yml,yaml}에는 해당 매핑이 없습니다.
  • 워크플로우에서 secrets.API_TOKEN이 설정돼 있는지, 포크 PR 빌드 시 시크릿 부재로 실패하지 않도록 조건도 다시 확인이 필요합니다.

수정 제안:

  1. 메인 설정파일에 프로퍼티 추가
# src/main/resources/application.properties (또는 application.yml)
replicate.api.token=${API_TOKEN}
  1. 워크플로우 빌드 조건 보강
   - name: Build with Gradle
-    if: steps.create_pr.outputs.pull-request-number == ''
+    if: steps.create_pr.outputs.pull-request-number == '' && (github.event_name == 'push' || !github.event.pull_request.head.repo.fork)
     env:
       API_TOKEN: ${{ secrets.API_TOKEN }}
  1. (선택) 시크릿 존재 검증 스텝 추가
- name: Validate required secrets
  if: steps.create_pr.outputs.pull-request-number == '' && (github.event_name == 'push' || !github.event.pull_request.head.repo.fork)
  run: |
    if [ -z "${API_TOKEN:-}" ]; then
      echo "::error::Missing required secret: API_TOKEN"
      exit 1
    fi
  env:
    API_TOKEN: ${{ secrets.API_TOKEN }}

위 내용을 반영해 시크릿 키가 정상 등록되어 있고, 애플리케이션이 ${API_TOKEN}을 통해 올바르게 토큰을 주입받는지 확인해 주세요.

src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java (1)

117-139: uploadPictureForDailyMission 테스트에 save 호출 검증 추가 필요

UserDailyMissionService.uploadPictureForDailyMission 메서드 내부에서

userDailyMissionRepository.save(userDailyMission);

를 명시적으로 호출하고 있으므로, 테스트에도 해당 save 호출을 검증해야 합니다.

  • 대상 파일:
    src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java
  • 대상 메서드: uploadPictureForDailyMission 성공 시 검증 (라인 117–139)

추가할 검증 예시:

     verify(s3Uploader, times(1)).upload(mockFile, "mission-images");
     verify(userDailyMissionRepository, times(1)).findById(userDailyMissionId);
+    verify(userDailyMissionRepository, times(1)).save(mission);
♻️ Duplicate comments (3)
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/repository/UserDailyMissionRepository.java (1)

7-9: 이전 리뷰 코멘트와 동일한 문제가 해결되었습니다.

엔티티의 @ManyToOne 관계를 고려하여 findAllByUser_Id 메서드명을 올바르게 사용했습니다. JPA가 중첩 프로퍼티 경로를 인식할 수 있습니다.

src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java (1)

34-36: 이전 리뷰 코멘트에서 지적된 cascade 문제가 해결되었습니다.

@ManyToOne 관계에서 cascade = CascadeType.ALL이 제거되어 부모 엔티티의 의도치 않은 삭제나 변경을 방지할 수 있습니다.

src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (1)

112-123: 중복된 헬퍼 메서드가 QuizService와 동일합니다.

앞서 QuizService에서 언급한 것과 동일한 중복 코드입니다. 공통 유틸리티로 추출을 고려해주세요.

🧹 Nitpick comments (21)
src/main/java/com/example/cp_main_be/domain/social/avatarpost/domain/AvatarPost.java (1)

30-32: likeCount 컬럼 제약 보강 제안(nullable=false, 하한 보장, 동시성 고려)

  • JPA 필드 타입이 primitive int인 만큼 DB 컬럼도 NOT NULL로 맞추는 것이 안전합니다.
  • 카운터는 음수로 내려가지 않도록 도메인에서 방어하고 있으나, DB 제약/마이그레이션으로 일관성 보장하면 더 견고합니다.
  • 좋아요/취소가 동시 다발적으로 발생할 수 있다면 낙관적 락(@Version) 또는 DB 레벨 증가 쿼리로 경쟁 상태를 줄이는 것을 고려해 주세요.

다음과 같이 NOT NULL을 우선 반영하는 것을 권장합니다:

-  @Column(name = "like_count")
+  @Column(name = "like_count", nullable = false)
   @Builder.Default
   private int likeCount = 0;

추가적으로(선택):

  • Hibernate 사용 시 엔티티 레벨 체크 제약으로 하한을 보장할 수 있습니다(마이그레이션으로 DDL에 반영하는 편을 더 권장).

예시(엔티티 외부 변경 코드, 참고용):

import org.hibernate.annotations.Check;

@Check(constraints = "like_count >= 0")
@Entity
public class AvatarPost { ... }
  • 동시성: 서비스 레이어에서 좋아요 처리에 Optimistic Lock(@Version long version 필드) 또는 “update ... set like_count = like_count + 1 where id = ?” 형태의 원자적 업데이트 사용을 검토해 주세요.
src/main/java/com/example/cp_main_be/domain/garden/plant_masters/domain/PlantMasters.java (2)

35-37: @Builder.Default 추가는 현재 int=0에서 효과 미미; DB null 방지 위해 NOT NULL 권장

  • int 필드는 Lombok 빌더 미설정 시 기본값 0이므로, 현재 초기값(0) 환경에서는 @Builder.Default의 실효가 크지 않습니다. 향후 0이 아닌 초기값을 의도할 때는 유지 가치가 있습니다.
  • 더 중요한 것은 DB 측 null 허용 여부입니다. 컬럼이 NULL을 허용하면, 기존 데이터에 null이 들어갈 수 있고, 이를 primitive int로 로드하는 시점에 문제를 야기할 수 있습니다. JPA 매핑에서 nullable=false로 명시해 두는 것을 권장합니다. (마이그레이션에서 NOT NULL 및 기본값 0을 함께 반영하는 것을 추천)

적용 제안(컬럼 제약 강화):

-  @Column(name = "growth_stages")
+  @Column(name = "growth_stages", nullable = false)
   @Builder.Default
   private int growthStages = 0;

선택사항(군더더기 제거): @Builder.Default가 굳이 필요 없다면 삭제해 간결하게 유지할 수도 있습니다.

-  @Builder.Default
-  private int growthStages = 0;
+  private int growthStages = 0;

39-41: unlockLevel도 동일하게 컬럼 제약 강화 권장

  • 현재 @Builder.Default는 int=0 기본값과 사실상 동일 동작입니다. 유지해도 무방하나, 가독성/간결성을 위해 제거 선택지도 있습니다.
  • DB 측 일관성을 위해 nullable=false를 명시하는 것을 권장합니다. (마이그레이션에서 NOT NULL + 기본값 0 반영 필요)

적용 제안(컬럼 제약 강화):

-  @Column(name = "unlock_level")
+  @Column(name = "unlock_level", nullable = false)
   @Builder.Default
   private int unlockLevel = 0;

선택사항(간결화):

-  @Builder.Default
-  private int unlockLevel = 0;
+  private int unlockLevel = 0;
.github/workflows/ci-cd.yml (3)

200-201: 서드파티 액션 버전 고정 권장(보안/재현성)

appleboy/ssh-action@master는 가변 참조로 공급망 리스크가 있습니다. 태그 또는 커밋 SHA로 핀닝하세요.

-      - name: Deploy to EC2 instance
-        uses: appleboy/ssh-action@master
+      - name: Deploy to EC2 instance
+        uses: appleboy/ssh-action@v0.1.10

가능하면 공개 릴리스 노트를 확인하여 최신 안정 버전으로 핀닝해 주세요.


177-179: 불필요한 태그 푸시 방지

docker push --all-tags는 로컬에 존재하는 동일 리포지토리의 모든 태그를 푸시합니다. 의도치 않은 태그 푸시를 피하려면 필요한 태그만 명시적으로 푸시하세요.

-          docker push --all-tags $ECR_REGISTRY/$ECR_REPOSITORY
+          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
+          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

218-236: 민감한 키 파일 취급 강화(권한/수명 관리)

EC2에 서비스 계정 키를 영구 파일로 남깁니다. 최소권한과 수명 관리를 위해 권한 제한 및 정리 절차를 추가하는 것이 안전합니다.

-            echo "${{ secrets.FIREBASE_KEY_JSON }}" > /home/${{ secrets.EC2_USER }}/serviceAccountKey.json
+            echo "${{ secrets.FIREBASE_KEY_JSON }}" > /home/${{ secrets.EC2_USER }}/serviceAccountKey.json
+            chmod 600 /home/${{ secrets.EC2_USER }}/serviceAccountKey.json
@@
               -v /home/${{ secrets.EC2_USER }}/serviceAccountKey.json:/app/serviceAccountKey.json \
               ${{ steps.login-ecr.outputs.registry }}/cp_main_be:latest
+            
+            # (선택) 컨테이너 기동 후 호스트의 키 파일 삭제 또는 주기적 로테이션 고려
+            # rm -f /home/${{ secrets.EC2_USER }}/serviceAccountKey.json

삭제 여부는 운영 정책에 맞춰 결정하되, 최소한 파일 권한 제한은 반영하시길 권장합니다.

src/main/java/com/example/cp_main_be/domain/social/diary/domain/Diary.java (5)

32-35: DiaryImage 1:1 매핑 시 orphan row 남을 수 있음 — orphanRemoval 권장

updateImage()에서 이미지 교체 시 기존 이미지가 더 이상 참조되지 않아도 DB에 남을 수 있습니다. 고아 레코드 방지를 위해 orphanRemoval = true를 권장합니다.

적용 diff:

-  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
+  @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
   @JoinColumn(name = "diary_image_id")
   private DiaryImage diaryImage;

36-39: @Builder.Default(isPublic) 추가는 적절 — not-null 제약으로 불변식 강화 권장

Builder 사용 시 기본값 보존이 보장되어 좋습니다. 추가로 DB/스키마 차원에서도 null을 막아두는 것이 안전합니다.

적용 diff:

-  @Column(name = "is_public")
+  @Column(name = "is_public", nullable = false)
   @Builder.Default
   private boolean isPublic = true; // 기본값 true

40-44: 타임스탬프 컬럼은 not-null 권장

@PrePersist/@PreUpdate로 값이 채워지므로 스키마 수준에서도 null을 금지하는 편이 안전합니다.

적용 diff:

-  @Column(name = "created_at")
+  @Column(name = "created_at", nullable = false)
   private LocalDateTime createdAt;

-  @Column(name = "updated_at")
+  @Column(name = "updated_at", nullable = false)
   private LocalDateTime updatedAt;

12-15: JPA 엔티티에 @builder 적용 시 필수 필드 보장 방안 제안

현재 클래스 레벨 @builder는 id/타임스탬프/연관관계까지 모두 세터처럼 열립니다. 생성 안정성을 위해 “생성에 필요한 최소 필드(title, content, user 등)”만 받는 생성자/정적 팩토리에 @builder를 거는 패턴을 권장합니다.

예시:

@Builder
private Diary(String title, String content, User user, boolean isPublic, DiaryImage diaryImage) {
  this.title = title;
  this.content = content;
  this.user = user;
  this.isPublic = isPublic;
  this.diaryImage = diaryImage;
}

필요 시 현재 클래스 레벨 @builder는 제거하고 위 생성자에만 @builder를 유지하세요.


46-49: 낙관적 락킹(@Version) 도입으로 lost update 방지하기

현재 프로젝트 전역에 @Version 어노테이션이 없어 엔티티 수준 증가 로직(increaseLikeCount()/decreaseLikeCount())에서 동시성 충돌 시 “lost update”가 발생할 수 있습니다. 아래 제안 사항을 검토해주세요.

  • 적용 대상
    • src/main/java/com/example/cp_main_be/domain/social/diary/domain/Diary.java
  • 제안 diff
       @Column(name = "like_count")
       @Builder.Default
       private int likeCount = 0;
    +
    +  @Version
    +  private Long version;
  • 추가 검토 사항
    1. DB 마이그레이션: version 컬럼(예: BIGINT) 추가
    2. OptimisticLockException 발생 시 예외 처리 전략
    3. 대량 동시성 업데이트나 성능 이슈가 우려될 경우, Repository 레벨 DML 업데이트(UPDATE diary SET like_count = like_count + 1 WHERE id = :id) 방식 검토
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (2)

112-123: 중복된 헬퍼 메서드가 UserDailyMissionService와 동일합니다.

getUserDailyMissiongetQuizByMissionId 메서드가 UserDailyMissionService의 동일한 메서드와 중복됩니다. 공통 유틸리티 클래스나 추상 서비스 클래스로 추출하여 코드 중복을 제거하는 것을 고려해주세요.

+// 공통 유틸리티 클래스 생성 또는 추상 서비스 클래스 사용
+@Component
+public class MissionServiceHelper {
+  
+  private final UserDailyMissionRepository userDailyMissionRepository;
+  private final QuizRepository quizRepository;
+  
+  public UserDailyMission getUserDailyMission(Long userDailyMissionId) {
+    return userDailyMissionRepository
+        .findById(userDailyMissionId)
+        .orElseThrow(() -> new RuntimeException("해당 ID를 갖는 미션이 존재하지 않습니다."));
+  }
+  
+  public Quiz getQuizByMissionId(Long missionId) {
+    return quizRepository
+        .findByDailyMissionMaster_Id(missionId)
+        .orElseThrow(() -> new RuntimeException("퀴즈가 존재하지 않습니다."));
+  }
+}

72-84: 정답 여부 계산 로직을 최적화할 수 있습니다.

현재 정답 여부는 스트림으로, 답안 번호는 반복문으로 따로 계산하고 있습니다. 하나의 반복으로 통합하면 성능을 개선할 수 있습니다.

-    // 정답 여부 계산 (엔티티에 저장되지 않으므로 다시 계산)
-    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;
-      }
-    }
+    // 정답 여부와 답안 번호를 한 번에 계산
+    boolean isCorrect = false;
+    Integer userSelectedAnswerNumber = null;
+    for (QuizOptions option : quizOptions) {
+      if (option.getId().equals(userSelectedOptionId)) {
+        isCorrect = option.isCorrect();
+        userSelectedAnswerNumber = option.getOptionOrder();
+        break;
+      }
+    }
src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java (3)

7-14: 패키지 구조/네임스페이스는 최신화되었지만, 테스트 클래스 패키지 경로와 불일치합니다

현재 테스트 파일 패키지명이 com.example.cp_main_be.domain.daily_mission_masters.service로 남아있고, 대상 서비스는 com.example.cp_main_be.domain.mission.user_daily_mission.service.UserDailyMissionService를 사용합니다. 새 도메인 구조에 맞춰 테스트 클래스/파일의 패키지를 com.example.cp_main_be.domain.mission.user_daily_mission.service로 이동/정리하거나, 이미 존재하는 UserDailyMissionServiceTest로 통합하는 것을 권장합니다. 중복 테스트 유지보수 비용을 줄일 수 있습니다.


77-86: 목록 순서에 의존하는 단정 최소화

현재 인덱스 기반 검증은 서비스 내부 정렬 변경에 매우 취약합니다. 순서 독립적인 단정으로 변경하면 회귀에 강해집니다.

다음과 같이 변경을 제안드립니다:

-    // DTO의 내부 클래스 MissionSummaryDTO의 필드명에 맞춰 검증
-    DailyMissionResponseDTO.MissionSummaryDTO firstMission = responseDTO.getTodayMissions().get(0);
-    assertThat(firstMission.getMissionId()).isEqualTo(1L);
-    assertThat(firstMission.getMissionTitle()).isEqualTo("걷기 30분");
-    assertThat(firstMission.getMissionDescription()).isEqualTo("공원에서 30분 이상 걷기");
-
-    DailyMissionResponseDTO.MissionSummaryDTO secondMission = responseDTO.getTodayMissions().get(1);
-    assertThat(secondMission.getMissionId()).isEqualTo(2L);
-    assertThat(secondMission.getMissionTitle()).isEqualTo("물 2L 마시기");
+    assertThat(responseDTO.getTodayMissions())
+        .extracting(
+            DailyMissionResponseDTO.MissionSummaryDTO::getMissionId,
+            DailyMissionResponseDTO.MissionSummaryDTO::getMissionTitle,
+            DailyMissionResponseDTO.MissionSummaryDTO::getMissionDescription)
+        .containsExactlyInAnyOrder(
+            tuple(1L, "걷기 30분", "공원에서 30분 이상 걷기"),
+            tuple(2L, "물 2L 마시기", "하루 동안 총 2L의 물 마시기"));

아래 정적 임포트를 추가해야 합니다(파일 상단):

import static org.assertj.core.api.Assertions.tuple;

30-31: 불필요한 Mock/임포트 정리 제안

DailyMissionMasterRepository는 본 테스트 시나리오에서 직접 사용되지 않습니다. 유지보수를 위해 제거를 권장합니다. 서비스가 해당 빈을 필요로 하지 않는다면 과도한 픽스처입니다.

다음 변경을 제안합니다:

- import com.example.cp_main_be.domain.mission.daily_mission_master.domain.repository.DailyMissionMasterRepository;
-  @Mock private DailyMissionMasterRepository dailyMissionMasterRepository;

Also applies to: 9-9

src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java (5)

46-48: 주석 내 구(舊) 명칭 사용 수정

주석에 DailyMissionMastersRepository, UserDailyMissions 등 옛 이름이 남아있습니다. 리팩터링 일관성을 위해 최신 명칭으로 갱신하세요.

다음과 같이 주석만 정정하는 것을 제안합니다:

-  // DailyMissionMastersRepository는 getDailyMissions 메소드에서만 사용되지만,
-  // 해당 메소드는 UserDailyMissions를 통해 DailyMissionMasters를 가져오므로 직접적인 Mocking은 필요하지 않습니다.
+  // DailyMissionMasterRepository는 getDailyMissions 메소드에서만 사용되지만,
+  // 해당 메소드는 UserDailyMission을 통해 DailyMissionMaster를 가져오므로 직접적인 Mocking은 필요하지 않습니다.

54-76: 중복 시나리오 테스트 통합 제안

동일한 getDailyMissions 성공 시나리오가 DailyMissionServiceTest에도 존재합니다. 한 곳으로 통합하고, 여기 테스트에선 보완 포인트(예: missionId/description 포함 검증, 정렬 독립성)만 유지하는 편이 장기 유지보수에 유리합니다.


80-92: 예외 타입 구체화 고려

현재 RuntimeException으로 단정하고 메시지를 검증하고 있습니다. 도메인 예외(예: NotFoundException 등)가 있다면 보다 구체적인 타입으로 검증하는 것이 의도 전달과 회귀 방지에 도움이 됩니다.

원한다면 커스텀 예외 타입 도입 및 테스트 갱신까지 제안 드릴 수 있습니다.

Also applies to: 94-112


180-185: 퀴즈 옵션 정합성 엣지 케이스 추가 테스트 제안

옵션에 정답이 없는 경우(모두 false), 정답이 2개 이상인 경우, 요청 answerNumber가 범위를 벗어난 경우 등에 대한 동작이 정의되어야 합니다. 현재는 정답 1개/정답 존재 가정만 검증합니다.

원하시면 위 3가지 케이스에 대한 테스트 메서드를 추가로 작성해 드리겠습니다.


163-166: 테스트 메서드명 오탈자 수정 (summit → submit)

테스트 메서드명에 오타(summit → submit)가 있어 혼동을 유발합니다. 서비스 메서드명은 별도 PR에서 리네이밍하고, 이 PR에서는 테스트 메서드명만 우선 수정해주세요.

  • 파일: src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java
    • summitAnswer_Correct → submitAnswer_Correct (라인 163–166)
    • summitAnswer_Incorrect → submitAnswer_Incorrect (라인 203–206)
    • summitAnswer_Fail_QuizNotFound → submitAnswer_Fail_QuizNotFound (라인 239–246)
--- a/src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java
+++ b/src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java
@@ -163,7 +163,7 @@
-  void summitAnswer_Correct() {
+  void submitAnswer_Correct() {
@@ -203,7 +203,7 @@
-  void summitAnswer_Incorrect() {
+  void submitAnswer_Incorrect() {
@@ -239,7 +239,7 @@
-  void summitAnswer_Fail_QuizNotFound() {
+  void submitAnswer_Fail_QuizNotFound() {
📜 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 ac9aa6b and d155e55.

📒 Files selected for processing (16)
  • .github/workflows/ci-cd.yml (1 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/avatar/domain/Avatar.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/garden/plant_masters/domain/PlantMasters.java (2 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/dto/QuizResponseDTO.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java (1 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/repository/UserDailyMissionRepository.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/avatarpost/domain/AvatarPost.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/diary/domain/Diary.java (3 hunks)
  • src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java (1 hunks)
  • src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java (5 hunks)
  • src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/QuizResponseDTO.java
  • src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java
  • src/main/java/com/example/cp_main_be/domain/avatar/domain/Avatar.java
  • src/main/java/com/example/cp_main_be/domain/admin/presentation/AdminController.java
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/presentation/UserDailyMissionController.java
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/repository/UserDailyMissionRepository.java (1)
src/main/java/com/example/cp_main_be/domain/user_daily_missions/repository/UserDailyMissionRepository.java (1)
  • UserDailyMissionRepository (9-11)
src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/CompletedQuizResponseDTO.java (1)
src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/QuizResponseDTO.java (2)
  • Getter (8-23)
  • Getter (17-22)
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (1)
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (1)
  • Service (22-124)
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (2)
src/main/java/com/example/cp_main_be/domain/mission/quiz/service/QuizService.java (1)
  • Service (19-124)
src/main/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionService.java (1)
  • Service (22-81)
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java (4)
src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java (1)
  • Entity (16-108)
src/main/java/com/example/cp_main_be/domain/mission/daily_mission_master/domain/DailyMissionMaster.java (1)
  • Entity (11-80)
src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/CompletedQuizResponseDTO.java (2)
  • Getter (8-29)
  • Getter (20-28)
src/main/java/com/example/cp_main_be/domain/user_daily_missions/domain/UserDailyMissions.java (1)
  • Entity (12-47)
🔇 Additional comments (9)
src/main/java/com/example/cp_main_be/domain/social/avatarpost/domain/AvatarPost.java (2)

30-32: @Builder.Default 추가로 빌더 초기값 보존 문제 해결 — 잘 반영됨

Lombok @builder 사용 시 필드 초기값을 보존하려면 @Builder.Default가 필수인데, 이를 추가해 likeCount=0 초기값이 빌더에서도 유지되도록 한 점 좋습니다.


3-3: 레거시 User 경로 참조 없음 확인 — 코드 변경 승인
rg 검사 결과, 기존 com.example.cp_main_be.domain.user.domain.User import가 모두 제거되었고, 신규 경로 com.example.cp_main_be.domain.member.user.domain.User만 사용 중임을 확인했습니다.
변경사항 승인합니다.

src/main/java/com/example/cp_main_be/domain/garden/plant_masters/domain/PlantMasters.java (1)

1-1: 패키지 이동 영향 검증 완료

코드베이스 전반에서 이전 패키지(com.example.cp_main_be.domain.plant_masters.domain)에 대한 참조(import, JPQL/native 쿼리 문자열, 리플렉션 등)를 검색한 결과, 아래와 같이 어떠한 매칭도 발견되지 않았습니다.
따라서 추가 조치 없이 패키지 이동을 적용해도 무방합니다.

  • import 참조 없음
  • JPQL/네이티브 쿼리 내 FQCN 문자열 없음
  • 리플렉션 혹은 하드코딩된 패키지 경로 조각 없음
src/main/java/com/example/cp_main_be/domain/social/diary/domain/Diary.java (1)

50-53: Diary–Comment 관계 모델링 재확인 요청 (ManyToOne은 비정형 케이스)

현 설정은 “여러 Diary가 하나의 Comment를 참조”하는 구조입니다. 도메인적으로는 보통 Diary 1 : Comment N 구조(= Comment가 Diary에 ManyToOne, Diary는 OneToMany 컬렉션 보유)가 일반적입니다. 의도된 모델인지 확인 부탁드립니다. 의도와 다르다면 FK 방향 및 컬렉션 매핑으로 전환이 필요합니다.

src/main/java/com/example/cp_main_be/domain/mission/quiz/dto/CompletedQuizResponseDTO.java (1)

8-29: 완료된 퀴즈 결과 응답 DTO 구조가 잘 설계되었습니다.

Lombok 빌더 패턴과 불변 필드를 사용하여 안전한 DTO를 구현했습니다. 중첩된 CompletedQuizOptionResponseDTO를 통해 각 옵션의 상세 정보(정답 여부, 선택 여부, 순서)를 체계적으로 전달할 수 있습니다.

src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java (2)

45-54: 퀴즈 관련 추가 필드들이 적절히 설계되었습니다.

퀴즈 답안 제출과 관련된 메타데이터를 체계적으로 저장할 수 있는 필드들이 추가되었습니다. 컬럼명도 스네이크 케이스로 일관되게 명명되었습니다.


59-72: 비즈니스 로직 메서드가 잘 구현되었습니다.

미션 완료 처리와 퀴즈 답안 제출 여부 확인을 위한 편의 메서드들이 적절히 구현되어 있습니다. 특히 hasSubmittedQuizAnswer() 메서드는 서비스 레이어에서 유용하게 활용될 것입니다.

src/test/java/com/example/cp_main_be/domain/daily_mission_masters/service/DailyMissionServiceTest.java (1)

97-107: 빈 목록 시나리오 테스트 케이스 적절

빈 결과 처리에 대한 검증 경로가 잘 구성되어 있습니다. NPE 리스크 없이 todayMissions의 not-null/empty 보장까지 확인하는 점 좋습니다.

src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java (1)

11-21: 의존성/네임스페이스 정합성 양호

도메인/리포지토리/DTO 경로 최신 구조에 맞게 정리되어 있습니다. S3Uploader 주입과 퀴즈 관련 리포지토리 모킹 구성도 적절합니다.

Comment on lines 59 to 102
public Boolean summitAnswer(QuizRequestDTO request, Long userDailyMissionId) {
UserDailyMission userDailyMission =
userDailyMissionRepository
.findById(userDailyMissionId)
.orElseThrow(() -> new RuntimeException("미션을 찾을 수 없습니다."));

Quiz quiz =
quizRepository
.findByDailyMissionMaster_Id(userDailyMission.getDailyMissionMaster().getId())
.orElseThrow(() -> new RuntimeException("퀴즈가 존재하지 않습니다."));

List<QuizOptions> quizOptionsList = quizOptionsRepository.findAllByQuizId(quiz.getId());

// 사용자가 선택한 옵션 찾기
QuizOptions selectedOption = null;
boolean isCorrect = false;

for (QuizOptions quizOptions : quizOptionsList) {
if (quizOptions.getOptionOrder() == request.getAnswerNumber()) {
selectedOption = quizOptions;
isCorrect = quizOptions.isCorrect();
break;
}
}

if (selectedOption == null) {
throw new RuntimeException("선택한 답안이 유효하지 않습니다.");
}

// UserDailyMission에 답안 정보 저장 (엔티티 필드에 맞춰서)
userDailyMission.setSelectedOptionId(selectedOption.getId());

// 정답이면 미션 완료 처리
if (isCorrect) {
userDailyMission.setCompleted(true); // isCompleted -> setCompleted
userDailyMission.setCompletedAt(LocalDateTime.now()); // 완료 시간 설정
// 점수 부여 로직 (필요에 따라)
userDailyMission.setScore(10L); // 예시 점수
}

userDailyMissionRepository.save(userDailyMission);

return isCorrect;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

퀴즈 답안 제출 로직에서 중복 저장 및 데이터 불일치 가능성이 있습니다.

현재 summitAnswer 메서드에서는 selectedOptionId만 저장하고 있지만, 엔티티에는 selectedAnswerNumber, isQuizCorrect, quizAnsweredAt 필드도 있습니다. 이로 인해 데이터 불일치가 발생할 수 있고, QuizService.getCompletedQuizResult에서 역계산이 필요해집니다.

    // UserDailyMission에 답안 정보 저장 (엔티티 필드에 맞춰서)
    userDailyMission.setSelectedOptionId(selectedOption.getId());
+   userDailyMission.setSelectedAnswerNumber(selectedOption.getOptionOrder());
+   userDailyMission.setIsQuizCorrect(isCorrect);
+   userDailyMission.setQuizAnsweredAt(LocalDateTime.now());

    // 정답이면 미션 완료 처리
    if (isCorrect) {
      userDailyMission.setCompleted(true); // isCompleted -> setCompleted
      userDailyMission.setCompletedAt(LocalDateTime.now()); // 완료 시간 설정
      // 점수 부여 로직 (필요에 따라)
      userDailyMission.setScore(10L); // 예시 점수
    }
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java
around lines 59 to 102, the method currently only sets selectedOptionId and
other derived fields later, which can lead to inconsistent stored state and
require recalculation elsewhere; update the method to populate all related
UserDailyMission fields atomically before saving: set selectedAnswerNumber =
request.getAnswerNumber(), selectedOptionId = selectedOption.getId(),
isQuizCorrect = isCorrect, quizAnsweredAt = LocalDateTime.now(), and
completed/completedAt/score only when isCorrect is true (leave completed false
and quizAnsweredAt set even for incorrect answers so the attempt is recorded);
then save the entity once to userDailyMissionRepository to avoid
duplicate/incomplete persistence and ensure field types match the entity
setters.

@@ -1,8 +1,8 @@
package com.example.cp_main_be.domain.social.diary.domain;

import com.example.cp_main_be.domain.member.user.domain.User;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

User import 경로 변경: 전역 일관성 및 조인 컬럼 매핑 확인 필요

User 엔티티 패키지 이동과 import 교체는 방향성 맞습니다. 다만 다음을 꼭 확인해주세요:

  • 기존 com.example.cp_main_be.domain.user.domain.User 경로 사용 잔존 여부
  • @JoinColumn(name = "user_id")가 새 User 엔티티의 테이블/컬럼 스키마와 여전히 일치하는지 (테이블명/PK 컬럼명 변경 여부)

다음 스크립트로 레거시 import 잔존 여부와 User 정의 위치를 빠르게 점검할 수 있습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) 레거시 User import 사용 검사"
rg -n -C2 --type=java $'com\\.example\\.cp_main_be\\.domain\\.user\\.domain\\.User' || echo "OK: 레거시 경로 미검출"

echo
echo "2) 새 User 클래스 정의/패키지 확인"
fd -t f -a 'User.java' | while read -r f; do
  echo "FILE: $f"
  rg -n '^package ' "$f" | sed 's/^/  /'
done

echo
echo "3) Diary에서 user_id 조인컬럼 사용 현황"
rg -n -C2 --type=java '@JoinColumn\\(name\\s*=\\s*"user_id"' src || true

Length of output: 955


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Diary에서 @JoinColumn(name = \"user_id\") 사용 현황"
rg -n -C2 --type=java '@JoinColumn\(name\s*=\s*"user_id"\)' src/main/java/com/example/cp_main_be/domain/social/diary/domain/Diary.java || echo "No join column annotation found in Diary.java"

echo
echo "2) 새 User 엔티티의 테이블/PK 매핑 정보"
rg -n -C1 --type=java '@Entity|@Table|@Id|@Column' src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java

Length of output: 1007


@joincolumn 매핑 누락: Diary 엔티티에 명시적 조인컬럼 추가 필요

설정한 대로 User 엔티티는 테이블명 users, PK 컬럼명 user_id로 매핑되어 있으나, Diary.java에서는 @JoinColumn(name = "user_id") 어노테이션이 없습니다. JPA 기본 네이밍 규칙(user_user_id)과 실제 컬럼(user_id)이 불일치할 수 있으므로 아래를 확인·수정해주세요:

  • src/main/java/com/example/cp_main_be/domain/social/diary/domain/Diary.java
    • @ManyToOne 관계 필드에 @JoinColumn(name = "user_id") 어노테이션 추가 또는 매핑 검증
🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/domain/social/diary/domain/Diary.java
around line 3, the Diary entity's @ManyToOne mapping to User is missing an
explicit @JoinColumn, which can cause JPA to expect a column named by default
(e.g., user_user_id) instead of the actual users.user_id; add @JoinColumn(name =
"user_id") to the @ManyToOne field (or adjust the name to match your DB column)
and verify the column name and nullable/fetch attributes match the intended
schema.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java (1)

87-89: 옵션 검증이 순서에 의존적입니다 — 순서 비의존 검증으로 안정성 개선

리스트 0번 인덱스를 직접 검증하면 정렬/조회 순서 변경 시 불필요하게 깨질 수 있습니다. 텍스트 값 집합만 검증하도록 개선을 권장합니다.

제안 diff:

-    assertThat(result.getQuizOptions()).hasSize(2);
-    assertThat(result.getQuizOptions().get(0).getText()).isEqualTo("코끼리");
+    assertThat(result.getQuizOptions())
+        .extracting("text")
+        .containsExactlyInAnyOrder("코끼리", "고양이");

추가 제안(원하시면 테스트 케이스 작성 도와드릴게요):

  • 퀴즈는 존재하나 옵션이 비어있는 경우(0건) 처리/메시지 검증 케이스를 추가해 서비스 경로 커버리지를 보완.
  • 예외 메시지의 “정확히 일치” 대신 contains 사용 또는 커스텀 예외 타입 검증으로 테스트 취약성 감소.
📜 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 d155e55 and c784cd2.

📒 Files selected for processing (3)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (1 hunks)
  • src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java (7 hunks)
  • src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java (11 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java
  • src/test/java/com/example/cp_main_be/domain/user_daily_missions/service/UserDailyMissionServiceTest.java
🔇 Additional comments (2)
src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java (2)

9-18: 임포트 경로 변경(…mission.*) 반영 — LGTM

도메인 리팩터링에 맞춘 import 정리가 정확합니다. 테스트가 새 패키징 구조와 정합적으로 연결됩니다.


69-70: QuizOptions 빌더 세터(optionText) 유지 — 변경 불필요
테스트에서 사용된 QuizOptions.builder().optionText(...) 호출은 엔티티의 필드 및 Lombok 빌더 세터와 일치하며, text(...) 메서드는 QuizOptions에 존재하지 않습니다. 리뷰 코멘트는 무시해 주세요.

Likely an incorrect or invalid review comment.

@lejuho lejuho merged commit 421d5a7 into develop Aug 20, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/test/java/com/example/cp_main_be/domain/avatar/service/AvatarServiceTest.java (1)

22-24: @Injectmocks AvatarService 생성에 필요한 의존성(Mock) 누락으로 테스트 인스턴스 생성 실패 가능

AvatarService는 생성자 주입으로 AvatarRepository, UserRepository, NotificationService가 필요합니다. 현재 AvatarRepository만 @mock입니다. 나머지 두 개를 추가하지 않으면 Mockito가 생성자 주입을 만족하지 못해 테스트가 실패합니다.

적용 패치:

   @Mock private AvatarRepository avatarRepository;
+  @Mock private UserRepository userRepository;
+  @Mock private NotificationService notificationService;

   @InjectMocks private AvatarService avatarService;
src/main/java/com/example/cp_main_be/domain/content/avatar/dto/response/AvatarResponse.java (1)

3-12: Jackson 직렬화 시 필드 노출이 되지 않을 수 있습니다: 접근자(@Getter) 및 가시성 수정 필요

현재 필드는 package-private이며 게터가 없습니다. 기본 Spring/Jackson 설정에서는 게터 기반 직렬화를 사용하므로 응답 JSON이 빈 객체로 내려갈 수 있습니다. 불필요한 ArrayList 초기값도 제거하세요.

권장 패치:

 package com.example.cp_main_be.domain.content.avatar.dto.response;

 import com.example.cp_main_be.domain.content.avatar.domain.Avatar;
-import java.util.ArrayList;
 import java.util.List;
 import lombok.AllArgsConstructor;
+import lombok.Getter;

 @AllArgsConstructor
+@Getter
 public class AvatarResponse {
 
-  List<Avatar> avatars = new ArrayList<>();
+  private final List<Avatar> avatars;
 }
src/main/java/com/example/cp_main_be/domain/content/avatar/service/AvatarService.java (1)

50-64: NPE 및 비즈니스 가드 누락: 소유자 없는 아바타/자기 자신에게 보내기 방지

avatar.getUser()가 null(예: 기본 아바타)일 경우 NPE가 발생할 수 있고, 자기 자신에게 꽃가루를 보내는 것도 제한해야 할 수 있습니다. 최소 가드를 추가하세요.

권장 패치:

   public void givePollen(Long senderId, Long avatarId) {
     User sender =
         userRepository
             .findById(senderId)
             .orElseThrow(() -> new UserNotFoundException("꽃가루를 보내는 사용자를 찾을 수 없습니다."));

     Avatar avatar = findAvatarById(avatarId);
-    User receiver = avatar.getUser();
+    User receiver = avatar.getUser();
+    if (receiver == null) {
+      throw new IllegalStateException("소유자가 없는 아바타에는 꽃가루를 보낼 수 없습니다.");
+    }
+    if (receiver.getId().equals(sender.getId())) {
+      throw new IllegalStateException("자기 자신의 아바타에는 꽃가루를 보낼 수 없습니다.");
+    }

     // TODO: 꽃가루 관련 비즈니스 로직 추가 (예: 사용자 꽃가루 개수 차감, 아바타 경험치 증가 등)

     // 알림 전송
     notificationService.send(
         receiver, sender, NotificationType.POLLEN, "/garden/" + receiver.getId());
   }
♻️ Duplicate comments (1)
src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java (1)

110-114: 이전 리뷰 피드백 반영: verifyNoInteractions로 안전하게 변경됨

times(0) + null 인자 검증을 제거하고 상호작용 자체 부재를 검증하도록 개선된 점 좋습니다.

🧹 Nitpick comments (29)
.github/workflows/ci-cd.yml (9)

101-117: 포크 PR에서 Secrets 미제공으로 빌드 실패 가능성 — 조건 분기 강화 및 대체 빌드 스텝 권장

pull_request(특히 fork) 이벤트에서는 repository secrets가 주입되지 않아 Gradle 빌드 시 환경 변수 치환에 실패할 수 있습니다. 현 설정은 PR에서도 이 스텝이 실행되므로, fork PR이 들어오면 쉽게 깨질 여지가 큽니다. Push/내부 PR에만 secrets 기반 빌드를 수행하고, fork PR에는 mock값으로 빌드하는 별도 스텝을 두는 것을 권장합니다.

아래와 같이 기존 빌드 스텝의 if 조건을 강화하세요:

-      - name: Build with Gradle
-        if: steps.create_pr.outputs.pull-request-number == ''
+      - name: Build with Gradle
+        # push 또는 fork가 아닌 PR만 secrets 기반 빌드 허용
+        if: steps.create_pr.outputs.pull-request-number == '' && (github.event_name == 'push' || github.event.pull_request.head.repo.fork == false)
         env:
           # Spring Boot는 이 환경 변수(API_TOKEN)를 인식하여
           # application.properties의 ${API_TOKEN} 플레이스홀더를 치환합니다.
           R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
           R2_BUCKET: ${{ secrets.R2_BUCKET }}
           R2_ACCESS_KEY: ${{ secrets.R2_ACCESS_KEY }}
           R2_SECRET_KEY: ${{ secrets.R2_SECRET_KEY }}
           JWT_SECRET: ${{ secrets.JWT_SECRET }}
           FASTAPI_URL: ${{ secrets.FASTAPI_URL }}
           API_TOKEN: ${{ secrets.API_TOKEN }}
-        run: ./gradlew build --stacktrace --info
+        run: ./gradlew build --stacktrace --info

그리고 fork PR 전용(Secrets 없이) 빌드 스텝을 추가하세요(참고용 스니펫, 기존 범위 밖이므로 별도 추가 필요):

- name: Build with Gradle (fork PR without secrets)
  if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
  env:
    R2_ENDPOINT: http://example.invalid
    R2_BUCKET: dummy
    R2_ACCESS_KEY: dummy
    R2_SECRET_KEY: dummy
    JWT_SECRET: dummy
    FASTAPI_URL: http://localhost
    API_TOKEN: dummy
  run: ./gradlew -PskipExternalDeps=true build --stacktrace --info

원하시면 fork PR 대응용 Gradle 프로파일/플래그 설계를 도와드리겠습니다.


117-117: Gradle 플래그 소소 개선 제안: --no-daemon 추가

CI 환경에서는 데몬이 큰 이점을 주지 않으며, 프로세스 잔존 문제를 피하는 데 도움됩니다.

-        run: ./gradlew build --stacktrace --info
+        run: ./gradlew --no-daemon build --stacktrace --info

146-156: nc(netcat) 사전 설치 없으면 EC2 상태 체크가 실패할 수 있습니다

ubuntu-latest에 netcat이 항상 기본 설치되어 있지 않습니다. 사용 전에 설치하도록 보강하세요.

       - name: Check EC2 instance status
         id: check-ec2-status
         run: |
+          sudo apt-get update -y
+          sudo apt-get install -y netcat-openbsd
           if timeout 10 nc -z ${{ secrets.EC2_HOST }} 22 2>/dev/null; then
             echo "EC2 instance is reachable"
             echo "available=true" >> $GITHUB_OUTPUT
           else
             echo "EC2 instance is not reachable or stopped"
             echo "available=false" >> $GITHUB_OUTPUT
           fi
         continue-on-error: true

206-207: 서드파티 액션 버전 고정 권장: appleboy/ssh-action@master 사용 지양

@master는 서플라이 체인 리스크가 큽니다. 안정된 릴리스 태그 또는 커밋 SHA로 핀하세요.


38-55: PR 템플릿 검사기 견고성 향상: 공백 본문/템플릿 파일 부재 처리

본문이 공백 문자만 있거나 템플릿 파일 경로가 틀린 경우 현재 스텝이 예기치 않게 실패할 수 있습니다. if 조건을 단순화하고, 스크립트 내부에서 trim 및 try/catch로 처리하세요.

-      - name: Check PR Template
-        if: github.event_name == 'pull_request' && github.event.pull_request.body == ''
+      - name: Check PR Template
+        if: github.event_name == 'pull_request'
         uses: actions/github-script@v7
         with:
           script: |
-            const fs = require('fs');
-            // 템플릿 파일이 있는 경로를 정확히 지정해야 합니다.
-            // 예: .github/pr_templates/for_develop.md
-            const template = fs.readFileSync('.github/pr_templates/for_develop.md', 'utf8');
-            github.rest.issues.createComment({
-              owner: context.repo.owner,
-              repo: context.repo.repo,
-              issue_number: context.issue.number,
-              body: `🚨 **PR 본문이 비어있습니다!**\n\n아래 템플릿을 복사하여 PR 내용을 작성해주세요.\n\n---\n\n${template}`
-            });
-            core.setFailed('PR 본문을 템플릿에 맞게 작성해주세요.');
+            const fs = require('fs');
+            const body = (context.payload.pull_request?.body || '').trim();
+            if (body.length > 0) {
+              return; // 본문이 존재하면 통과
+            }
+            let template = '(템플릿 파일을 찾지 못했습니다: .github/pr_templates/for_develop.md)';
+            try {
+              template = fs.readFileSync('.github/pr_templates/for_develop.md', 'utf8');
+            } catch (e) {
+              core.warning(`PR 템플릿 파일을 읽지 못했습니다: ${e.message}`);
+            }
+            await github.rest.issues.createComment({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              issue_number: context.issue.number,
+              body: `🚨 **PR 본문이 비어있습니다!**\n\n아래 템플릿을 복사하여 PR 내용을 작성해주세요.\n\n---\n\n${template}`
+            });
+            core.setFailed('PR 본문을 템플릿에 맞게 작성해주세요.');

59-59: actions/cache 최신 메이저로 업데이트 권장

actions/cache@v4가 일반적으로 권장됩니다. 호환성 문제 없으면 올리는 것을 추천드립니다.

-        uses: actions/cache@v3
+        uses: actions/cache@v4

123-126: JUnit 결과 수집 경로 넓히기(멀티모듈 대응)

단일 모듈만 대상으로 설정되어 있습니다. 멀티모듈을 대비해 글롭을 확장하세요.

-          path: build/test-results/test/*.xml
+          path: '**/build/test-results/test/*.xml'

224-242: 원격 호스트에 Firebase 키 파일 직접 저장 방식 주의: base64 전달/권한 제한 권장

민감 키를 echo로 그대로 기록하면 줄바꿈/특수문자 이슈와 보안 노출 위험이 있습니다. base64 인코딩된 시크릿을 전달하고 원격에서 디코드, 최소 권한(600)으로 저장한 뒤 컨테이너 마운트를 권장합니다. 배포 후 정리도 고려하세요.

예시(시크릿을 FIREBASE_KEY_JSON_B64로 준비한 경우):

echo "${{ secrets.FIREBASE_KEY_JSON_B64 }}" | base64 -d > /home/${{ secrets.EC2_USER }}/serviceAccountKey.json
chmod 600 /home/${{ secrets.EC2_USER }}/serviceAccountKey.json

배포 완료 후 파일 삭제 정책이 필요하면 알려주세요. 스크립트에 안전한 정리 절차를 추가해 드리겠습니다.


2-2: 워크플로우 이름 정합성(선택): R2 전환 반영

이름이 “with AWS”로 고정되어 있으나 R2 도입으로 스토리지 백엔드가 혼용됩니다. 혼선 방지를 위해 “with AWS & R2” 등으로 명확히 하는 것을 고려해 주세요.

src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java (3)

82-88: 옵션 순서 의존성 제거 — 텍스트 값만 집합으로 검증 권장

현재 Line 87은 첫 번째 옵션이 항상 "코끼리"라고 가정합니다. 저장소/서비스에서 정렬이 바뀌면 취약해집니다. 텍스트만 추출해 순서 무관 검증으로 바꿔 주세요.

적용 diff:

-    assertThat(result.getQuizOptions().get(0).getText()).isEqualTo("코끼리");
+    assertThat(result.getQuizOptions())
+        .extracting("text")
+        .containsExactlyInAnyOrder("코끼리", "고양이");

136-145: 예외 단언을 더 견고하게 만들기 위한 제안(선택)

문자열 전체 일치 메시지 단언은 i18n/문구 변경에 취약합니다. contains/startsWith로 완화하거나, 도메인 예외 타입(예: NotFoundException 등)이 있다면 타입을 명시적으로 검증하는 방식을 고려해 주세요.

원하시면 AssertJ 스타일로 변환한 단언 코드(contains 기반) 패치를 제안드리겠습니다.


66-70: 경계 케이스 테스트 추가 제안: '퀴즈는 있으나 옵션이 없는 경우'

서비스 기대 동작(예외 vs 빈 리스트 반환)에 따라 별도 테스트가 필요합니다. 현재 테스트는 정상/미존재 두 경로만 다룹니다.

아래 스켈레톤 중 기대 동작을 알려주시면 완성해 드리겠습니다:

@Test
@DisplayName("퀴즈 조회 실패(또는 특수 처리) - 퀴즈는 있으나 옵션 없음")
void getQuiz_QuizOptionsEmpty() {
  // Given
  Long userDailyMissionId = 1L;
  Long dailyMissionMasterId = 101L;
  Long quizId = 201L;

  DailyMissionMaster dailyMissionMaster = DailyMissionMaster.builder()
      .id(dailyMissionMasterId).build();
  UserDailyMission userDailyMission = UserDailyMission.builder()
      .id(userDailyMissionId).dailyMissionMaster(dailyMissionMaster).build();
  Quiz quiz = Quiz.builder()
      .id(quizId).dailyMissionMaster(dailyMissionMaster)
      .quizType(QuizType.MULTI_CHOICE).quizQuestion("Q").build();

  given(userDailyMissionRepository.findById(userDailyMissionId)).willReturn(Optional.of(userDailyMission));
  given(quizRepository.findByDailyMissionMaster_Id(dailyMissionMasterId)).willReturn(Optional.of(quiz));
  given(quizOptionsRepository.findAllByQuizId(quizId)).willReturn(List.of());

  // When & Then
  // TODO: 기대 동작을 결정해 주세요.
  // 1) 예외를 던지는 경우:
  // assertThrows(ExpectedException.class, () -> quizService.getQuiz(userDailyMissionId));
  // 2) 빈 리스트를 허용하는 경우:
  // QuizResponseDTO result = quizService.getQuiz(userDailyMissionId);
  // assertThat(result.getQuizOptions()).isEmpty();
  // verify(...) 호출 검증 등
}
src/main/java/com/example/cp_main_be/domain/content/image/DailyMissionImage.java (1)

17-18: imageUrl 제약 추가 권장 (nullable/length 지정)

DB 무결성과 스키마 명시성을 위해 컬럼 제약을 추가하는 것을 권장합니다.

-  @Column(name = "image_url")
+  @Column(name = "image_url", nullable = false, length = 1024)
   private String imageUrl;

추가 고려(선택): 엔티티 변경 최소화를 위해 @Setter 제거 또는 접근제어 축소(예: @Setter(AccessLevel.PROTECTED)).

src/test/java/com/example/cp_main_be/domain/image/service/ImageProcessingServiceTest.java (1)

3-3: 서비스 패키지 경로 업데이트는 적절 — 테스트 패키지도 content 경로로 통일 권장

테스트가 기존 package 경로(com.example.cp_main_be.domain.image.service) 아래에 남아 있어 탐색성에 혼동이 올 수 있습니다. 테스트 클래스 package를 content 경로로 맞추고 파일 경로도 이동하는 것을 권장합니다.

변경 제안(파일 상단 package 선언):

package com.example.cp_main_be.domain.content.image.service;

파일 경로도 src/test/java/com/example/cp_main_be/domain/content/image/service/ 로 이동해 주세요.

src/main/java/com/example/cp_main_be/domain/content/image/presentation/ImageController.java (2)

31-33: MIME 타입 비교 시 상수 사용으로 가독성/오타 방지

문자열 리터럴 대신 MediaType 상수 사용을 권장합니다.

-    if (contentType == null
-        || (!contentType.equals("image/png") && !contentType.equals("image/jpeg"))) {
+    if (contentType == null
+        || (!contentType.equals(MediaType.IMAGE_PNG_VALUE) && !contentType.equals(MediaType.IMAGE_JPEG_VALUE))) {
       throw new CustomApiException(ErrorCode.INVALID_FILE);
     }

23-24: API 명세 명확화: produces=IMAGE_PNG_VALUE 추가 제안

응답이 항상 PNG임을 명세에 드러내면 클라이언트/문서화가 명확해집니다.

-  @PostMapping(value = "/register/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+  @PostMapping(
+      value = "/register/upload",
+      consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
+      produces = MediaType.IMAGE_PNG_VALUE)
   public ResponseEntity<byte[]> generateAvatar(@RequestParam("image") MultipartFile imageFile) {
src/test/java/com/example/cp_main_be/domain/avatar/service/AvatarServiceTest.java (1)

1-1: 테스트 패키지 네임스페이스가 리팩터링된 경로와 불일치합니다

프로덕션 코드가 content.avatar.*로 이동했으므로, 테스트도 동일한 패키지 네임스페이스로 맞추면 탐색/정렬성이 좋아집니다. 파일 이동도 함께 필요합니다.

적용 예:

-package com.example.cp_main_be.domain.avatar.service;
+package com.example.cp_main_be.domain.content.avatar.service;
src/main/java/com/example/cp_main_be/domain/content/avatar/dto/response/AvatarResponse.java (1)

11-12: 엔티티(Avatar) 직접 노출 대신 응답 DTO로 매핑하는 것을 고려하세요

엔티티 직렬화는 지연 로딩(Lazy) 및 순환 참조 문제를 유발할 수 있습니다. 가벼운 AvatarItemResponse( id, imageUrl 등 ) 리스트로 매핑하면 안정성과 독립성이 올라갑니다.

원하시면 매퍼와 DTO 초안까지 생성해드리겠습니다.

src/main/java/com/example/cp_main_be/domain/content/avatar/presentation/AvatarController.java (3)

22-27: 엔드포인트 경로 가독성 개선 제안

베이스 경로가 /api/v1/avatars 인데, 세부 경로가 /register/avatars 로 중복됩니다. 예: GET /api/v1/avatars/selectable 또는 /api/v1/avatars/register 가 더 자연스럽습니다. 변경 시 클라이언트 영향도 확인 필요합니다.

예시:

-  @GetMapping("/register/avatars")
+  @GetMapping("/selectable")
   public ResponseEntity<AvatarResponse> selectableAvatars() {
     AvatarResponse avatarResponse = new AvatarResponse(avatarService.getAllAvatar());
     return ResponseEntity.ok(avatarResponse);
   }

31-34: 인증 주체 전체 객체 주입 대신 필요한 최소 정보(id)만 주입 권장

도메인 User를 직접 주입하면 컨트롤러-보안/도메인 결합도가 올라갑니다. id만 사용하므로 @AuthenticationPrincipal(expression="id")로 최소 주입이 바람직합니다. 보안 설정에 따라 SpEL 접근 가능 여부를 확인하세요.

패치 예:

-  public ResponseEntity<ApiResponse<Void>> givePollen(
-      @AuthenticationPrincipal User user, @PathVariable Long avatarId) {
-    avatarService.givePollen(user.getId(), avatarId);
+  public ResponseEntity<ApiResponse<Void>> givePollen(
+      @AuthenticationPrincipal(expression = "id") Long userId, @PathVariable Long avatarId) {
+    avatarService.givePollen(userId, avatarId);
     return ResponseEntity.ok(ApiResponse.success(null));
   }

34-35: 성공 응답 규약 확인

본문이 없는 성공 응답은 204 No Content를 고려할 수 있습니다. 다만, 프로젝트 전역에서 ApiResponse.success(null) 컨벤션을 사용 중이라면 현재 구현 유지로 일관성을 지키세요.

src/main/java/com/example/cp_main_be/domain/content/avatar/service/AvatarService.java (3)

33-38: 예외 타입 일관성 개선: IllegalArgumentException 대신 도메인 예외 사용 권장

UserNotFoundException을 사용 중인데, 아바타는 IllegalArgumentException을 던집니다. AvatarNotFoundException(또는 공통 NotFound 예외)로 통일하면 핸들러/로깅/모니터링 일관성이 좋아집니다.

예시:

-        .orElseThrow(() -> new IllegalArgumentException("아바타를 찾을 수 없습니다."));
+        .orElseThrow(() -> new AvatarNotFoundException("아바타를 찾을 수 없습니다."));

필요 시 AvatarNotFoundException도 함께 생성해드릴 수 있습니다.


28-31: 메서드 네이밍 개선 제안: getAllAvatar → getAllAvatars

복수 반환에 대한 의미가 더 명확해집니다. 연쇄적으로 Controller/Test/호출부도 함께 변경 필요합니다.


59-64: TODO 작업 범위 정의 및 후속 이슈 트래킹 제안

꽃가루 차감/경험치 증가/쿨다운/중복 방지 등 구체 요건 정의가 필요합니다. 재현/검증 체크리스트와 연결해 단위/통합 테스트도 함께 추가하시길 권장합니다.

원하시면:

  • 도메인 서비스 설계 초안
  • 트랜잭션 경계/락 전략
  • 단위/통합 테스트 템플릿
    을 생성해드리겠습니다.
src/main/java/com/example/cp_main_be/domain/content/avatar/domain/Avatar.java (2)

23-25: 기본 아바타에서 user가 null 허용됨을 명시적으로 표현

JPA 기본값으로 optional = true이지만, 도메인 설명(기본 아바타인 경우 null)에 맞춰 의도를 명확히 하면 가독성이 좋아집니다.

-  @ManyToOne(fetch = FetchType.LAZY)
+  @ManyToOne(fetch = FetchType.LAZY, optional = true)
   @JoinColumn(name = "user_id") // 외래 키 컬럼 지정
   private User user; // 특정 유저가 생성한 아바타인 경우 해당 유저, 기본 아바타인 경우 null

28-29: boolean 필드 네이밍 및 파생 쿼리 메서드 일관성 개선 권장

검증 결과,

  • Avatar 엔티티의 boolean 필드명은 isDefaultAvatar
  • Spring Data 리포지토리에는 findByIsDefaultAvatarTrue()만 사용 중
  • 게터 isDefaultAvatar()는 내부적으로 프로퍼티명 defaultAvatar로 해석되지만, 환경에 따라 findByIsDefaultAvatarTrue가 올바르게 매핑되지 않아 PropertyReferenceException을 일으킬 위험이 있습니다.

단기 조치 (권장):

  • 리포지토리 메서드를 findByDefaultAvatarTrue() (및 findByDefaultAvatarFalse())로 변경
  • 서비스 호출부도 동일하게 업데이트

중기 조치 (선택):

  • 엔티티 필드명을 defaultAvatar로 리팩토링 (DB 컬럼명 is_default_avatar는 유지)

수정 대상 위치

  • src/main/java/com/example/cp_main_be/domain/content/avatar/domain/repository/AvatarRepository.java
    - List<Avatar> findByIsDefaultAvatarTrue();
    + List<Avatar> findByDefaultAvatarTrue();
  • src/main/java/com/example/cp_main_be/domain/content/avatar/service/AvatarService.java
    - return avatarRepository.findByIsDefaultAvatarTrue();
    + return avatarRepository.findByDefaultAvatarTrue();

위 조치는 파생 쿼리의 안정성을 높이기 위한 optional 리팩토링입니다.

src/main/java/com/example/cp_main_be/domain/content/avatar/domain/repository/AvatarRepository.java (2)

9-12: 조회 성능 향상을 위한 인덱스 고려

이 레포지토리 쿼리는 user_id, is_default_avatar 컬럼을 조건으로 사용합니다. 다음 인덱스를 추가하면 조회 성능이 개선됩니다.

  • 단일 인덱스: (user_id), (is_default_avatar)
  • 또는 복합 인덱스: (user_id, is_default_avatar)

마이그레이션 도구(Flyway/Liquibase)로 적용하는 것을 권장합니다.


9-12: 파생 쿼리 메서드 네이밍 정리 및 호출부 함께 수정 권장

엔티티 필드 isDefaultAvatar는 JavaBean 규약에 따라 내부적으로 defaultAvatar 프로퍼티로 간주됩니다. 따라서 아래와 같이 Repository 메서드명과 이를 호출하는 Service 코드를 일관되게 변경하는 것이 안정적입니다.

수정 대상:

  • src/main/java/com/example/cp_main_be/domain/content/avatar/domain/repository/AvatarRepository.java
  • src/main/java/com/example/cp_main_be/domain/content/avatar/service/AvatarService.java (42행)
--- a/src/main/java/com/example/cp_main_be/domain/content/avatar/domain/repository/AvatarRepository.java
@@
-  List<Avatar> findByIsDefaultAvatarTrue();  // 기본 아바타 조회
+  List<Avatar> findByDefaultAvatarTrue();    // 기본 아바타 조회

-  List<Avatar> findByUserIdAndIsDefaultAvatarFalse(Long userId); // 특정 유저가 소유한 아바타 조회
+  List<Avatar> findByUserIdAndDefaultAvatarFalse(Long userId);   // 특정 유저가 소유한 아바타 조회
--- a/src/main/java/com/example/cp_main_be/domain/content/avatar/service/AvatarService.java
@@ public class AvatarService {
-    return avatarRepository.findByIsDefaultAvatarTrue();
+    return avatarRepository.findByDefaultAvatarTrue();

이후 findByIsDefaultAvatarFalse 호출부가 있을 경우 동일하게 findByDefaultAvatarFalse로 변경해 주세요.

src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java (1)

103-105: addExperience 시그니처를 long으로 맞추고, 음수/널 가드 추가 권장

필드가 Long으로 변경되었으므로 계산형 메서드도 일관되게 long을 사용하고, 음수/널을 방어하면 안전합니다.

-  public void addExperience(int amount) {
-    this.experience += amount;
+  public void addExperience(long amount) {
+    if (amount <= 0) return; // 필요 시 IllegalArgumentException 등으로 변경
+    this.experience = (this.experience == null ? 0L : this.experience) + amount;
📜 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 96ca330 and 4a8080d.

📒 Files selected for processing (19)
  • .github/workflows/ci-cd.yml (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/avatar/dto/request/AvatarRequest.java (0 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/avatar/domain/Avatar.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/avatar/domain/repository/AvatarRepository.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/avatar/dto/request/AvatarRequest.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/avatar/dto/response/AvatarResponse.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/avatar/presentation/AvatarController.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/avatar/service/AvatarService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/image/DailyMissionImage.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/image/ImageUploader.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/image/presentation/ImageController.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/content/image/service/ImageProcessingService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/global/infra/S3Uploader.java (2 hunks)
  • src/test/java/com/example/cp_main_be/domain/avatar/service/AvatarServiceTest.java (1 hunks)
  • src/test/java/com/example/cp_main_be/domain/image/service/ImageProcessingServiceTest.java (2 hunks)
  • src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java (7 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/com/example/cp_main_be/domain/avatar/dto/request/AvatarRequest.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/com/example/cp_main_be/domain/content/image/service/ImageProcessingService.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/domain/UserDailyMission.java
  • src/main/java/com/example/cp_main_be/global/infra/S3Uploader.java
  • src/main/java/com/example/cp_main_be/domain/mission/user_daily_mission/service/UserDailyMissionService.java
🔇 Additional comments (12)
.github/workflows/ci-cd.yml (1)

106-113: 환경 변수 참조 확인 완료

CI/CD 워크플로우에서 주입된 모든 환경 변수(R2_*, JWT_SECRET, FASTAPI_URL, API_TOKEN)가 애플리케이션 및 테스트 설정 파일에서 정상적으로 참조되고 있어 누락된 사용처는 없습니다.

• src/main/resources/application.properties
– cloudflare.r2.endpoint=${R2_ENDPOINT}
– cloudflare.r2.bucket=${R2_BUCKET}
– cloudflare.r2.access-key=${R2_ACCESS_KEY}
– cloudflare.r2.secret-key=${R2_SECRET_KEY}
– fastapi.server.url=${FASTAPI_URL}
– jwt.secret=${JWT_SECRET}

• src/test/resources/application.properties
– cloudflare.r2.endpoint: ${R2_ENDPOINT}
– cloudflare.r2.bucket: ${R2_BUCKET}
– cloudflare.r2.access-key: ${R2_ACCESS_KEY}
– cloudflare.r2.secret-key: ${R2_SECRET_KEY}
– replicate.api.token: ${API_TOKEN}
– jwt.secret: ${JWT_SECRET}

src/test/java/com/example/cp_main_be/domain/quiz/service/QuizServiceTest.java (4)

6-17: 도메인 리팩터링 반영 및 Mockito 정적 임포트 OK

새 패키지 경로와 타입으로의 마이그레이션이 테스트에 정확히 반영되었습니다. Mockito 정적 임포트 구성도 일관됩니다.


48-56: 픽스처 구성 및 연관관계 설정 적절

DailyMissionMaster → UserDailyMission → Quiz로 이어지는 연관관계와 엔티티 빌더 사용이 자연스럽고 명확합니다. 옵션 더미 데이터 구성도 서비스 매핑 검증에 충분합니다.

Also applies to: 61-65, 66-70


72-77: 리포지토리 메서드명 변경 반영 OK

findByDailyMissionMaster_Id로의 변경이 stubbing/verify에 잘 반영되었습니다. 성공 경로에서 호출 횟수 검증도 타당합니다.

Also applies to: 91-93


123-134: 퀴즈 없음 경로: 플로우와 상호작용 검증 타당

미션 조회 성공 → 퀴즈 미존재 → 옵션 저장소 미호출로 이어지는 흐름이 정확히 검증됩니다. 메시지 단언도 구현과 일치합니다.

Also applies to: 146-149

src/main/java/com/example/cp_main_be/domain/content/image/DailyMissionImage.java (1)

1-1: 패키지 경로 이동 적절

도메인 재구성과 일관됩니다. JPA 스캔 범위가 com.example.cp_main_be 기준이면 런타임 영향은 없습니다.

src/main/java/com/example/cp_main_be/domain/content/image/ImageUploader.java (1)

1-1: 패키지 경로 변경 검증 완료
이전 패키지(com.example.cp_main_be.domain.image)로의 임포트 및 직접 참조가 모두 제거되었습니다. 추가 변경 없이 그대로 머지하셔도 좋습니다.

src/main/java/com/example/cp_main_be/domain/content/image/presentation/ImageController.java (1)

1-3: 패키지/임포트 경로 변경 LGTM

도메인 재구성과 일관되며, 동작 변화는 없습니다.

src/main/java/com/example/cp_main_be/domain/content/avatar/domain/Avatar.java (1)

1-3: 패키지/의존성 이동 적합

콘텐츠 도메인 하위로의 이동과 User import 경로 변경이 리팩터링 방향(회원/콘텐츠 분리)에 부합합니다. 컴포넌트 스캔 범위도 루트 패키지 하위이므로 빈 탐색에 문제 없을 것으로 보입니다.

src/main/java/com/example/cp_main_be/domain/content/avatar/domain/repository/AvatarRepository.java (1)

1-3: 패키지/의존성 경로 정리 적절

도메인 재구성에 따른 패키지/임포트 경로 업데이트가 일관성 있게 반영되었습니다.

src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java (2)

1-4: Garden 패키지 경로 확인 완료 – 중복된 garden.garden 구조가 의도된 패키지입니다

Garden.java의 패키지 선언(com.example.cp_main_be.domain.garden.garden.domain)이 실제 파일에도 일치하며, 해당 경로로의 import도 여러 곳에서 정상 참조되고 있습니다. 추가 수정이나 검증은 필요하지 않습니다.


49-51: DB 마이그레이션 확인 필요

JPA Entity에서 level/experience 필드를 Long으로 확장한 것은 합리적이지만, 코드베이스에서 해당 컬럼을 추가·변경하는 마이그레이션(DDL)을 찾을 수 없습니다. 다음 사항을 확인해 주세요:

  • Flyway/Liquibase 등 migration 폴더(src/main/resources/db/migration 등)에 users 테이블의 level, experience 컬럼 추가 스크립트 존재 여부
  • 실제 DB 컬럼 타입이 BIGINTLong과 일치하는지 DDL 검토
  • 기존 데이터가 NULL인 레코드 처리 방안 (예: addExperience 호출 시 NPE 방지 로직)

위 항목을 확인한 뒤, 마이그레이션이 적용되었는지 알려주세요.

@@ -0,0 +1,3 @@
package com.example.cp_main_be.domain.content.avatar.dto.request;

public class AvatarRequest {}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

빈 요청 DTO는 사용처가 없으면 제거하거나, 실제 스펙에 맞춰 필드/검증을 정의하세요

현재 Controller/Service 어디에도 사용되지 않는 것으로 보입니다. 불필요한 보일러플레이트를 남기지 않도록 사용처 확인 후 정리하는 것이 좋습니다.

사용 여부 확인 스크립트:


🏁 Script executed:

#!/bin/bash
# AvatarRequest 사용처 검색
rg -nP --type=java '\bAvatarRequest\b' -g '!**/build/**' -C2

Length of output: 435


사용되지 않는 AvatarRequest 제거 또는 구현 필요
src/main/java/com/example/cp_main_be/domain/content/avatar/dto/request/AvatarRequest.java 파일의 AvatarRequest 클래스가 코드베이스 내에서 참조되지 않습니다. 불필요한 빈 요청 DTO는 아래 중 하나를 선택해 처리해주세요.

  • 실제 API 스펙에 맞춰 필드와 검증 로직을 추가
  • 사용 예정 기능이 없다면 해당 클래스를 제거

대상 위치:

  • src/main/java/com/example/cp_main_be/domain/content/avatar/dto/request/AvatarRequest.java:3

도움이 필요하시면 언제든 말씀해주세요.

🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/content/avatar/dto/request/AvatarRequest.java
around line 3, the AvatarRequest class is an unused empty DTO; either implement
it to match the API contract (add required fields, constructors/getters, and
validation annotations like @NotNull/@Size per spec) or delete the file if the
feature is not planned; if you implement it, ensure package imports and any
controller/service method signatures use the new fields and include unit tests;
if you remove it, search and clean up any stale references and run a full build
to confirm no compilation errors.

static void registerDynamicProperties(DynamicPropertyRegistry registry) {
registry.add("fastapi.server.url", () -> mockWebServer.url("/").toString());
registry.add("jwt.secret", () -> "this-is-a-dummy-secret-key-for-testing-purpose");
registry.add("r");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

컴파일 오류: DynamicPropertyRegistry.add(...) 호출이 잘못되었습니다

registry.add("r")는 시그니처에 맞지 않아 컴파일되지 않습니다. 불필요한 잔재로 보이며 제거가 필요합니다.

다음과 같이 제거해 주세요:

-    registry.add("r");

만약 의도한 속성이 있었다면 Supplier를 제공해야 합니다(예시):

registry.add("some.key", () -> "some-value");

또한 현재 테스트는 응답 검증(assert)이 없어 회귀 방지가 어렵습니다. 최소한 바이트 동등성 검증을 추가해 주세요(예시):

// then
org.junit.jupiter.api.Assertions.assertArrayEquals(
    "processed-image-from-fastapi".getBytes(),
    response,
    "FastAPI에서 받은 이미지 바이트가 기대값과 다릅니다"
);

// (선택) MockWebServer로 실제 요청이 발생했는지 확인
okhttp3.mockwebserver.RecordedRequest req = mockWebServer.takeRequest();
org.junit.jupiter.api.Assertions.assertNotNull(req);

원하시면 위 수정 사항을 반영한 커밋 패치를 생성해 드리겠습니다.

🤖 Prompt for AI Agents
In
src/test/java/com/example/cp_main_be/domain/image/service/ImageProcessingServiceTest.java
around line 42, remove the invalid call registry.add("r") (it doesn't match
DynamicPropertyRegistry.add signature) or replace it with a proper key+Supplier
form like registry.add("some.key", () -> "some-value") if a property was
intended; additionally add at least one assertion to validate the response bytes
(e.g., assertArrayEquals against the expected processed-image bytes) and
optionally verify the MockWebServer received a request (takeRequest and
assertNotNull) so the test prevents regressions.

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.

브랜치 충돌 및 코드 유실 복구

3 participants