81 포스트 일기 상세조회 dto 수정#82
Hidden character warning
Conversation
Walkthrough포스트와 일기 상세 조회 DTO가 레코드로 재작성되고 서비스/컨트롤러에서 해당 DTO를 사용하도록 변경되었습니다. DiaryRepository가 패키지로 이동·확장(상세 조회 쿼리 추가)되었고 여러 서비스의 import 경로가 정정되었습니다. 일부 리포지토리에 메서드가 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as 사용자
participant C as DiaryController
participant S as DiaryService
participant R as DiaryRepository
participant L as LikeRepository
U->>C: GET /diaries/{id}
C->>S: getDiaryInfo(diaryId, currentUser)
S->>R: findByIdWithDetails(diaryId)
R-->>S: Diary(+user,+image,+comments+writers)
alt 공개이거나 작성자
S->>L: existsByUserAndTargetIdAndTargetType(currentUser, diaryId, "DIARY")
L-->>S: isLiked
S-->>C: DiaryInfoResponse.from(diary, isLiked)
C-->>U: 200 OK + ApiResponse<DiaryInfoResponse>
else 접근불가
S-->>C: throw AccessDeniedException
C-->>U: 403
end
sequenceDiagram
autonumber
actor U as 사용자
participant S as AvatarPostService
participant R as AvatarPostRepository
participant L as LikeRepository
U->>S: getAvatarPostInfo(postId, currentUser)
S->>R: findByIdWithDetails(postId)
R-->>S: AvatarPost(+user,+comments+writers)
S->>L: existsByUserIdAndTargetIdAndTargetType(currentUserId, postId, "AVATAR_POST")
L-->>S: isLiked
S-->>U: PostInfoResponse.from(post, isLiked)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/main/java/com/example/cp_main_be/domain/social/like/service/LikeService.java (1)
95-125: 버그: removeLike에 FEED 분기 누락
addLike에는 "feed" 분기가 있으나,removeLike에는 FEED에 대한 해제 처리가 없습니다. FEED 좋아요가 존재하는 경우 해제 호출 시 사이드이펙트 없이 0을 반환하더라도 분기 자체는 대칭적으로 존재해야 합니다(추후 로직 확장 대비).다음 패치를 추가해 최소 대응하세요(집계 변경이 없다면 0 반환 유지):
} else if ("avatar_post".equalsIgnoreCase(targetType)) { AvatarPost avatarPost = avatarPostRepository .findById(targetId) .orElseThrow(() -> new IllegalArgumentException("포스트를 찾을 수 없습니다.")); avatarPost.decreaseLikeCount(); return avatarPost.getLikeCount(); + } else if ("FEED".equalsIgnoreCase(targetType)) { + // 현재 FEED는 좋아요 수를 별도 집계하지 않는 것으로 보임 + // (addLike에서도 0을 반환). 대칭 처리만 보장. + return 0; } return 0;src/main/java/com/example/cp_main_be/global/listener/NotificationEventListener.java (2)
87-93: Optional.get() 무방비 사용 및 targetType 분기 불명확
findById(...).get()은 데이터가 없을 때NoSuchElementException을 유발합니다. 안전하게orElseThrow로 바꾸세요.targetType이"DIARY"가 아닌 모든 경우를 아바타 포스트로 간주하고 있습니다. 새로운 타입이 추가되면 잘못된 분기/알림이 발생합니다. 명시적으로 분기하고 알 수 없는 타입은 경고 후 리턴하세요.아래처럼 수정 권장:
- 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("존재하지 않는 일기입니다: id=" + targetId)); + receiver = diary.getUser(); + url = "/api/v1/diaries/" + targetId; + } else if (Objects.equals(targetType, "AVATAR_POST")) { + var post = avatarPostRepository.findById(targetId) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 아바타 포스트입니다: id=" + targetId)); + receiver = post.getUser(); + url = "/api/v1/avatar-posts/" + targetId; + } else { + log.warn("지원하지 않는 targetType입니다: {} (targetId={})", targetType, targetId); + return; + }
36-38: 이벤트 알림은 커밋 이후 전송 권장: @TransactionalEventListener(phase = AFTER_COMMIT)로 전환현재
@EventListener+@Transactional조합은 트랜잭션 롤백 시에도 알림이 전송될 수 있습니다(이벤트가 커밋 전에 발행되는 경우). 알림은 사이드 이펙트이므로 커밋 이후에 전송되도록@TransactionalEventListener(AFTER_COMMIT)로 바꾸세요.예시(패턴만 제시, 모든 핸들러에 동일 적용 권장):
- @EventListener - @Transactional - public void handleCommentCreatedEvent(CommentCreatedEvent event) { + @org.springframework.transaction.event.TransactionalEventListener( + phase = org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT) + public void handleCommentCreatedEvent(CommentCreatedEvent event) {같은 변경을 Like/Guestbook/Follow/Watering 핸들러에도 적용해 주세요.
Also applies to: 78-80, 100-102, 124-126
🧹 Nitpick comments (21)
src/main/java/com/example/cp_main_be/domain/social/comment/service/CommentService.java (1)
45-64: targetType 문자열 분기를 enum/상수 기반으로 일원화하세요현재 "DIARY", "AVATAR_POST" 문자열 비교에 의존합니다. 오타·케이스 불일치에 취약하고 전역 일관성 유지가 어렵습니다. 최소한 상수로 묶거나, 가능하면 TargetType enum을 도입해 DTO와 서비스 전반에서 타입 안정성을 보장하세요.
적용 예(선호: enum 사용):
public enum TargetType { DIARY, AVATAR_POST }선 최소 조치(본 메서드 내 문자열 상수화) — 선택 라인 범위 내 치환:
- if ("DIARY".equalsIgnoreCase(targetType)) { + if (TARGET_DIARY.equalsIgnoreCase(targetType)) { ... - } else if ("AVATAR_POST".equalsIgnoreCase(targetType)) { + } else if (TARGET_AVATAR_POST.equalsIgnoreCase(targetType)) {클래스 상단(선택 범위 외) 보조 추가:
private static final String TARGET_DIARY = "DIARY"; private static final String TARGET_AVATAR_POST = "AVATAR_POST";src/main/java/com/example/cp_main_be/domain/social/like/domain/repository/LikeRepository.java (1)
15-16: 파라미터 이름 정정 제안: 'post' → 'targetType'Spring Data JPA 파생 쿼리는 파라미터 “이름”에 의존하진 않지만, 가독성과 오해 방지를 위해 메서드 시그니처의 세 번째 파라미터 이름을 의미에 맞게 바꾸는 것이 좋습니다. 또한 13라인의
postId도 대상이 포스트로 한정되지 않는다면targetId로 정리하는 것을 권장합니다.권장 변경:
- boolean existsByUserIdAndTargetIdAndTargetType(Long id, Long postId, String post); + boolean existsByUserIdAndTargetIdAndTargetType(Long id, Long targetId, String targetType);src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java (1)
83-97: 인메모리 병합/정렬 비용 최적화 고려현재 두 소스를 (page+1)*size로 넉넉히 가져와 인메모리 정렬/슬라이싱합니다. 데이터가 커지면 GC/힙 압박 및 O(N log N) 정렬 비용이 증가합니다. 향후 스케일 고려 시:
- 커서 기반(keyset) 페이지네이션
- 각 소스에서 동일한 createdAt 상한/하한 커서 조건으로 조합 후보 축소
- DB 레벨에서 union all + 정렬(가능한 구조라면)로 오프셋/리밋 처리
를 검토하세요.src/main/java/com/example/cp_main_be/domain/social/like/service/LikeService.java (1)
33-41: targetType 문자열 상수/enum으로 일원화이 파일 내에서도 "feed"/"FEED", "DIARY"/"diary", "AVATAR_POST"/"avatar_post"가 혼재합니다.
equalsIgnoreCase로 동작은 하지만, 상수/enum으로 통일해 가독성과 안전성을 높이세요. 최소치로 상수 도입을 권장합니다.부분 치환(선택 라인 범위 내):
- if ("feed".equalsIgnoreCase(targetType)) { + if (TARGET_FEED.equalsIgnoreCase(targetType)) { ... - } else if ("DIARY".equalsIgnoreCase(targetType)) { + } else if (TARGET_DIARY.equalsIgnoreCase(targetType)) { ... - } else if ("AVATAR_POST".equalsIgnoreCase(targetType)) { + } else if (TARGET_AVATAR_POST.equalsIgnoreCase(targetType)) { ... - if ("diary".equalsIgnoreCase(targetType)) { + if (TARGET_DIARY.equalsIgnoreCase(targetType)) { ... - } else if ("avatar_post".equalsIgnoreCase(targetType)) { + } else if (TARGET_AVATAR_POST.equalsIgnoreCase(targetType)) {클래스 상단(선택 범위 외) 보조 추가:
private static final String TARGET_FEED = "FEED"; private static final String TARGET_DIARY = "DIARY"; private static final String TARGET_AVATAR_POST = "AVATAR_POST";또는 enum 도입(권장) 후 DTO/컨트롤러 계층까지 적용을 검토하세요.
Also applies to: 50-66, 72-79, 85-91, 107-115, 115-123
src/main/java/com/example/cp_main_be/global/listener/NotificationEventListener.java (1)
50-52: 알림 리다이렉트 URL 규칙 불일치: UI 경로(/diaries/…) vs API 경로(/api/v1/… ) 혼재댓글의 경우
/diaries/{id}, 좋아요는/api/v1/diaries/{id}, 방명록은/garden/{ownerId}, 물주기 알림은/garden등 서로 다른 규칙이 섞여 있어 클라이언트 라우팅/딥링크가 불안정해질 수 있습니다. 한 가지 규칙(UI 라우팅용 경로 권장)으로 통일하고, URL 빌더 헬퍼로 중복 생성을 제거하세요.예시(헬퍼 메서드 도입):
private String buildRedirectUrl(String targetType, Long targetId) { return switch (targetType) { case "DIARY" -> "/diaries/" + targetId; // UI 라우팅 기준 case "AVATAR_POST" -> "/avatar-posts/" + targetId; case "GUESTBOOK" -> "/garden/" + targetId; // 필요 시 의미에 맞게 조정 case "WATERING" -> "/garden"; default -> "/"; }; }그리고 각 이벤트 핸들러에서는
buildRedirectUrl(...)만 호출하도록 맞추면 유지보수가 수월합니다.Also applies to: 58-60, 89-93, 106-107, 129-131
src/main/java/com/example/cp_main_be/domain/social/diary/presentation/DiaryController.java (1)
50-50: TODO는 서비스에서 이미 처리됨 — 주석 정리 권장비공개 접근 제어는 서비스(
DiaryService.getDiaryInfo)에서 수행하고 있으므로 컨트롤러의 TODO는 제거하거나 "서비스에서 처리"로 업데이트하세요.- // TODO: 비공개 글일 경우 작성자만 볼 수 있도록 하는 로직 추가 필요 + // 비공개 접근 제어는 DiaryService.getDiaryInfo에서 수행됨src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java (3)
87-89: 좋아요 조회는 userId 기반 시그니처가 더 효율적현재
existsByUserAndTargetIdAndTargetType(User, Long, String)를 사용하고 있습니다. 가능하다면existsByUser_IdAndTargetIdAndTargetType(Long, Long, String)형태로 바꿔 조인 비용을 줄이세요. 레포 메서드가 없다면 추가를 검토해 주세요.
66-68: 리소스 미존재 예외 매핑 재검토(404 권장)
IllegalArgumentException("일기를 찾을 수 없습니다.")는 보통 400으로 매핑될 수 있습니다. REST语의 관점에서는 404가 더 적절합니다.ResponseStatusException(HttpStatus.NOT_FOUND, "...")또는 전용NotFoundException+@ControllerAdvice매핑을 제안합니다.
80-83: SecurityException 대신 Spring 표준 예외 사용 제안보안 컨텍스트와의 일관성을 위해
SecurityException대신AccessDeniedException(spring-security)을 사용하면 전역 예외 처리/필터 체인과의 통합이 수월합니다. 위 주요 코멘트의 diff에 반영했습니다.src/main/java/com/example/cp_main_be/domain/social/avatarpost/service/AvatarPostService.java (4)
24-24: 예외 타입을 도메인 의미에 맞게 구체화 제안
IllegalArgumentException대신 404 매핑이 되는EntityNotFoundException(혹은 커스텀NotFoundException) 사용을 고려해 주세요. 컨트롤러 어드바이스에서 404로 일괄 변환하기도 수월합니다.
적용 예시:- .orElseThrow(() -> new IllegalArgumentException("해당 포스트를 찾을 수 없습니다.")); + .orElseThrow(() -> new EntityNotFoundException("AvatarPost not found: id=" + postId));
29-30: 타겟 타입 문자열 리터럴 제거 제안하드코딩 문자열
"AVATAR_POST"는 오타·변경에 취약합니다.TargetType.AVATAR_POST와 같은 enum을 도입하고, 리포지토리 시그니처도 enum을 받도록 정리하는 것을 권장합니다.예시(호출부):
- likeRepository.existsByUserIdAndTargetIdAndTargetType(currentUser.getId(), postId, "AVATAR_POST"); + likeRepository.existsByUserIdAndTargetIdAndTargetType(currentUser.getId(), postId, TargetType.AVATAR_POST);
21-22:var사용은 JDK 버전 의존적 — 프로젝트 소스/타깃 호환성 확인
var는 JDK 10+에서만 지원됩니다. 빌드 설정(maven-compiler-plugin의maven.compiler.release또는 Gradle의sourceCompatibility/targetCompatibility)이 이를 보장하는지 확인 바랍니다. 팀 컨벤션에 따라 명시적 타입 사용을 선호한다면 교체를 고려해 주세요.
29-30: LikeRepository 메서드 시그니처(User vs. userId) 통일 및 하드코딩된 타입 문자열 enum 대체 권장
- LikeRepository 인터페이스(src/main/java/com/example/cp_main_be/domain/social/like/domain/repository/LikeRepository.java)에는
• boolean existsByUserAndTargetIdAndTargetType(User user, Long targetId, String targetType)
• boolean existsByUserIdAndTargetIdAndTargetType(Long id, Long postId, String postType)
두 가지 오버로드된 메서드가 공존하고 있습니다.- AvatarPostService(src/main/java/com/example/cp_main_be/domain/social/avatarpost/service/AvatarPostService.java:29–30)
→ existsByUserIdAndTargetIdAndTargetType(currentUser.getId(), postId, "AVATAR_POST") 사용- DiaryService(src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java:88–89)
→ existsByUserAndTargetIdAndTargetType(currentUser, diaryId, "DIARY") 사용위와 같은 혼재는 유지보수성과 가독성을 저해하므로, 다음과 같이 리팩터링을 권장드립니다.
- LikeRepository 내 메서드를 하나의 시그니처로 통일 (
User객체 또는userId중 하나 선택)- 내부에 공통 로직(private 메서드)으로 분리해 중복 제거
"AVATAR_POST","DIARY"등의 하드코딩된 문자열을 enum 타입(e.g.TargetType.AVATAR_POST,TargetType.DIARY)으로 대체src/main/java/com/example/cp_main_be/domain/social/diary/dto/response/DiaryInfoResponse.java (4)
15-15: boolean 직렬화 키 일관성 확보 제안 (isLiked)
isPublic에는@JsonProperty를 부여했지만isLiked에는 미부여입니다. Jackson이 record 구성요소 이름을 기본 키로 쓰긴 하나, 팀/클라이언트 측에서 “isLiked” 키를 계약으로 고정하려면 명시를 권장합니다.- boolean isLiked, + @JsonProperty("isLiked") boolean isLiked,
24-27: 댓글 정렬 보장 필요 가능성 (UI 일관성)현재
diary.getComments().stream()순서는 DB/컬렉션 기본 순서에 의존합니다. 작성일 ASC(또는 DESC) 요구가 있으면 여기서 명시적으로 정렬해 주세요.- List<CommentResponseDTO> commentDTOs = - diary.getComments().stream().map(CommentResponseDTO::from).toList(); + List<CommentResponseDTO> commentDTOs = + diary.getComments().stream() + .sorted(java.util.Comparator.comparing(c -> c.getCreatedAt())) + .map(CommentResponseDTO::from) + .toList();
45-51: null-안전성 확인(작성자/프로필 이미지)
comment.getWriter()또는getProfileImageUrl()가 null일 가능성이 도메인 제약상 완전히 배제되는지 확인해 주세요. 만약 탈퇴 사용자/이미지 미등록 케이스가 존재하면 NPE를 방지하는 null 안전 매핑이 필요합니다.예시:
- return new CommentResponseDTO( - comment.getId(), - comment.getWriter().getProfileImageUrl(), - comment.getWriter().getNickname(), - comment.getContent()); + var writer = comment.getWriter(); + return new CommentResponseDTO( + comment.getId(), + writer != null ? writer.getProfileImageUrl() : null, + writer != null ? writer.getNickname() : "알 수 없음", + comment.getContent());
9-53: 중복 DTO 구조의 공통화 제안 (댓글 응답 구조)
PostInfoResponse.CommentResponseDTO와 동일 구조가 반복됩니다. 공용 패키지에 재사용 가능한CommentResponseDTO를 두거나, 매퍼 유틸(예: MapStruct)로 중복 제거를 고려해 주세요.src/main/java/com/example/cp_main_be/domain/social/avatarpost/dto/PostInfoResponse.java (4)
22-25: 댓글 리스트 생성 시 정렬 고려Diary와 동일하게 댓글 표시 순서 요구가 있다면 여기서 정렬을 명시하세요.
- List<CommentResponseDTO> commentDTOs = - post.getComments().stream().map(CommentResponseDTO::from).toList(); + List<CommentResponseDTO> commentDTOs = + post.getComments().stream() + .sorted(java.util.Comparator.comparing(c -> c.getCreatedAt())) + .map(CommentResponseDTO::from) + .toList();
14-15:isLiked직렬화 키 고정 일관성
DiaryInfoResponse와 동일하게@JsonProperty("isLiked")를 부여하여 직렬화 키를 계약으로 고정하는 것을 권장합니다.- boolean isLiked, + @JsonProperty("isLiked") boolean isLiked,
41-49: 댓글 DTO 매핑 로직 일관성 좋음 — 공통화 고려다이어리와 동일 구조의 매핑을 사용하고 있어 클라이언트 일관성에 유리합니다. 중복 제거는 별도 공통 DTO/매퍼로의 추출로 해결 가능합니다.
26-38: AvatarPost에 가시성 필드가 없으므로true하드코딩은 현재 타당합니다.
다만, 추후 공개/비공개 기능이 도입될 가능성을 대비해 아래를 적용해 주세요.
PostInfoResponse.from메서드의 하드코딩 부분에 TODO 주석 추가true // AvatarPost는 항상 public이라고 가정
true // AvatarPost는 항상 public (추후 가시성 필드 추가 시 매핑 필요) // TODO
- PR 설명에 “AvatarPost 도메인에는 가시성 필드가 없어 현재 모든 포스트를 공개로 처리함”을 명시
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (13)
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/avatarpost/domain/repository/AvatarPostRepository.java(1 hunks)src/main/java/com/example/cp_main_be/domain/social/avatarpost/dto/PostInfoResponse.java(1 hunks)src/main/java/com/example/cp_main_be/domain/social/avatarpost/service/AvatarPostService.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/diary/domain/Repository/DiaryRepository.java(2 hunks)src/main/java/com/example/cp_main_be/domain/social/diary/dto/response/DiaryInfoResponse.java(1 hunks)src/main/java/com/example/cp_main_be/domain/social/diary/presentation/DiaryController.java(2 hunks)src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java(2 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/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/listener/NotificationEventListener.java(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/example/cp_main_be/domain/social/avatarpost/service/AvatarPostService.java (2)
src/main/java/com/example/cp_main_be/domain/social/comment/service/CommentService.java (1)
Service(21-121)src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java (1)
Service(24-213)
🔇 Additional comments (18)
src/main/java/com/example/cp_main_be/domain/social/comment/service/CommentService.java (1)
13-13: DiaryRepository import 경로 정정: 문제 없음패키지 구조 변경에 따른 import 수정으로 보이며, 동작 영향은 없습니다.
src/main/java/com/example/cp_main_be/domain/social/like/domain/repository/LikeRepository.java (1)
15-16: existsByUserAndTargetIdAndTargetType 오버로드 사용처 검토 결과
existsByUserAndTargetIdAndTargetType(User, Long, String)
- LikeService.java (line 39)
- DiaryService.java (line 88)
existsByUserIdAndTargetIdAndTargetType(Long, Long, String)
- AvatarPostService.java (line 29)
위 두 가지 오버로드는 각각 엔티티 기반 호출과 ID 기반 호출이라는 분명한 용도가 있고, 실제로 모두 사용되고 있으므로 중복이나 미사용 메서드가 아닙니다. 불필요한 API 제거는 필요하지 않습니다.
src/main/java/com/example/cp_main_be/domain/social/feed/service/FeedService.java (1)
7-7: DiaryRepository import 경로 정정: 문제 없음패키지 이동에 따른 import 수정이며, 동작 영향은 없습니다.
src/main/java/com/example/cp_main_be/domain/reports/service/ReportService.java (1)
14-14: DiaryRepository import 경로 정정: 문제 없음패키지 이동에 따른 import 수정으로 보이며, 동작 영향은 없습니다.
src/main/java/com/example/cp_main_be/domain/social/like/service/LikeService.java (2)
10-10: DiaryRepository import 경로 정정: 문제 없음패키지 이동에 따른 import 수정이며, 로직 영향은 없습니다.
39-49: 중복 좋아요 유니크 제약 확인 완료
src/main/java/com/example/cp_main_be/domain/social/like/domain/Like.java의@Table애노테이션에
uniqueConstraints = @UniqueConstraint(name = "uk_likes_user_target", columnNames = {"user_id", "target_id", "target_type"})가 정의되어 있어
user_id + target_id + target_type 조합에 대한 유니크 제약이 존재합니다.이로써 경쟁 상태에서도 중복 삽입을 방지할 수 있는 DB 레벨 유니크 제약이 확인되었습니다.
src/main/java/com/example/cp_main_be/global/listener/NotificationEventListener.java (2)
10-10: DiaryRepository import 경로 정정 LGTM패키지 이동(
...domain.repository) 반영이 정확합니다. 추가 작업 없이 컴포넌트 스캔에도 영향 없어 보입니다.
29-29: 로거 필드 위치 변경 무관하지만 유지보수 관점에서 OK로거를 상단으로 올린 건 가독성에 도움 됩니다. 기능 영향은 없습니다.
src/main/java/com/example/cp_main_be/domain/social/diary/presentation/DiaryController.java (2)
47-52: DTO 교체(DiaryInfoResponse) 및 서비스 시그니처 연동 LGTM상세 조회가
DiaryInfoResponse로 일원화되면서 컨트롤러-서비스 간 계약이 명확해졌습니다. 반환 타입 변경도 적절합니다.
47-52: API 응답 스키마 변경 영향도 확인 필요 (DiaryInfoResponse 적용)
rg -nP -C2 --type=java '\bPostInfoResponse\b|ApiResponse<\s*PostInfoResponse\s*>'명령을 실행한 결과,PostInfoResponse는avatarpost관련 코드에만 남아있고diary패키지에는 잔존 사용처가 없음을 확인했습니다.
다음 항목들도 반드시 점검 부탁드립니다:
- 모바일/Web 클라이언트
·getDiaryDetail엔드포인트 호출부가DiaryInfoResponse반환 스키마로 업데이트되었는지- API 문서 (Swagger)
· OpenAPI 정의에DiaryInfoResponse가 정상 반영되었는지- 스냅샷 테스트
· 변경된 스키마에 맞춰 테스트 스냅샷이 갱신되었는지src/main/java/com/example/cp_main_be/domain/social/diary/domain/Repository/DiaryRepository.java (1)
1-1: 패키지 이동(repository 소문자) 반영 LGTM패키지 컨벤션(
repository)으로 정리된 점 좋습니다. 관련 import 업데이트도 일관적입니다.src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java (1)
88-89: AI 요약과 구현이 상이: existsByUserId… vs existsByUser…AI 요약에는
existsByUserIdAndTargetIdAndTargetType사용으로 기재되어 있으나 실제 코드는existsByUserAndTargetIdAndTargetType입니다. 문서/PR 설명을 코드와 일치하도록 수정하거나, 필요 시 구현을 userId 버전으로 교체하세요.src/main/java/com/example/cp_main_be/domain/social/avatarpost/service/AvatarPostService.java (3)
12-12: 클래스 레벨 read-only 트랜잭션 적용 좋습니다조회 전용 서비스에
@Transactional(readOnly = true)를 부여해 JPA 플러시·더티체킹 비용을 줄입니다.
21-24: N+1 회피 목적의 fetch 메서드 사용은 적절합니다만, 자식 연관(댓글 작성자 등)까지 실제 fetch 되는지 확인 필요
findByIdWithDetails(postId)가 댓글과 댓글 작성자까지 fetch join 혹은 배치 사이즈로 로딩하는지 확인해 주세요. 컬렉션 fetch join이 다수일 경우 카티전 폭발 위험도 있으므로 필요 컬렉션만 선택적으로 fetch 하거나 배치 사이즈를 권장합니다.
원하시면 쿼리 힌트와 함께 리포지토리 JPQL을 점검해 드리겠습니다.
33-33: DTO 팩토리 메서드로의 위임이 가독성과 응집도 면에서 적절합니다서비스 단의 매핑 로직을 DTO 정적 팩토리로 위임한 방향성 좋습니다. 동일 패턴을 다른 응답 DTO에도 일관되게 적용하면 유지보수성이 높아집니다.
src/main/java/com/example/cp_main_be/domain/social/diary/dto/response/DiaryInfoResponse.java (2)
9-21: Record 기반 응답 DTO 전환 좋습니다불변·명세 중심의 응답 모델로 명확하며 필드 구성이 직관적입니다.
@JsonProperty("isPublic")로 boolean 직렬화 키를 고정한 점도 👍
28-41: 카운트 계산은 DTO 리스트 크기 활용 — 일관성 OK
commentCount를commentDTOs.size()로 채우는 선택은 일관성이 있습니다. 성능 최적화가 필요하면 도메인에 집계 필드/프로젝션을 도입하는 대안도 가능합니다.src/main/java/com/example/cp_main_be/domain/social/avatarpost/dto/PostInfoResponse.java (1)
9-21: Record 기반 전환 및 필드 구성 적절불변 DTO와 명시적 필드로 응답 스키마가 명확합니다.
@JsonProperty("isPublic")지정도 좋습니다.
| @Query( | ||
| "SELECT ap FROM AvatarPost ap " | ||
| + "LEFT JOIN FETCH ap.user u " // 포스트 작성자 fetch | ||
| + "LEFT JOIN FETCH ap.comments c " // 댓글 목록 fetch | ||
| + "LEFT JOIN FETCH c.writer cw " // 댓글 작성자 fetch | ||
| + "WHERE ap.id = :postId") | ||
| Optional<AvatarPost> findByIdWithDetails(@Param("postId") Long postId); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
컬렉션 fetch join 시 DISTINCT 누락 — 중복 행으로 단일 결과 쿼리 실패 위험
LEFT JOIN FETCH ap.comments c가 있으므로 동일 ap.id에 대해 다중 행이 발생합니다. Spring Data가 단건/Optional 매핑을 수행할 때 IncorrectResultSizeDataAccessException이 날 수 있어 DISTINCT를 추가해야 합니다.
- @Query(
- "SELECT ap FROM AvatarPost ap "
+ @Query(
+ "SELECT DISTINCT ap FROM AvatarPost ap "
+ "LEFT JOIN FETCH ap.user u "
+ "LEFT JOIN FETCH ap.comments c "
+ "LEFT JOIN FETCH c.writer cw "
+ "WHERE ap.id = :postId")추가로, 댓글 정렬이 필요하다면 쿼리 ORDER BY c.createdAt 대신, 엔티티의 @OneToMany(mappedBy="...") 쪽에 @OrderBy("createdAt ASC")를 부여하는 접근을 권장합니다(페치 조인과 정렬 충돌 회피).
📝 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.
| @Query( | |
| "SELECT ap FROM AvatarPost ap " | |
| + "LEFT JOIN FETCH ap.user u " // 포스트 작성자 fetch | |
| + "LEFT JOIN FETCH ap.comments c " // 댓글 목록 fetch | |
| + "LEFT JOIN FETCH c.writer cw " // 댓글 작성자 fetch | |
| + "WHERE ap.id = :postId") | |
| Optional<AvatarPost> findByIdWithDetails(@Param("postId") Long postId); | |
| @Query( | |
| "SELECT DISTINCT ap FROM AvatarPost ap " | |
| "LEFT JOIN FETCH ap.user u " // 포스트 작성자 fetch | |
| "LEFT JOIN FETCH ap.comments c " // 댓글 목록 fetch | |
| "LEFT JOIN FETCH c.writer cw " // 댓글 작성자 fetch | |
| "WHERE ap.id = :postId") | |
| Optional<AvatarPost> findByIdWithDetails(@Param("postId") Long postId); |
🤖 Prompt for AI Agents
In
src/main/java/com/example/cp_main_be/domain/social/avatarpost/domain/repository/AvatarPostRepository.java
around lines 15 to 21, the JPQL should use DISTINCT to avoid duplicate rows when
fetch-joining the comments collection; change the query to select DISTINCT ap
(e.g., "SELECT DISTINCT ap FROM AvatarPost ap ...") so Optional mapping won't
fail. Also, if you need comments ordered, do not rely on ORDER BY in the
fetch-join; instead add @OrderBy("createdAt ASC") on the AvatarPost.comments
collection in the AvatarPost entity to enforce stable ordering while avoiding
fetch-join sort issues.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/main/java/com/example/cp_main_be/domain/social/avatarpost/domain/repository/AvatarPostRepository.java (1)
15-21: DISTINCT 추가로 컬렉션 fetch join 중복 방지 — 이전 코멘트 반영 OK
LEFT JOIN FETCH ap.comments c가 있는 단건 조회에서SELECT DISTINCT ap로 루트 엔티티 중복을 제거해Optional매핑 오류 위험을 줄였습니다. 지난 리뷰의 지적 사항이 정확히 반영되었습니다.src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java (1)
80-86: 비공개 접근 제어와 null 사용자 처리 — 이전 리뷰 이슈가 정확히 해소되었습니다currentUser null 안전성 및 비공개 글의 접근 제어가 올바르게 반영되었습니다. 403을 기대하는 클라이언트와의 계약에도 부합합니다.
🧹 Nitpick comments (7)
src/main/java/com/example/cp_main_be/domain/social/avatarpost/domain/repository/AvatarPostRepository.java (4)
15-21: 댓글 정렬 일관성이 필요하다면 엔티티 측 @orderby 권장fetch join에
ORDER BY를 섞으면 예기치 않은 동작/경고가 날 수 있습니다. 댓글 표시 순서를 보장해야 한다면AvatarPost엔티티의 컬렉션에 정렬을 선언하세요.
- 권장:
@OrderBy("createdAt ASC")(또는 필요한 정렬 키)엔티티 측 변경 예시(참고용):
// AvatarPost.java @OneToMany(mappedBy = "post") @OrderBy("createdAt ASC") private List<Comment> comments;
15-21: 불필요한 별칭 정리로 JPQL 가독성 향상 (경미)
u,cw별칭을 추가로 사용하지 않으므로 제거하면 가독성이 조금 좋아집니다. 기능 변화는 없습니다.- "SELECT DISTINCT ap FROM AvatarPost ap " - + "LEFT JOIN FETCH ap.user u " // 포스트 작성자 fetch - + "LEFT JOIN FETCH ap.comments c " // 댓글 목록 fetch - + "LEFT JOIN FETCH c.writer cw " // 댓글 작성자 fetch + "SELECT DISTINCT ap FROM AvatarPost ap " + + "LEFT JOIN FETCH ap.user " + + "LEFT JOIN FETCH ap.comments c " + + "LEFT JOIN FETCH c.writer " + "WHERE ap.id = :postId")
15-21: 대용량 댓글이 예상된다면 카티전 곱으로 인한 row 폭증 가능성 점검단건 조회라도 댓글 수가 매우 많으면
ap × comments × writer조인으로 ResultSet이 커질 수 있습니다. 제품 특성상 댓글 수가 커질 가능성이 있으면 아래 대안을 검토해주세요.
- 대안 A: 댓글은 별도 API 또는 지연 로딩 +
@BatchSize/hibernate.default_batch_fetch_size로 N+1 완화- 대안 B:
@EntityGraph(attributePaths = {"user"})+ 댓글은 페이지네이션 조회필요 시 성능 추적 포인트를 추가해 평균/최대 댓글 수를 계측하는 것도 권장합니다.
15-21: Java 텍스트 블록으로 JPQL 가독성 개선 (JDK 15+ 환경일 때만)멀티라인 문자열을 텍스트 블록으로 바꾸면 주석 없이도 읽기 쉬워집니다. 기능 변화는 없습니다.
- @Query( - "SELECT DISTINCT ap FROM AvatarPost ap " - + "LEFT JOIN FETCH ap.user " - + "LEFT JOIN FETCH ap.comments c " - + "LEFT JOIN FETCH c.writer " - + "WHERE ap.id = :postId") + @Query(""" + SELECT DISTINCT ap FROM AvatarPost ap + LEFT JOIN FETCH ap.user + LEFT JOIN FETCH ap.comments c + LEFT JOIN FETCH c.writer + WHERE ap.id = :postId + """)src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java (3)
87-91: 좋아요 exists 조회: 엔티티 파라미터/매직 스트링 → ID 기반 + Enum 전환 제안
- 현재 구현은 엔티티 파라미터(User)와 매직 스트링("DIARY")에 의존합니다. ID 기반 existsBy... 시그니처와 Enum을 사용하면 JPA 바인딩/평가가 단순해지고 오타 리스크가 줄어듭니다.
적용 예시(레포지토리 시그니처를 추가/변경하는 경우):
- isLiked = likeRepository.existsByUserAndTargetIdAndTargetType(currentUser, diaryId, "DIARY"); + isLiked = likeRepository.existsByUserIdAndTargetIdAndTargetType( + currentUser.getId(), diaryId, LikeTargetType.DIARY);레포지토리/타입 정의(파일 외 보조 변경):
// LikeRepository boolean existsByUserIdAndTargetIdAndTargetType(Long userId, Long targetId, LikeTargetType targetType); // LikeTargetType public enum LikeTargetType { DIARY /*, POST, ...*/ }운영 성능 팁: likes 테이블에 (user_id, target_type, target_id) 복합 인덱스가 없다면 추가를 고려해 주세요.
73-95: 핵심 경계 조건 테스트 케이스 보강 제안이 메서드의 리그레션 방지를 위해 아래 테스트를 권장합니다.
- 공개 글 + 비로그인: 200 OK, isLiked=false
- 비공개 글 + 비로그인: 403
- 비공개 글 + 작성자 본인: 200 OK
- 공개/비공개 글 + 로그인 타 사용자: 403(비공개), 200(공개)
- 로그인 사용자 + 좋아요 존재/부재: isLiked true/false 분기 검증
원하시면 컨트롤러 통합 테스트(WebMvcTest/RestDocs 포함)와 서비스 단위 테스트 샘플을 생성해 드리겠습니다.
77-78: findByIdWithDetails 페치 조인 쿼리 최적화 권장
- DiaryRepository.findByIdWithDetails 메서드(@Param("diaryId") Long diaryId)에는
와 같이 작성되어 있어, 페치 조인만으로 연관 엔티티를 로딩하고 있습니다. 이 경우 댓글이 여러 개일 때 SQL 결과에 같은 Diary 엔티티가 중복으로 노출될 수 있으므로, 아래 중 하나를 적용해 중복 제거를 권장드립니다.@Query("SELECT d FROM Diary d " + "LEFT JOIN FETCH d.comments c " + "LEFT JOIN FETCH c.writer cw " + "WHERE d.id = :diaryId")
- JPQL에 DISTINCT 추가
@Query("SELECT DISTINCT d FROM Diary d " + "LEFT JOIN FETCH d.comments c " + "LEFT JOIN FETCH c.writer cw " + "WHERE d.id = :diaryId")- @EntityGraph(@NamedEntityGraph) 적용
@EntityGraph(attributePaths = {"comments", "comments.writer"}) Optional<Diary> findByIdWithDetails(@Param("diaryId") Long diaryId);- Diary 작성자(d.user)도 조회 시점에 자주 사용된다면, 추가 페치 조인 대상에 포함하거나 트랜잭션(readOnly = true) 내에서 안전히 lazy 로드되는지 재확인해 주세요 (현재 getDiaryInfo는 @transactional(readOnly = true)로 감싸져 있어, 같은 트랜잭션 내에서 lazy 로드가 가능합니다).
위 개선을 통해 중복 로우에 의한 장애를 예방하고, 필요한 연관만 효율적으로 로딩할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
src/main/java/com/example/cp_main_be/domain/social/avatarpost/domain/repository/AvatarPostRepository.java(1 hunks)src/main/java/com/example/cp_main_be/domain/social/diary/domain/Repository/DiaryRepository.java(2 hunks)src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/example/cp_main_be/domain/social/diary/domain/Repository/DiaryRepository.java
🔇 Additional comments (4)
src/main/java/com/example/cp_main_be/domain/social/avatarpost/domain/repository/AvatarPostRepository.java (2)
10-10: 굳 — @Param 추가로 이름 기반 바인딩 안정화
@Query의 네임드 파라미터(:postId)와 일치하도록@Param("postId")를 사용한 점 좋습니다. 리플렉션 파라미터 이름 보존 여부와 무관하게 안정적으로 동작합니다.
21-21: 메서드명 변경 완료 확인
findByIdWithComments호출은 더 이상 코드베이스 내에서 발견되지 않습니다.findByIdWithDetails호출이 DiaryService, AvatarPostService, 각 Repository 파일에서 정상적으로 반영된 것을 확인했습니다.추가 조치가 필요 없으므로 이 검토 요청을 닫습니다.
src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java (2)
73-79: DTO 반환 시그니처 변경 + fetch-join 기반 조회로 N+1 완화 — 좋습니다메서드 시그니처를 DiaryInfoResponse로 명확히 하고, findByIdWithDetails로 연관 로딩을 한 번에 처리한 점이 합리적입니다.
93-95: DTO 팩토리 위임은 적절합니다지연 로딩 컬렉션을 순회하지 않는 선에서 필요한 필드만 접근하면 성능 안정성이 확보됩니다. from(...) 내부에서 추가 쿼리 유발 여부만 유의해 주세요.
| import com.example.cp_main_be.domain.social.avatarpost.dto.PostInfoResponse; | ||
| import com.example.cp_main_be.domain.social.diary.domain.Diary; | ||
| import com.example.cp_main_be.domain.social.diary.domain.Repository.DiaryRepository; | ||
| import com.example.cp_main_be.domain.social.diary.domain.repository.DiaryRepository; |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
AccessDeniedException 도입 및 Repository 경로 정정은 적절합니다 — 예외 타입 일관화만 추가 제안
- AccessDeniedException 추가로 보안语义가 명확해졌습니다.
- 동일 파일 내 update/delete 경로(예: Line 107, 142)의 SecurityException도 AccessDeniedException으로 통일 시 HTTP 403 매핑 일관성이 좋아집니다. 글로벌 예외 핸들러가 403으로 매핑되는지도 함께 확인 부탁드립니다.
예외 매핑 및 레포지토리 선언을 점검하려면 아래 스크립트를 실행하세요.
Also applies to: 10-10, 19-19
🏁 Script executed:
#!/bin/bash
# AccessDeniedException -> 403 매핑 여부 확인
rg -nC3 '@ControllerAdvice|@RestControllerAdvice'
rg -nC3 '@ExceptionHandler\s*\(\s*AccessDeniedException\.class\s*\)'
# SecurityException 사용 위치 확인(교체 후보)
rg -nC2 'throw new SecurityException\(' src/main/java
# DiaryRepository 패키지/선언 확인
rg -nC2 'interface\s+DiaryRepository\b' src/main/javaLength of output: 2888
아래 스크립트를 실행해 GlobalExceptionHandler에서 AccessDeniedException 핸들러 매핑이 있는지, 그리고 DiaryRepository 파일의 패키지 선언(디렉토리명 대소문자 일치 여부)을 확인해 주세요.
🏁 Script executed:
#!/bin/bash
# 1. GlobalExceptionHandler에서 AccessDeniedException 매핑 여부 확인
grep -n '@ExceptionHandler.*AccessDeniedException' src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java || echo "매핑 없음"
# 2. DiaryRepository 패키지 선언 및 디렉토리명 확인
head -n 10 src/main/java/com/example/cp_main_be/domain/social/diary/domain/Repository/DiaryRepository.javaLength of output: 722
GlobalExceptionHandler에서 AccessDeniedException 및 SecurityException 매핑을 확인하기 위해 아래 스크립트를 실행해 주세요.
🏁 Script executed:
#!/bin/bash
# GlobalExceptionHandler 내 ExceptionHandler 어노테이션 확인
rg -n '@ExceptionHandler' -C2 src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.javaLength of output: 1148
GlobalExceptionHandler에 AccessDeniedException 매핑 및 Repository 디렉토리명 수정 필요
아래 작업을 통해 보안 예외 일관성과 패키지 경로 일치를 확보해주세요.
- GlobalExceptionHandler.java에 AccessDeniedException 전용 핸들러 추가
- 파일:
src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java - 예시:
@ExceptionHandler(AccessDeniedException.class) public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException e) { ErrorResponse response = ErrorResponse.of(HttpStatus.FORBIDDEN, e.getMessage()); return new ResponseEntity<>(response, HttpStatus.FORBIDDEN); }
- 파일:
- DiaryService의 SecurityException → AccessDeniedException 교체
- 파일:
src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java
• Line 107:throw new SecurityException("일기를 수정할 권한이 없습니다.");
• Line 142:throw new SecurityException("일기를 삭제할 권한이 없습니다."); - 수정안 예시:
throw new AccessDeniedException("일기를 수정할 권한이 없습니다.");
- 파일:
- DiaryRepository 디렉토리명 소문자화
- 경로 수정 전:
src/main/java/com/example/cp_main_be/domain/social/diary/domain/Repository/DiaryRepository.java - 수정 후:
src/main/java/com/example/cp_main_be/domain/social/diary/domain/repository/DiaryRepository.java
- 경로 수정 전:
위 세 가지 이슈를 반영하신 뒤, 글로벌 핸들러가 의도한 대로 403 응답을 반환하는지 통합 테스트를 통해 확인해 주세요.
🤖 Prompt for AI Agents
In src/main/java/com/example/cp_main_be/global/GlobalExceptionHandler.java (add
new handler) and
src/main/java/com/example/cp_main_be/domain/social/diary/service/DiaryService.java
(lines ~107 and ~142) and the repository path, add an @ExceptionHandler for
AccessDeniedException that returns ErrorResponse with HttpStatus.FORBIDDEN;
replace the two throws of SecurityException in DiaryService with throws of
AccessDeniedException using the same messages; and rename/move the
DiaryRepository file from .../domain/Repository/DiaryRepository.java to
.../domain/repository/DiaryRepository.java and update any imports to the new
package so the project compiles and the global handler returns 403 as intended.
📝 개요
이번 PR의 핵심 내용을 한 줄로 요약해 주세요.
💻 작업 내용
이번 PR에서 작업한 내용을 상세히 설명해 주세요.
작업 내용 1
작업 내용 2
...
✅ PR 체크리스트
PR을 보내기 전에 아래 체크리스트를 확인해 주세요.
커밋 메시지는 포맷에 맞게 작성했나요?
스스로 코드를 다시 한번 검토했나요?
관련 이슈를 연결했나요?
빌드 및 테스트가 로컬에서 성공했나요?
🔗 관련 이슈
이번 PR과 관련된 이슈 번호를 기재해 주세요. 예: Closes #81
스크린샷 (선택)
UI 변경 사항이 있다면 스크린샷을 첨부해 주세요.
Summary by CodeRabbit
New Features
Refactor
Chores