Skip to content

97 요청 사항 반영#98

Merged
xoruddl merged 2 commits into
developfrom
97-요청-사항-반영
Aug 26, 2025

Hidden character warning

The head ref may contain hidden characters: "97-\uc694\uccad-\uc0ac\ud56d-\ubc18\uc601"
Merged

97 요청 사항 반영#98
xoruddl merged 2 commits into
developfrom
97-요청-사항-반영

Conversation

@xoruddl
Copy link
Copy Markdown
Member

@xoruddl xoruddl commented Aug 26, 2025

📝 개요
이번 PR의 핵심 내용을 한 줄로 요약해 주세요.

💻 작업 내용
이번 PR에서 작업한 내용을 상세히 설명해 주세요.

작업 내용 1
작업 내용 2
...

✅ PR 체크리스트
PR을 보내기 전에 아래 체크리스트를 확인해 주세요.

커밋 메시지는 포맷에 맞게 작성했나요?
스스로 코드를 다시 한번 검토했나요?
관련 이슈를 연결했나요?
빌드 및 테스트가 로컬에서 성공했나요?

🔗 관련 이슈
이번 PR과 관련된 이슈 번호를 기재해 주세요.

스크린샷 (선택)
UI 변경 사항이 있다면 스크린샷을 첨부해 주세요.

Summary by CodeRabbit

  • Refactor
    • 일지(다이어리) 도메인을 Social에서 Mission으로 일원화하여 참조를 정리했습니다. 기능 동작은 동일합니다.
    • DiaryImage 삭제 API 경로가 변경되었습니다: DELETE /api/v1/diaries/{diaryId}/images/{imageId}
    • DiaryResponse의 필드명이 변경되었습니다: id → diaryId. 해당 응답을 사용하는 클라이언트 키를 업데이트하세요. 그 외 스펙은 동일합니다.
  • Chores
    • 알림, 댓글, 좋아요, 피드 등 관련 모듈의 의존성 정리로 일관성 및 유지보수성을 개선했습니다.

@xoruddl xoruddl linked an issue Aug 26, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 26, 2025

Walkthrough

소셜 영역의 Diary 관련 코드가 미션 영역으로 전체 리패키징되었고, 이에 따라 도메인/리포지토리/DTO/서비스/컨트롤러와 연관 소비자 코드의 import 및 타입이 일괄 갱신되었습니다. 추가로 Comment에 Diary 연관관계가 도입되었고, DiaryImageController의 매핑 경로가 변경되었으며 일부 응답 DTO가 불변 필드로 수정되었습니다.

Changes

Cohort / File(s) Summary
Mission Diary 도메인 이전
.../domain/mission/diary/domain/Diary.java, .../domain/mission/diary/domain/repository/DiaryRepository.java, .../domain/mission/diaryimage/domain/DiaryImage.java, .../domain/mission/diaryimage/domain/DiaryImageRepository.java
패키지 social.*mission.*로 이동. 관련 import 경로 업데이트. 동작 로직 변화 없음.
Diary 요청 DTO 리패키징
.../domain/mission/diary/dto/request/CreateDiaryRequest.java, .../domain/mission/diary/dto/request/DiaryWriteRequest.java, .../domain/mission/diary/dto/request/UpdateDiaryRequest.java
패키지 경로를 social.diary.dto.requestmission.diary.dto.request로 변경.
Diary 응답 DTO 리패키징/불변화
.../domain/mission/diary/dto/response/DiaryResponse.java, .../domain/mission/diary/dto/response/DiaryFeedItemResponse.java, .../domain/mission/diary/dto/response/DiaryIdResponse.java, .../domain/mission/diary/dto/response/DiaryInfoResponse.java
패키지 경로 social.*mission.*. DiaryResponse: 필드 모두 final, iddiaryId로 변경. DiaryFeedItemResponse: 주요 필드 final로 변경. 나머지는 import 갱신.
컨트롤러/서비스 리패키징 및 매핑 수정
.../domain/mission/diary/presentation/DiaryController.java, .../domain/mission/diary/service/DiaryService.java, .../domain/mission/diaryimage/presentation/DiaryImageController.java
패키지 social.*mission.*. DiaryImageController의 base path api/v1/diariesapi/v1, delete 엔드포인트를 /diaries/{diaryId}/images/{imageId}로 변경.
소비자 계층의 Diary 참조 갱신
.../domain/reports/service/ReportService.java, .../global/listener/NotificationEventListener.java, .../domain/social/like/service/LikeService.java, .../domain/social/feed/service/FeedService.java, .../domain/social/feed/dto/response/FeedResponse.java
Diary/DiaryRepository 참조를 mission.*로 교체. ReportServiceDiaryRepository 필드 추가 및 생성자에 포함. FeedResponse.from(Diary)의 파라미터 타입이 미션 Diary로 변경.
Comment 도메인/서비스 갱신
.../domain/social/comment/domain/Comment.java, .../domain/social/comment/service/CommentService.java
CommentDiary 연관관계(@manytoone, diary_id) 추가. 서비스는 미션 DiaryRepository를 사용하여 DIARY 대상 댓글 생성 시 Diary 설정.
User 참조 정리
.../domain/member/user/domain/User.java
Diary import를 mission.*로 교체. 공용 시그니처 변화 없음.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant CS as CommentService
  participant DR as DiaryRepository (mission)
  participant AR as AvatarPostRepository
  participant R as CommentRepository

  C->>CS: createComment(request: targetType, targetId, ...)
  alt targetType == DIARY
    CS->>DR: findById(targetId)
    DR-->>CS: Diary
    CS->>R: save(Comment.withDiary(...))
    R-->>CS: Comment
  else targetType == AVATAR_POST
    CS->>AR: findById(targetId)
    AR-->>CS: AvatarPost
    CS->>R: save(Comment.withAvatarPost(...))
    R-->>CS: Comment
  end
  CS-->>C: CommentResponse
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • lejuho

Poem

깡총, 패키지 길을 바꿨어요
소셜 숲 넘어 미션 들판으로
댓글은 일기와 손을 잡고
이미지 길표지도 새로 달고
final 깃발 펄럭이며
오늘의 코드에도 봄바람, 삐약? 🐇🌿

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 97-요청-사항-반영

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Status, Documentation and Community

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

@github-actions
Copy link
Copy Markdown
Contributor

🚨 PR 본문이 비어있습니다!

아래 템플릿을 복사하여 PR 내용을 작성해주세요.


📝 개요

이번 PR의 핵심 내용을 한 줄로 요약해 주세요.


💻 작업 내용

이번 PR에서 작업한 내용을 상세히 설명해 주세요.

  • 작업 내용 1
  • 작업 내용 2
  • ...

✅ PR 체크리스트

PR을 보내기 전에 아래 체크리스트를 확인해 주세요.

  • 커밋 메시지는 포맷에 맞게 작성했나요?
  • 스스로 코드를 다시 한번 검토했나요?
  • 관련 이슈를 연결했나요?
  • 빌드 및 테스트가 로컬에서 성공했나요?

🔗 관련 이슈

이번 PR과 관련된 이슈 번호를 기재해 주세요.
예: Closes #123


스크린샷 (선택)

UI 변경 사항이 있다면 스크린샷을 첨부해 주세요.

Copy link
Copy Markdown

@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 (12)
src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java (1)

58-81: blockedUserIds가 빈 리스트일 때 파생 쿼리의 NOT IN이 실패/오동작할 수 있음

findByUserInAndIsPublicIsTrueAndUser_IdNotIn(...)findByIsPublicIsTrueAndUser_IdNotIn(...)에 빈 리스트를 전달하면, JPA 구현에 따라 SQL NOT IN ()이 발생하거나 전체 반환/예외로 이어질 수 있습니다. 안전하게 가드가 필요합니다.

다음 두 가지 중 하나를 권장합니다.

  • 빠른 안전 패치(가드 값 사용): 빈 경우 불일치하는 센티널 ID를 주입
@@
-    List<Long> blockedUserIds = userBlockRepository.findBlockedUserIdsByBlocker(currentUser);
+    List<Long> blockedUserIds = userBlockRepository.findBlockedUserIdsByBlocker(currentUser);
+    // 빈 리스트일 때 NOT IN () 방지용 센티널 가드
+    if (blockedUserIds == null || blockedUserIds.isEmpty()) {
+      blockedUserIds = List.of(-1L); // 존재할 수 없는 PK 가정
+    }
  • 보다 명시적 해결(권장): 빈 리스트면 기존 메서드로 분기
@@
-      diaryStream =
-          diaryRepository
-              .findByUserInAndIsPublicIsTrueAndUser_IdNotIn(
-                  followingUsers, blockedUserIds, candidatePageable)
-              .stream()
-              .map(FeedResponse::from);
+      diaryStream =
+          (blockedUserIds == null || blockedUserIds.isEmpty()
+              ? diaryRepository.findByUserInAndIsPublicIsTrue(followingUsers, candidatePageable)
+              : diaryRepository.findByUserInAndIsPublicIsTrueAndUser_IdNotIn(
+                  followingUsers, blockedUserIds, candidatePageable))
+              .stream()
+              .map(FeedResponse::from);
@@
-      diaryStream =
-          diaryRepository
-              .findByIsPublicIsTrueAndUser_IdNotIn(blockedUserIds, candidatePageable)
-              .stream()
-              .map(FeedResponse::from);
+      diaryStream =
+          (blockedUserIds == null || blockedUserIds.isEmpty()
+              ? diaryRepository.findByIsPublicIsTrue(candidatePageable)
+              : diaryRepository.findByIsPublicIsTrueAndUser_IdNotIn(blockedUserIds, candidatePageable))
+              .stream()
+              .map(FeedResponse::from);

AvatarPostRepository 쪽도 동일 리스크가 있을 수 있습니다. 해당 리포지토리에 findByUserIn(...), findAll(...) 시그니처가 존재하는지 확인 후 동일한 분기 적용을 권장합니다.

src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/CreateDiaryRequest.java (1)

25-27: 주석과 유효성 제약 불일치: isPublic 필수 여부 결정 필요

주석은 “선택사항(기본 true)”인데, 코드에는 @NotNull로 필수값입니다. API 계약을 명확히 해 주세요. 필수로 유지한다면 주석을 수정하는 최소 변경을 권장합니다.

적용 예시(주석만 정정):

-  // 공개 여부는 선택사항으로, 값을 보내지 않으면 엔티티의 기본값(true)을 따릅니다.
-  @NotNull(message = "공개 여부는 필수값입니다. (true/false)")
+  // 공개 여부는 필수값입니다. (true/false)
+  @NotNull(message = "공개 여부는 필수값입니다. (true/false)")

선택값으로 전환하려면 @NotNull 제거 + Service/Entity에서 기본값 적용 분기 추가가 필요합니다.

src/main/java/com/example/cp_main_be/domain/mission/diary/domain/repository/DiaryRepository.java (1)

19-23: FeedService에서 DiaryRepository 호출 전 빈 컬렉션 검사 적용 필요

현재 아래 두 지점에서 빈 리스트가 그대로 Repository로 전달될 수 있어, JPA 구현체에 따라 SQL 구문 오류 또는 비의도적 결과가 발생할 위험이 있습니다. 서비스 계층(FeedService)에서 호출 전에 빈 컬렉션 분기 또는 센티널 값 적용 등 안전 가드를 반드시 추가해 주세요.

  • src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java
    • diaryRepository.findByUserInAndIsPublicIsTrueAndUser_IdNotIn(followingUsers, blockedUserIds, candidatePageable)
    • diaryRepository.findByIsPublicIsTrueAndUser_IdNotIn(blockedUserIds, candidatePageable)

※ 예시 처리 방식 중 하나를 선택하세요:

  • if (CollectionUtils.isEmpty(followingUsers)) return List.of();
  • 또는 blockedUserIds가 빈 경우 List.of(-1L) 같은 센티널 ID 리스트 사용
src/main/java/com/example/cp_main_be/domain/mission/diary/domain/Diary.java (2)

36-38: DiaryImage 연관관계: orphanRemoval/cascade 추가 제안

이미지 교체/삭제 시 고아 레코드와 FK 제약 위반을 예방하려면 부모 측에 orphanRemoval과 적절한 cascade를 두는 편이 안전합니다.

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

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

81-86: updateImage 편의 메서드: 기존 이미지 교체/제거 안전성 강화

현재는 새 이미지 설정 시 기존 연관을 끊는 처리가 없어, 1:1 연관 불변식을 깨뜨리거나 누수 위험이 있습니다. 교체/제거 모두 안전하게 처리해 주세요.

권장 구현:

-  public void updateImage(DiaryImage diaryImage) {
-    this.diaryImage = diaryImage;
-    if (diaryImage != null) {
-      diaryImage.setDiary(this); // 자식(DiaryImage)에게 부모(Diary)가 누구인지 알려줌
-    }
-  }
+  public void updateImage(DiaryImage newImage) {
+    // 1) 기존 이미지 제거(부모 참조 제거) → orphanRemoval=true면 삭제됨
+    if (this.diaryImage != null && this.diaryImage != newImage) {
+      this.diaryImage.setDiary(null);
+    }
+    // 2) 새 이미지 연결
+    this.diaryImage = newImage;
+    if (newImage != null) {
+      newImage.setDiary(this);
+    }
+  }

주의: DiaryImage.diary의 nullable=false를 유지한다면, 제거 시에는 부모 참조(null) 설정 이전에 orphan removal이 작동해야 합니다. 위 제안과 함께 부모 측 orphanRemoval=true 적용이 선행되어야 안전합니다.

src/main/java/com/example/cp_main_be/global/listener/NotificationEventListener.java (1)

88-93: Optional.get() 직호출로 인한 NoSuchElementException 위험

findById(...).get()은 대상이 없을 때 런타임 예외를 유발합니다. 알림 플로우 특성상 안전하게 처리하는 편이 좋습니다.

권장 변경:

-  if (Objects.equals(targetType, "DIARY")) {
-    receiver = diaryRepository.findById(targetId).get().getUser();
-    url = "/api/v1/diaries/" + targetId;
-  } else {
-    receiver = avatarPostRepository.findById(targetId).get().getUser();
-    url = "/api/v1/avatar-posts/" + targetId;
-  }
+  if (Objects.equals(targetType, "DIARY")) {
+    var diary = diaryRepository.findById(targetId)
+        .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 일기입니다: " + targetId));
+    receiver = diary.getUser();
+    url = "/api/v1/diaries/" + targetId;
+  } else if (Objects.equals(targetType, "AVATAR_POST")) {
+    var avatarPost = avatarPostRepository.findById(targetId)
+        .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 아바타 포스트입니다: " + targetId));
+    receiver = avatarPost.getUser();
+    url = "/api/v1/avatar-posts/" + targetId;
+  } else {
+    log.warn("알 수 없는 targetType: {}, targetId: {}", targetType, targetId);
+    return;
+  }
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryInfoResponse.java (1)

32-46: 레코드 생성자 인자 순서가 잘못되어 필드 매핑이 뒤섞여 있습니다 (치명적).

현재 3~7번째 인자에 title/content/imageUrl/writerName/profileImageUrl 순으로 전달되어, writerNameprofileImageUrl가 각각 title/content 자리에 들어갑니다. 응답 JSON 필드가 모두 어긋납니다. 아래처럼 순서를 고쳐주세요.

     return new DiaryInfoResponse(
         diary.getId(),
         diary.getUser().getId(),
-        diary.getTitle(),
-        diary.getContent(),
-        imageUrl,
-        writerName,
-        profileImageUrl,
+        writerName,
+        profileImageUrl,
+        diary.getTitle(),
+        diary.getContent(),
+        imageUrl,
         isLiked,
         diary.getLikeCount(),
         commentDTOs.size(),
         commentDTOs,
         diary.getCreatedAt(),
         diary.getUpdatedAt(),
         diary.isPublic());
src/main/java/com/example/cp_main_be/domain/mission/diary/service/DiaryService.java (5)

36-61: 이미지 연관관계 양방향 동기화 누락으로 인메모리 상태가 불일치할 수 있습니다

DiaryImage(연관관계의 주인)만 세팅하고 Diary 쪽 역방향을 갱신하지 않아, 같은 트랜잭션 내 응답(DiaryResponse.from(savedDiary))에 이미지가 반영되지 않을 수 있습니다.

아래처럼 Diary에도 반영해 주세요.

@@
-    if (request.getImageUrl() != null && !request.getImageUrl().isEmpty()) {
+    if (request.getImageUrl() != null && !request.getImageUrl().isEmpty()) {
       DiaryImage diaryImage =
           DiaryImage.builder()
               .imageUrl(request.getImageUrl())
               .diary(savedDiary) // ✅ 연관관계의 주인인 DiaryImage에 Diary를 설정
               .build();
-      diaryImageRepository.save(diaryImage);
+      // 역방향 동기화로 인메모리 상태 일치
+      savedDiary.updateImage(diaryImage);
+      diaryImageRepository.save(diaryImage);
     }

110-131: 이미지 변경/삭제 시 Diary ↔ DiaryImage 동기화 누락

업데이트/삭제 시 Diary의 역방향 필드가 갱신되지 않아 응답에 이전 이미지가 섞여 나올 수 있습니다.

다음과 같이 수정해 주세요.

@@
-    if (request.getImageUrl() != null && !request.getImageUrl().isEmpty()) {
+    if (request.getImageUrl() != null && !request.getImageUrl().isEmpty()) {
       if (existingImage != null) {
         // 기존 이미지가 있으면 URL만 업데이트
         existingImage.updateImageUrl(request.getImageUrl());
+        // 역방향도 보정
+        diary.updateImage(existingImage);
       } else {
         // 기존 이미지가 없으면 새로 생성
         DiaryImage newDiaryImage =
             DiaryImage.builder().imageUrl(request.getImageUrl()).diary(diary).build();
         diaryImageRepository.save(newDiaryImage);
+        diary.updateImage(newDiaryImage);
       }
     } else {
       // 이미지 URL이 null이거나 빈 문자열이면 기존 이미지 삭제
       if (existingImage != null) {
+        // 역방향 끊기
+        diary.updateImage(null);
         diaryImageRepository.delete(existingImage);
       }
     }

148-181: SecurityContext principal 캐스팅이 불안정합니다 (ClassCastException 가능)

컨트롤러 전반은 @AuthenticationPrincipal User 패턴인데, 본 메서드는 principal을 String(UUID)로 가정합니다. 환경에 따라 즉시 런타임 오류가 납니다.

안전한 캐스팅으로 보완하거나(단기), 서비스가 컨텍스트에 의존하지 않도록 컨트롤러에서 user/userId를 인자로 받도록 변경(권장)해 주세요.

단기 보완 예시:

@@
-    String uuidString =
-        (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
-    UUID userUuid = UUID.fromString(uuidString);
-    User user =
-        userRepository
-            .findByUuid(userUuid)
-            .orElseThrow(() -> new IllegalArgumentException("해당 사용자가 존재하지 않습니다."));
+    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+    User user;
+    if (principal instanceof User p) {
+      user = p;
+    } else if (principal instanceof String s) {
+      try {
+        UUID userUuid = UUID.fromString(s);
+        user =
+            userRepository
+                .findByUuid(userUuid)
+                .orElseThrow(() -> new IllegalArgumentException("해당 사용자가 존재하지 않습니다."));
+      } catch (IllegalArgumentException e) {
+        throw new AccessDeniedException("인증 정보가 유효하지 않습니다.");
+      }
+    } else {
+      throw new AccessDeniedException("인증 정보가 유효하지 않습니다.");
+    }

또한 외부 I/O(imageUploader.upload)는 트랜잭션 경계 밖으로 분리(이벤트/아웃박스)하면 실패 보정이 쉬워집니다.


183-215: deleteDiaryImage도 principal 처리/연관 해제 누락 동일 + 예외 타입 일관성

  • principal을 String으로 단정하고 있어 동일한 ClassCastException 위험이 있습니다.
  • 이미지 삭제 시 Diary 역방향을 null로 해제하지 않아 인메모리 불일치가 발생할 수 있습니다.
  • 권한 오류는 AccessDeniedException으로 통일 권장.
@@
-    String uuidString =
-        (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
-    UUID userUuid = UUID.fromString(uuidString);
-    User user =
-        userRepository
-            .findByUuid(userUuid)
-            .orElseThrow(() -> new IllegalArgumentException("해당 사용자가 존재하지 않습니다."));
+    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+    User user;
+    if (principal instanceof User p) {
+      user = p;
+    } else if (principal instanceof String s) {
+      try {
+        UUID userUuid = UUID.fromString(s);
+        user =
+            userRepository
+                .findByUuid(userUuid)
+                .orElseThrow(() -> new IllegalArgumentException("해당 사용자가 존재하지 않습니다."));
+      } catch (IllegalArgumentException e) {
+        throw new AccessDeniedException("인증 정보가 유효하지 않습니다.");
+      }
+    } else {
+      throw new AccessDeniedException("인증 정보가 유효하지 않습니다.");
+    }
@@
-    if (!diary.getUser().getId().equals(user.getId())) {
-      throw new IllegalStateException("해당 다이어리에 이미지를 추가할 권한이 없습니다.");
-    }
+    if (!diary.getUser().getId().equals(user.getId())) {
+      throw new AccessDeniedException("해당 다이어리에 이미지를 추가할 권한이 없습니다.");
+    }
@@
-    if (!diary.getUser().getId().equals(user.getId())) {
-      throw new IllegalStateException("해당 다이어리에 이미지를 삭제할 권한이 없습니다.");
-    }
+    if (!diary.getUser().getId().equals(user.getId())) {
+      throw new AccessDeniedException("해당 다이어리에 이미지를 삭제할 권한이 없습니다.");
+    }
@@
-    // 4. 클라우드 스토리지(S3)에서 실제 파일 삭제
+    // 4. 역방향 연관 해제 + 클라우드 스토리지(S3)에서 실제 파일 삭제
+    diary.updateImage(null);
     imageUploader.delete(diaryImage.getImageUrl());
@@
-    diaryImageRepository.deleteById(diaryImage.getId());
+    diaryImageRepository.deleteById(diaryImage.getId());

외부 I/O 순서/실패 보정은 이벤트 기반 비동기 삭제 등으로 개선 여지가 있습니다(선택).


136-146: 일기 삭제 시 연관 데이터 정리(cascade/orphanRemoval) 미구현—필수 수정 필요

검증 결과

  • Diary 엔티티의 @OneToOne(mappedBy="diary") 관계에 cascade/orphanRemoval 설정이 빠져 있어, 삭제 시 DiaryImage가 고아 레코드로 남습니다.
  • Like 엔티티는 JPA 매핑이 아닌 targetType/targetId 필드로 참조하므로, 일기 삭제 시 연관된 좋아요 레코드가 자동 정리되지 않습니다.

따라서 아래 위치에 반드시 다음 조치를 반영해 주세요.

• src/main/java/com/example/cp_main_be/domain/mission/diary/domain/Diary.java (약 36행)

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

• src/main/java/com/example/cp_main_be/domain/mission/diary/service/DiaryService.java deleteDiary 메서드 (136–146행)

   // 연관 좋아요(Like) 삭제
+  likeRepository.deleteByTargetTypeAndTargetId("DIARY", diaryId);

   diaryRepository.delete(diary);

• 또는 DB 스키마에 FK ON DELETE CASCADE, 혹은 DiaryImage@OnDelete(action = OnDeleteAction.CASCADE) 적용

위 조치 없이 운영 시 DiaryImageLike 테이블에 고아 레코드가 남아 데이터 무결성이 깨질 위험이 있으므로, 반드시 위 리팩터링 후 재검증 바랍니다.

🧹 Nitpick comments (36)
src/main/java/com/example/cp_main_be/domain/mission/diaryimage/domain/DiaryImageRepository.java (1)

1-5: Repository 패키지 위치 일관성 제안

DiaryRepository...diary.domain.repository 하위에 위치하는 반면, 본 파일은 ...diaryimage.domain에 위치합니다. 모듈 탐색성/일관성을 위해 ...diaryimage.domain.repository로 정리하는 것을 권장합니다. (스프링 컴포넌트 스캔에는 영향 없으나, 팀 규칙 관점의 일관성 유지에 유리)

가능한 변경 예:

-package com.example.cp_main_be.domain.mission.diaryimage.domain;
+package com.example.cp_main_be.domain.mission.diaryimage.domain.repository;

변경 시, 사용처 import 경로도 함께 업데이트 부탁드립니다.

src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/UpdateDiaryRequest.java (3)

15-26: Update vs Patch 의도 확인 필요

현재 @NotBlank/@NotNull 제약으로 모든 필드가 필수입니다. 부분 수정(PATCH) 의도라면 null 허용 및 필드별 선택적 반영이 필요합니다. 반대로 전체 수정(PUT) 의도라면 현 상태 유지가 타당합니다. API 의도 확인 후 제약을 맞춰 주세요.


22-22: imageUrl 필드 유지 필요성 재검토

이미지 업로드/삭제가 DiaryImageController의 멀티파트 엔드포인트로 분리되어 있어, UpdateDiaryRequestimageUrl은 중복 혹은 혼선을 유발할 수 있습니다. 유지가 필요 없다면 제거하거나, 문서상 폐기(deprecated)로 표시를 추천합니다.

예: 폐기 표시(스웨거 사용 시)

추가 import:

import io.swagger.v3.oas.annotations.media.Schema;

필드 주석:

-  private String imageUrl;
+  @Schema(description = "이미지 URL(별도 이미지 API 사용 시 무시됨)", deprecated = true)
+  private String imageUrl;

24-25: Boolean/boolean 혼용 및 JSON 프로퍼티명 불일치 가능성

본 DTO는 Boolean isPublic, DiaryWriteRequestboolean isPublic을 사용합니다. Lombok/Jackson 조합에서 접근자 메서드명(getIsPublic vs isPublic) 차이로 직렬화/역직렬화 시 프로퍼티명이 혼선될 수 있습니다. 두 DTO 모두에 @JsonProperty("isPublic")를 부여해 API 계약을 고정하는 것을 권장합니다.

이 파일 내 변경 예(필드에만 적용; 추가 import 필요):

-  private Boolean isPublic;
+  @com.fasterxml.jackson.annotation.JsonProperty("isPublic")
+  private Boolean isPublic;

추가 import:

import com.fasterxml.jackson.annotation.JsonProperty;
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/DiaryWriteRequest.java (2)

10-14: 검증 애너테이션 누락 — Update DTO와의 일관성 확보

생성 요청에도 최소한의 유효성 검증을 권장합니다. title 길이 제한과 content 공백 불가 등은 Update와 동일하게 가져가는 편이 API 일관성에 유리합니다.

가능한 변경 예(필드 레벨에만 적용; 추가 import 필요):

-  private String title;
-  private String content;
+  @jakarta.validation.constraints.NotBlank(message = "제목은 필수 입력 항목입니다.")
+  @jakarta.validation.constraints.Size(max = 100, message = "제목은 100자를 초과할 수 없습니다.")
+  private String title;
+  @jakarta.validation.constraints.NotBlank(message = "내용은 필수 입력 항목입니다.")
+  private String content;

또한 isPublic 프로퍼티명 고정을 위해 아래와 같이 권장합니다:

-  private boolean isPublic;
+  @com.fasterxml.jackson.annotation.JsonProperty("isPublic")
+  private boolean isPublic;

추가 import:

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import com.fasterxml.jackson.annotation.JsonProperty;

13-13: Boolean/boolean 타입 전략 통일 검토

본 DTO는 boolean, UpdateDiaryRequestBoolean을 사용합니다. 선택 입력 허용 여부에 따라 둘 중 하나로 통일하는 편이 혼선을 줄입니다. 생성 시 반드시 입력 받아야 한다면 boolean, 선택이면 Boolean로 맞춰 주세요.

src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryIdResponse.java (1)

6-11: 응답 DTO 불변성 강화 제안

응답 객체는 가급적 불변이 바람직합니다. Java 17+라면 record 사용, 아니라면 final 필드 + 적절한 생성자 조합을 권장합니다.

옵션 A: Java 17+ record

-@Getter
-@Builder
-public class DiaryIdResponse {
-
-  private Long id;
-}
+public record DiaryIdResponse(Long id) {}

옵션 B: Lombok 불변화(Builder 유지)

 @Getter
 @Builder
-public class DiaryIdResponse {
+public class DiaryIdResponse {
 
-  private Long id;
+  private final Long id;
 }
src/main/java/com/example/cp_main_be/domain/mission/diaryimage/presentation/DiaryImageController.java (2)

23-31: POST 생성 응답 코드 201 Created 고려

이미지 생성 성공 시 200 OK 대신 201 Created를 반환하는 것이 REST 컨벤션에 더 부합합니다. (리소스 ID를 응답/Location 헤더로 제공할 수 있으면 더 좋음)

가능한 최소 변경:

-    return ResponseEntity.ok(ApiResponse.success(null));
+    return ResponseEntity.status(org.springframework.http.HttpStatus.CREATED)
+        .body(ApiResponse.success(null));

23-27: 멀티파트 파라미터 명시성 — @RequestPart 사용 고려

단일 파일 업로드라도 @RequestPart("file")로 명시하면 Swagger 문서/바인딩 명확성이 개선됩니다. 현재 구현도 동작에는 문제 없습니다.

-      @PathVariable Long diaryId, @RequestParam MultipartFile file) {
+      @PathVariable Long diaryId, @RequestPart("file") MultipartFile file) {
src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java (1)

74-77: 일기 컬렉션의 cascade=ALL + orphanRemoval=true 재확인

현재 설정대로면 User.diaries 컬렉션에서 요소를 제거하는 것만으로 해당 Diary가 물리 삭제됩니다. 미션 도메인으로 이동하면서 소유권/삭제 정책이 바뀌지 않았다면 문제없지만, “사용자 탈퇴 시만 삭제” 같은 정책이면 과도합니다. 정책에 맞는지 한번 더 확인 부탁드립니다.

src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java (1)

39-47: 메모리 내 병합·정렬 비용: 깊은 페이지로 갈수록 비용 증가

현재 전략은 각 소스에서 (page+1)*size 만큼 당겨와 메모리에서 합치고 정렬 후 슬라이싱합니다. 페이지가 커질수록 불필요한 로드를 유발합니다. 트래픽/데이터 증가를 고려하면 커서 기반 페이징(키셋) 또는 소스별 커서를 유지해 병합하는 방식으로의 전환을 제안합니다.

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

33-49: 중복 좋아요 방지 로직은 OK, 예외 타입 통일 고려

exists 사전 체크 + Unique 제약 위반 캐치로 경쟁 상황을 흡수하고 있습니다. 다만 RuntimeException/IllegalArgumentException 혼용보다는 도메인 예외로 통일하면 API 응답 일관성이 좋아집니다.


50-66: targetType 문자열 상수 난립 — Enum으로 수렴 권장

"feed"/"DIARY"/"AVATAR_POST"/"diary"/"avatar_post" 등 케이스/스펠링이 혼재합니다. equalsIgnoreCase로 버티고 있으나, 잘못된 값이 들어와도 일부 분기 누락 위험이 있습니다. LikeTargetType Enum을 도입해 컴파일 타임으로 통제하는 것을 권장합니다.

다음과 같이 도입을 제안합니다.

public enum LikeTargetType { FEED, DIARY, AVATAR_POST }

서비스 입구에서 문자열 → Enum 파싱, 분기는 Enum 기반 switch로 단순화하세요.

Also applies to: 95-125


65-78: 엔티티에 버전 필드 미존재로 인한 동시성 정합성 리스크 확인
스크립트 결과 @Version 애노테이션이 적용된 버전 필드가 코드베이스 전역에 존재하지 않습니다. 따라서 현재의 increaseLikeCount()/decreaseLikeCount() 단순 증감 로직은 동시성 충돌 시 마지막 커밋으로 덮어씌워져 좋아요 수 정합성이 깨질 수 있습니다.

수정이 필요한 위치

  • Diary 엔티티 클래스
  • AvatarPost 엔티티 클래스

다음 중 하나를 선택해 적용을 검토해주세요:

  • 엔티티에 @Version 필드를 추가하여 낙관적 락 적용
  • 좋아요 테이블의 레코드 수를 조회하는 파생값(Like 테이블 count) 방식으로 로직 전환
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/CreateDiaryRequest.java (1)

19-21: 본문/이미지 URL 유효성 보강 제안

  • content에 상한선(@SiZe(max=...))을 두어 과도한 페이로드를 방지
  • imageUrl은 URL 형식 검증(@pattern 또는 커스텀 Validator) 고려
src/main/java/com/example/cp_main_be/domain/mission/diary/domain/repository/DiaryRepository.java (1)

24-31: 다중 fetch join 시 연관 형태 점검 권장

LEFT JOIN FETCH d.diaryImage di + LEFT JOIN FETCH d.comments c 조합입니다. 만약 diaryImage가 to-many면 Hibernate에서 동시 bag fetch 예외가 발생할 수 있습니다. diaryImage가 to-one이면 문제 없습니다. 연관 형태를 확인해 주세요. to-many라면 컬렉션 하나는 batch fetch로 전환하거나 별도 조회로 분리하는 방안을 권장합니다.

src/main/java/com/example/cp_main_be/domain/social/feed/dto/response/FeedResponse.java (1)

20-27: AvatarPost 이미지 필드 주석 제거 및 필드명 확정 필요

프로덕션 코드에 “필드가 있다고 가정” 주석을 남겨두지 말고 실제 필드명으로 확정해 주세요. 현재 구현은 컴파일은 되지만, 도메인 스키마를 바꾸면 쉽게 깨집니다.

권장 변경(주석 제거만 예시):

   public static FeedResponse from(AvatarPost avatarPost) {
-    // AvatarPost에 imageUrl 필드가 있다고 가정합니다. 필드명은 실제 코드에 맞게 수정해주세요.
     return new FeedResponse(
         avatarPost.getId(),
         PostType.AVATAR_POST,
         avatarPost.getImageUrl(),
         avatarPost.getCreatedAt());
   }
src/main/java/com/example/cp_main_be/domain/mission/diary/domain/Diary.java (2)

53-57: 엔티티 직렬화 의존도 축소 및 @JsonManaged/@JsonBack 페어링 확인

다중 연관(@comments, @user)에 기본 키값 미지정 @JsonManagedReference/@JsonBackReference를 혼용하면 직렬화 쪽에서 페어링 충돌 가능성이 있습니다. 가능하면 응답 DTO를 일관 사용하고, 엔티티에는 Jackson 애노테이션 의존을 줄이세요. 계속 유지한다면 각 페어에 value를 명시해 충돌을 방지해 주세요.

예시:

@JsonManagedReference("diary-comments")
private List<Comment> comments = new ArrayList<>();

@JsonBackReference("user-diaries")
private User user;

Also applies to: 58-61


63-72: 생성/수정 시각은 Auditing으로 위임 권장

수동 LocalDateTime.now()는 테스트/타임존 이슈를 초래합니다. Spring Data JPA Auditing(@CreatedDate, @LastModifiedDate)으로 일원화하는 것을 권장합니다.

권장 변경 요약:

  • 클래스에 @EntityListeners(AuditingEntityListener.class) 추가
  • 필드에 @CreatedDate, @LastModifiedDate 적용
  • onCreate/onUpdate 제거
src/main/java/com/example/cp_main_be/domain/mission/diaryimage/domain/DiaryImage.java (2)

22-25: OneToOne 기본 페치 전략 EAGER → LAZY 권장

이미지 로딩 시 매번 Diary까지 즉시 로딩되는 비용을 줄이려면 LAZY로 전환하는 편이 일반적입니다.

권장 변경:

-  @OneToOne
+  @OneToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "diary_id", nullable = false)
   private Diary diary;

27-33: createdAt 자동 설정 누락

createdAt이 세팅되지 않습니다. Auditing 사용 또는 @PrePersist 훅을 추가해 주세요.

권장 추가:

 public class DiaryImage {
@@
   @Column(name = "created_at")
   private LocalDateTime createdAt;
 
+  @PrePersist
+  private void onCreate() {
+    this.createdAt = LocalDateTime.now();
+  }
src/main/java/com/example/cp_main_be/domain/social/comment/service/CommentService.java (3)

48-64: 문자열 기반 targetType 분기 → enum 기반으로 치환 권장

"DIARY"/"AVATAR_POST" 하드코딩은 오타에 취약합니다. 공용 enum(TargetType 등)으로 치환하고 요청 DTO에서 변환하도록 하면 서비스 단 로직이 견고해집니다.

권장 개략:

public enum TargetType { DIARY, AVATAR_POST }

@TargetTypeConstraint // 커스텀 밸리데이션(Optional)
private TargetType targetType;

그리고 분기는 switch (request.getTargetType()) 형태로 단순화 가능합니다.


49-55: 예외 타입 정교화

현재 IllegalArgumentException은 404 시맨틱과 어울리지 않습니다. 도메인별 NotFound 예외(예: DiaryNotFoundException, AvatarPostNotFoundException)를 사용하거나 통일된 ResourceNotFoundException으로 교체하세요. 기존에 UserNotFoundException을 사용하고 있으므로 일관성 차원에서 맞추는 것이 좋습니다.

원하시면 공용 ResourceNotFoundException 생성과 전역 예외 처리기(@ControllerAdvice) 보강용 패치도 드릴게요.

Also applies to: 56-62


76-85: update/delete의 RuntimeException 제거

작성자 불일치와 리소스 미존재는 비즈니스 에러입니다. ForbiddenOperationException/ResourceNotFoundException 등 의미 있는 예외로 대체해 주세요. 또한 save()는 변경감지 하에 불필요할 수 있습니다(트랜잭션 유지 시).

권장 변경(요지):

- .orElseThrow(() -> new RuntimeException("댓글을 찾을 수 없습니다."))
+ .orElseThrow(() -> new ResourceNotFoundException("댓글을 찾을 수 없습니다."))
 
- throw new RuntimeException("댓글 작성자만 수정할 수 있습니다.")
+ throw new ForbiddenOperationException("댓글 작성자만 수정할 수 있습니다.")

Also applies to: 93-101

src/main/java/com/example/cp_main_be/global/listener/NotificationEventListener.java (2)

50-51: 리다이렉트 URL 규칙 일관화

댓글 알림은 /diaries/{id}, 좋아요 알림은 /api/v1/diaries/{id}로 불일치합니다. FE 라우팅/딥링크 기준으로 하나로 정렬해 주세요.

Also applies to: 89-93


31-32: 미사용 의존성 정리

FeedRepository가 사용되지 않습니다. 불필요한 주입은 제거하세요.

src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryInfoResponse.java (1)

17-17: boolean 직렬화 키 안정화: isLiked@JsonProperty로 고정하는 것을 권장합니다.

Records에서는 대개 컴포넌트명이 그대로 키가 되지만, 팀 전반에서 boolean 키를 명시적으로 고정하는 패턴(isPublic)을 이미 사용 중입니다. 동일한 컨벤션을 적용해 예기치 않은 키 변경을 방지하세요.

-    boolean isLiked,
+    @JsonProperty("isLiked") boolean isLiked,
src/main/java/com/example/cp_main_be/domain/social/comment/domain/Comment.java (1)

37-41: 두 부모(AvatarPost/Diary) 중 단 하나만 설정되도록 Bean Validation 및 헬퍼 메서드 추가

현재 Comment 엔티티에는 “둘 중 하나만 값이 있게 됨”이라는 주석만 있을 뿐, 런타임이나 DB 레벨에서 이를 보장할 수 있는 검증 로직이 없습니다. 잘못된 상태(둘 다 null 혹은 둘 다 non-null)가 저장될 수 있으므로, 아래와 같이 도메인 제약과 헬퍼 메서드를 도입해 안전성을 강화하세요.

  • Comment 엔티티 내부에 Bean Validation을 활용한 배타적 부모 제약 추가

    // imports
    import jakarta.validation.constraints.AssertTrue;
    
    // Comment 클래스 내부에 추가
    @AssertTrue(message = "avatarPost 또는 diary 중 정확히 하나만 설정되어야 합니다.")
    private boolean isExclusiveParent() {
      return (avatarPost != null) ^ (diary != null);
    }
    
    public void attachToAvatarPost(AvatarPost post) {
      this.avatarPost = post;
      this.diary = null;
    }
    
    public void attachToDiary(Diary diary) {
      this.diary = diary;
      this.avatarPost = null;
    }
  • JSON 직렬화 순환 방지를 위한 @JsonBackReference/@JsonManagedReference 쌍 확인
    • Diary 쪽에는 이미

    // Diary.java:53-54
    @OneToMany(mappedBy = "diary", cascade = CascadeType.ALL, orphanRemoval = true)
    @JsonManagedReference
    private List<Comment> comments = new ArrayList<>();

    가 존재합니다.
    • AvatarPost 쪽에는 다음과 같은 매핑만 있을 뿐, @JsonManagedReference가 빠져 있습니다.

    // AvatarPost.java (comments 필드)
    @OneToMany(mappedBy = "avatarPost", cascade = CascadeType.ALL)
    - private List<Comment> comments;
    + @JsonManagedReference
    + private List<Comment> comments = new ArrayList<>();

위 변경을 통해

  1. 런타임에 잘못된 부모 참조 상태가 저장되는 것을 방지
  2. 양방향 연관관계 직렬화 시 순환 참조 예외 방지
    를 동시에 달성할 수 있습니다.
src/main/java/com/example/cp_main_be/domain/reports/service/ReportService.java (1)

70-77: 예외 메시지 로캘/도메인 일관성 정리 (사소하지만 권장).

다른 곳은 한글 메시지인데 여기만 영문입니다. 사용자/운영 로그 일관성을 위해 메시지를 한글로 정리해 주세요. 또한 IllegalArgumentException 대신 프로젝트 표준 예외가 있다면 그걸 사용하세요.

-                .orElseThrow(() -> new IllegalArgumentException("Diary not found"));
+                .orElseThrow(() -> new IllegalArgumentException("다이어리를 찾을 수 없습니다."));
...
-                .orElseThrow(() -> new IllegalArgumentException("Comment not found"));
+                .orElseThrow(() -> new IllegalArgumentException("댓글을 찾을 수 없습니다."));
src/main/java/com/example/cp_main_be/domain/mission/diary/presentation/DiaryController.java (4)

28-34: 생성 API는 201 Created로 응답하는 것이 REST 관례에 더 적합합니다

현재 200 OK를 반환합니다. 생성 시 201 Created(+ Location 헤더)로 응답하면 클라이언트 측 의미가 더 명확합니다.

다음과 같이 상태 코드를 조정해 보세요:

+import org.springframework.http.HttpStatus;
@@
-    return ResponseEntity.ok(ApiResponse.success(DiaryResponse.from(createdDiary)));
+    return ResponseEntity.status(HttpStatus.CREATED)
+        .body(ApiResponse.success(DiaryResponse.from(createdDiary)));

필요 시 Location 헤더도 함께 세팅 가능합니다.


36-43: 목록 조회는 페이지네이션 지원을 검토해 주세요

다이어리 수가 증가하면 전체 목록 반환은 비효율적입니다. Pageable(PageRequest), Slice/Page 기반 응답으로 확장성 확보를 권장합니다.


45-52: TODO 중복/불일치: 비공개 접근 제어는 서비스에서 이미 처리됩니다

DiaryService.getDiaryInfo(...)에서 비공개 접근을 차단합니다(AccessDeniedException). 컨트롤러 TODO 주석은 제거하여 혼선을 줄이는 게 좋습니다.

-    // TODO: 비공개 글일 경우 작성자만 볼 수 있도록 하는 로직 추가 필요

64-70: 삭제 API는 204 No Content가 적합합니다

현재 200 OK + body(null)를 반환합니다. 삭제에는 204가 더 일반적입니다. (서명 변경 영향이 허용된다면) 아래처럼 조정해 보세요.

-  public ResponseEntity<ApiResponse<Void>> deleteDiary(
+  public ResponseEntity<Void> deleteDiary(
       @AuthenticationPrincipal User user, @PathVariable Long diaryId) {
     diaryService.deleteDiary(user.getId(), diaryId);
-    return ResponseEntity.ok(ApiResponse.success(null));
+    return ResponseEntity.noContent().build();
   }
src/main/java/com/example/cp_main_be/domain/mission/diary/service/DiaryService.java (3)

103-109: 권한 예외 타입을 AccessDeniedException으로 통일

update/delete에서는 SecurityException을 사용하고, 조회는 AccessDeniedException을 사용합니다. 스프링 시큐리티/전역 예외 처리와의 일관성을 위해 AccessDeniedException으로 통일을 권장합니다.

-    if (!Objects.equals(diary.getUser().getId(), userId)) {
-      throw new SecurityException("일기를 수정할 권한이 없습니다.");
-    }
+    if (!Objects.equals(diary.getUser().getId(), userId)) {
+      throw new AccessDeniedException("일기를 수정할 권한이 없습니다.");
+    }

동일 패턴을 deleteDiary(...)에도 적용해 주세요.


71-95: 하드코딩된 타깃 타입 문자열은 enum/상수로 치환 권장

"DIARY" 문자열은 오타에 취약합니다. enum(TargetType.DIARY) 또는 상수로 추출해 LikeRepository 시그니처도 일관되게 가져가면 안전합니다.


97-102: 내 일기 목록 조회: 페이지네이션 고려

findByUserOrderByCreatedAtDesc(user)는 전체 반환입니다. Page 기반으로 전환하면 대량 데이터에서도 안정적입니다.

📜 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 dfd7dbe and 5a1385a.

📒 Files selected for processing (22)
  • src/main/java/com/example/cp_main_be/domain/member/user/domain/User.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/domain/Diary.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/domain/repository/DiaryRepository.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/CreateDiaryRequest.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/DiaryWriteRequest.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/UpdateDiaryRequest.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryFeedItemResponse.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryIdResponse.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryInfoResponse.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryResponse.java (2 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/presentation/DiaryController.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diary/service/DiaryService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diaryimage/domain/DiaryImage.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diaryimage/domain/DiaryImageRepository.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/mission/diaryimage/presentation/DiaryImageController.java (3 hunks)
  • src/main/java/com/example/cp_main_be/domain/reports/service/ReportService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/comment/domain/Comment.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/comment/service/CommentService.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/feed/dto/response/FeedResponse.java (1 hunks)
  • src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.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/listener/NotificationEventListener.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryFeedItemResponse.java (1)
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryResponse.java (1)
  • Getter (7-52)
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryResponse.java (5)
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/CreateDiaryRequest.java (1)
  • Getter (10-28)
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/DiaryWriteRequest.java (1)
  • Getter (6-14)
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/UpdateDiaryRequest.java (1)
  • Getter (10-26)
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryFeedItemResponse.java (1)
  • Getter (9-31)
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryIdResponse.java (1)
  • Getter (6-11)
🔇 Additional comments (17)
src/main/java/com/example/cp_main_be/domain/mission/diaryimage/domain/DiaryImageRepository.java (1)

1-1: 패키지 리네임 정상 반영

소셜 → 미션 네임스페이스 이동만 반영된 것으로 보이며, JPA 리포지토리 시그니처는 그대로 유지되어 문제 없습니다.

src/main/java/com/example/cp_main_be/domain/mission/diaryimage/presentation/DiaryImageController.java (1)

15-16: 엔드포인트 변경 영향 없음 확인

아래 스크립트 실행 결과, DiaryImageController 외에 구 버전 경로(api/v1/diaries/.../images 또는 Swagger 설명 내 /diaries/{diaryId}/images)가 하드코딩된 곳을 찾을 수 없었습니다.

  • LikeController의 좋아요 API(/diaries/{diaryId}/likes)는 의도된 대로 유지됨
  • DiaryImageController의 삭제 엔드포인트(/api/v1/diaries/{diaryId}/images/{imageId})만 존재함

따라서 코드 내 추가적인 호출부, 필터 매칭, 시큐리티 설정, API 문서 문자열 수정은 필요치 않습니다.

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

5-5: Diary 엔티티 user 매핑 및 어노테이션 확인 완료

다음 사항을 점검했습니다.

  • Diary 클래스에 private User user 필드가 존재하며
    • @ManyToOne(fetch = FetchType.LAZY)
    • @JoinColumn(name = "user_id", nullable = false)
    • @JsonBackReference
      어노테이션이 올바르게 적용되어 있습니다.
  • User 클래스의 임포트 경로(com.example.cp_main_be.domain.member.user.domain.User)도 정확합니다.

추가 조치 없이 머지 진행하셔도 좋습니다.

src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java (1)

6-6: DiaryRepository import 경로 변경: 잔여 구(舊) 패키지 참조 확인 완료

  • rg 검색 결과, 실제 코드(import 구문)에서는 com.example.cp_main_be.domain.social.diary 패키지를 참조하는 구(舊) import가 전혀 없습니다.
  • 남아 있는 경로·문자열은 모두 테스트 코드의 주석 처리된 라인에서만 발견되었으며(예: // import com.example…social.diary…), 실제 빌드나 실행에는 영향이 없습니다.
  • 파일 시스템상에도 src/test/java/com/example/cp_main_be/domain/social/diary 디렉터리만 남아 있으나, 해당 테스트들은 모두 새 경로를 사용 중이므로 동작에는 무관합니다.

따라서 패키지 교체는 안전하며 변경 사항을 승인합니다.
테스트 주석 및 불필요 디렉터리 정리는 선택적 리팩토링으로 고려하실 수 있습니다.

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

7-9: Diary/DiaryRepository import 경로 교체 OK

리패키징과 일치하며 동작 영향은 없습니다.

src/main/java/com/example/cp_main_be/domain/mission/diary/dto/request/CreateDiaryRequest.java (1)

1-1: 패키지 이동 OK

미션 도메인으로의 일관된 이동과 맞습니다.

src/main/java/com/example/cp_main_be/domain/mission/diary/domain/repository/DiaryRepository.java (1)

1-5: 패키지 및 import 이동 정상

소셜 → 미션 네임스페이스 이전과 정합합니다.

src/main/java/com/example/cp_main_be/domain/social/feed/dto/response/FeedResponse.java (2)

3-3: DIARY 도메인 전환 반영 OK

mission.diary로의 타입 전환 및 팩토리 메서드 시그니처 정렬이 일관되게 적용되었습니다. 추가적인 사이드 이펙트는 없어 보입니다.

Also applies to: 15-18


12-13: createdAt에 @JsonIgnore를 유지할지 재검토

피드 정렬/표시에 createdAt이 필요하다면 직렬화 제외는 클라이언트 요구사항과 충돌할 수 있습니다. 반대로 서버 정렬 전용이라면 주석으로 의도를 명시하거나 전용 정렬 키로 분리하는 방안을 고려해 주세요.

src/main/java/com/example/cp_main_be/domain/social/comment/service/CommentService.java (1)

5-7: DIARY 도메인 전환 및 Repository 주입 정리 OK

mission.diary.*로의 import/주입 변경이 자연스럽고, 기존 플로우와 충돌 없어 보입니다.

Also applies to: 28-31

src/main/java/com/example/cp_main_be/global/listener/NotificationEventListener.java (1)

6-8: DIARY 도메인 import 전환 OK

미션 도메인으로의 의존성 전환이 이벤트 리스너에도 일관 적용되었습니다.

src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryInfoResponse.java (1)

26-27: 확인 결과: Diary 엔티티에 comments 필드는 @onetomany 매핑이 적용되어 있으며, 클래스 레벨 @Getter 어노테이션으로 인해 getComments() 메서드가 자동 생성됩니다. 따라서 해당 DTO의 diary.getComments() 호출은 안전합니다.

  • src/main/java/com/example/cp_main_be/domain/mission/diary/domain/Diary.java:16 라인에 클래스 레벨 @Getter 어노테이션 확인
  • src/main/java/com/example/cp_main_be/domain/mission/diary/domain/Diary.java:56 라인에 private List<Comment> comments = new ArrayList<>();@OneToMany(mappedBy = "diary", cascade = CascadeType.ALL, orphanRemoval = true) 매핑 확인
src/main/java/com/example/cp_main_be/domain/reports/service/ReportService.java (1)

5-7: 미션 도메인으로의 Diary 리포트 경로 전환: 적용 적절합니다.

Diary/DiaryRepositorymission.diary로 교체하고, DIARY 분기에서 작성자 ID를 안전하게 해석하는 흐름이 일관됩니다. 트랜잭션 범위도 클래스 레벨에서 커버되고 있어 문제 없어 보입니다.

Also applies to: 28-29, 66-72

src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryFeedItemResponse.java (1)

18-30: commentCount를 final로 변경하고 생성자 주입 방식으로 전환 필요

현재 commentCount 필드는 가변(int)으로 선언되어 있으며, 생성자에서 초기화되지 않아 기본값 0만 갖습니다. DTO를 불변 객체로 설계하려는 의도에 맞추어, 서비스 계층에서 집계한 댓글 수를 주입하도록 생성자를 수정해 주세요.

주요 제안 사항:

  • commentCount 필드를 final int로 변경
  • 기존 생성자 시그니처에 int commentCount 파라미터 추가
  • 필요 시 기본값 0을 주입하는 보조 생성자 추가

호출부 검증

  • rg -nP 'new\s+DiaryFeedItemResponse\s*\(' -C2 명령어를 실행했으나 호출부를 찾을 수 없었습니다.
    – 호출부가 아직 구현되지 않았거나, 다른 방식으로 인스턴스화되고 있을 수 있으므로 수동 검증을 권장합니다.

예시 Diff:

-  private int commentCount; // Diary에 Comment 리스트가 있다고 가정
+  private final int commentCount;

-  public DiaryFeedItemResponse(Diary diary) {
+  public DiaryFeedItemResponse(Diary diary, int commentCount) {
     this.postId = diary.getId();
     this.author = new AuthorResponse(diary.getUser());
     this.title = diary.getTitle();
     this.content = diary.getContent();
     this.imageUrl = diary.getDiaryImage() != null ? diary.getDiaryImage().getImageUrl() : null;
     this.likeCount = diary.getLikeCount();
     this.createdAt = diary.getCreatedAt();
-    // this.commentCount = diary.getComments().size(); // Diary에 OneToMany Comment 관계가 필요
+    this.commentCount = commentCount;
   }

+  // 호출부 호환성을 위한 기본값 주입 보조 생성자
+  public DiaryFeedItemResponse(Diary diary) {
+    this(diary, 0);
+  }

✔️ 서비스/컨트롤러 등 DiaryFeedItemResponse를 생성하는 모든 곳에서 댓글 수 인자를 함께 전달하도록 코드 전체를 검토·수정해 주세요.

src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryResponse.java (1)

40-51: 정적 팩토리와 불변 필드 구성은 깔끔합니다.

이미지 URL null-세이프 처리, 불변 필드, private 생성자 + 정적 팩토리 패턴이 일관되고 명확합니다. 위 isPublic 직렬화 키만 보완되면 충분히 안정적입니다.

src/main/java/com/example/cp_main_be/domain/mission/diary/presentation/DiaryController.java (1)

1-9: 리패키징 및 임포트 경로 정리 👍

social → mission 네임스페이스로의 이동이 일관되게 반영되었습니다. 컨트롤러의 공개 API 시그니처 변화도 없어 안전합니다.

src/main/java/com/example/cp_main_be/domain/mission/diary/service/DiaryService.java (1)

1-13: 리패키징 정합성 확인

mission.diary 및 mission.diaryimage 네임스페이스로의 의존성 이동이 일관됩니다. 컨슈머(컨트롤러)와의 연결도 정상입니다.

private final String title;
private final String content;
private final String imageUrl;
private final boolean isPublic;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

JSON 키 호환성: isPublic 직렬화 키가 public로 내려갈 수 있습니다.

Lombok @Getter의 boolean 접근자(isPublic()) 때문에 Jackson이 속성명을 public으로 해석할 수 있습니다. DiaryInfoResponse에서는 이미 @JsonProperty("isPublic")로 고정하셨으니, 본 DTO도 동일하게 맞춰야 프런트 계약이 깨지지 않습니다.

-  private final boolean isPublic;
+  @com.fasterxml.jackson.annotation.JsonProperty("isPublic")
+  private final boolean isPublic;
📝 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
private final boolean isPublic;
@com.fasterxml.jackson.annotation.JsonProperty("isPublic")
private final boolean isPublic;
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/diary/dto/response/DiaryResponse.java
around line 13, the boolean field declaration private final boolean isPublic may
be serialized by Jackson as "public" due to Lombok-generated isPublic()
accessor; add an explicit @JsonProperty("isPublic") annotation on the field (or
its getter) and import com.fasterxml.jackson.annotation.JsonProperty so Jackson
uses the "isPublic" key to match DiaryInfoResponse and preserve the frontend
contract.

@RestController
@RequiredArgsConstructor
@RequestMapping("api/v1/diaries")
@RequestMapping("api/v1")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

매핑 경로 슬래시 누락 및 경로 불일치로 인한 오동작 가능성

클래스 레벨 @RequestMapping("api/v1")와 메서드 레벨 @PostMapping("{diaryId}/images")는 선행 슬래시가 없어 조합 경로가 의도와 달라질 수 있습니다. 또한 Delete는 /diaries/...를 포함하는 반면, Post는 diaries 세그먼트가 빠져 불일치합니다. 아래와 같이 정정 제안드립니다.

-@RequestMapping("api/v1")
+@RequestMapping("/api/v1")
...
-  @PostMapping(value = "{diaryId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+  @PostMapping(value = "/diaries/{diaryId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

Also applies to: 21-23

🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/mission/diaryimage/presentation/DiaryImageController.java
around line 15 (and also affecting lines 21-23), the class-level @RequestMapping
is missing a leading slash and the method-level paths are inconsistent (POST
lacks the "diaries" segment while DELETE includes it), causing incorrect
combined routes; fix by using consistent, leading-slash paths — for example set
the class-level mapping to "/api/v1" and change the POST mapping to
"/diaries/{diaryId}/images" (or alternatively change class-level to
"/api/v1/diaries" and make method mappings "/{diaryId}/images"), and ensure all
mappings include the leading slash and the same "diaries" segment so POST and
DELETE routes align.

@xoruddl xoruddl merged commit 7569713 into develop Aug 26, 2025
15 of 16 checks passed
@xoruddl xoruddl deleted the 97-요청-사항-반영 branch August 27, 2025 14:33
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.

요청 사항 반영

1 participant