Conversation
WalkthroughComment 및 Post 도메인(컨트롤러, 서비스, 리포지토리, 엔티티, DTO, 매퍼)과 전역 페이징/무한스크롤 응답·매퍼가 추가되었고, S3 경로 분리(PathName·S3Config) 및 S3ServiceImpl 매핑이 변경되었으며 AuthServiceImpl에 PasswordEncoder 주입 및 테스트 사용자 자동 생성 로직이 추가되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant PostController
participant PostService
participant S3Service
participant PostMapper
participant PostRepository
participant Database
rect rgb(200,220,255)
Note over Client,Database: 게시물 생성 흐름 (이미지 포함)
Client->>PostController: POST /api/posts/new + PostRequest + images
PostController->>PostService: createPost(request, images)
PostService->>S3Service: uploadImages(images, PathName.POST)
S3Service-->>PostService: imageUrlList
PostService->>PostMapper: toPost(request, imageUrlList, user)
PostMapper-->>PostService: Post entity
PostService->>PostRepository: save(post)
PostRepository->>Database: INSERT post
Database-->>PostRepository: saved post
PostRepository-->>PostService: saved post
PostService->>PostMapper: toDetailResponse(post, user)
PostMapper-->>PostService: PostDetailResponse
PostService-->>PostController: PostDetailResponse
PostController-->>Client: 200 OK (BaseResponse<PostDetailResponse>)
end
sequenceDiagram
participant Client
participant PostController
participant PostService
participant PostRepository
participant Database
participant InfiniteMapper
participant PostMapper
rect rgb(255,240,200)
Note over Client,Database: 카테고리 기반 무한 스크롤 조회
Client->>PostController: GET /api/posts?category=...&lastPostId=...&size=...
PostController->>PostService: getPostsByCategory(category, lastPostId, size)
PostService->>PostRepository: findByCategoryListContainingAndIdLessThan(category, lastPostId, pageable)
PostRepository->>Database: SELECT ... (페이징 쿼리)
Database-->>PostRepository: Page<Post>
PostRepository-->>PostService: Page<Post>
loop 각 게시물
PostService->>PostMapper: toDetailResponse(post, user)
PostMapper-->>PostService: PostDetailResponse
end
PostService->>InfiniteMapper: toInfiniteResponse(contents, lastCursor, hasNext, size)
InfiniteMapper-->>PostService: InfiniteResponse
PostService-->>PostController: InfiniteResponse
PostController-->>Client: 200 OK (BaseResponse<InfiniteResponse>)
end
sequenceDiagram
participant Client
participant CommentController
participant CommentService
participant PostRepository
participant CommentMapper
participant CommentRepository
participant Database
rect rgb(220,255,220)
Note over Client,Database: 댓글 생성 흐름
Client->>CommentController: POST /api/comments/new?postId=... + CommentRequest
CommentController->>CommentService: createComment(request, postId)
CommentService->>PostRepository: findById(postId)
PostRepository->>Database: SELECT post
Database-->>PostRepository: Post (or null)
PostRepository-->>CommentService: Post
CommentService->>CommentMapper: toComment(request, user, post)
CommentMapper-->>CommentService: Comment entity
CommentService->>CommentRepository: save(comment)
CommentRepository->>Database: INSERT comment
Database-->>CommentRepository: saved comment
CommentRepository-->>CommentService: saved comment
CommentService->>CommentMapper: toDetailResponse(comment, user)
CommentMapper-->>CommentService: CommentDetailResponse
CommentService-->>CommentController: CommentDetailResponse
CommentController-->>Client: 200 OK (BaseResponse<CommentDetailResponse>)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🧰 Additional context used🧬 Code graph analysis (1)src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java (2)
🔇 Additional comments (4)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 15
🧹 Nitpick comments (12)
src/main/java/com/sku/refit/domain/auth/service/AuthServiceImpl.java (1)
103-118: 테스트 사용자 자동 생성 로직 승인테스트 로그인 시 사용자가 없으면 자동으로 생성하는 로직이 적절하게 구현되었습니다.
PasswordEncoder를 사용하여 비밀번호를 안전하게 인코딩하고 있습니다.한 가지 개선 제안:
orElse(null)후 null 체크 대신Optional의ifPresentOrElse또는orElseGet을 활용하면 더 간결해질 수 있습니다.- User user = userRepository.findByUsername(testUsername).orElse(null); - - if (user == null) { - user = - User.builder() + User user = userRepository.findByUsername(testUsername).orElseGet(() -> { + User newUser = User.builder() .profileImageUrl(testUsername + ".png") .nickname("김다입") .username(testUsername) .password(passwordEncoder.encode(testPassword)) .locationConsent(true) .role(Role.ROLE_USER) .build(); - - userRepository.save(user); - log.info("테스트 사용자 생성됨: {}", user.getUsername()); - } + userRepository.save(newUser); + log.info("테스트 사용자 생성됨: {}", newUser.getUsername()); + return newUser; + });src/main/java/com/sku/refit/domain/comment/entity/Comment.java (1)
41-47: EAGER 페치 타입으로 인한 성능 문제 가능성
user와post관계 모두FetchType.EAGER를 사용하고 있습니다. 관련 코드 스니펫에서Post엔티티도commentList에 EAGER 페치를 사용하는 것을 확인했습니다.이 구조는 다음과 같은 문제를 야기할 수 있습니다:
- Comment 조회 시 항상 User와 Post를 함께 로드
- 여러 Comment 조회 시 N+1 쿼리 문제 발생
- 양방향 EAGER 관계로 인한 연쇄 로딩 (Comment → Post → commentList → 각 Comment의 User)
LAZY페치로 변경하고 필요 시@EntityGraph또는 fetch join을 사용하는 것을 권장합니다:- @ManyToOne(fetch = FetchType.EAGER) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; - @ManyToOne(fetch = FetchType.EAGER) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id", nullable = false) private Post post;
Post.java의commentList와user관계도 동일하게LAZY로 변경을 고려해 주세요.src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java (1)
18-19: Swagger example 형식을 개선할 수 있습니다.
categoryList는List<String>타입인데 example이"자유, 수선"문자열로 표현되어 있습니다. API 문서의 명확성을 위해 JSON 배열 형식으로 표현하는 것이 좋습니다.다음과 같이 수정할 수 있습니다:
- @Schema(description = "카테고리 리스트", example = "자유, 수선") + @Schema(description = "카테고리 리스트", example = "[\"자유\", \"수선\"]") private List<String> categoryList;src/main/java/com/sku/refit/domain/post/repository/PostRepository.java (1)
16-19: 대량 데이터에서 쿼리 성능을 모니터링하세요.
findByCategoryListContaining메서드는post_category조인 테이블과 조인하여 검색합니다. 게시글이 많아지면 인덱싱 전략이 필요할 수 있습니다.향후 성능 개선을 위한 고려사항:
post_category테이블의category컬럼에 인덱스 추가- 쿼리 실행 계획 모니터링 및 slow query 로그 확인
- 필요시
@Query로 최적화된 JPQL 작성 고려src/main/java/com/sku/refit/domain/post/entity/Post.java (2)
61-63: User 관계에 EAGER fetch 전략을 재검토하세요.
User에 대해FetchType.EAGER를 사용하면 Post를 조회할 때마다 User 정보가 자동으로 로드됩니다. 대부분의 경우 필요하지만, 모든 게시글 목록 조회 시 추가 쿼리가 발생할 수 있습니다.필요에 따라 LAZY로 변경하고, 필요한 곳에서만 명시적으로 로드하는 것을 고려하세요:
- @ManyToOne(fetch = FetchType.EAGER) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user;서비스 레이어에서 필요시 fetch join 사용:
@Query("SELECT p FROM Post p JOIN FETCH p.user WHERE p.id = :id") Optional<Post> findByIdWithUser(@Param("id") Long id);
51-59: 빈 컬렉션에 대한 검증을 추가하세요.
categoryList는@NotEmpty검증이 PostRequest에 있지만, 엔티티 레벨에서는 빈 리스트가 저장될 수 있습니다.nullable=false는 컬럼이 null이 아님을 보장하지만, 빈 컬렉션은 허용합니다.엔티티 레벨에서 검증을 추가하거나, 최소 하나의 카테고리가 있는지 확인하는 로직을 추가하세요:
@ElementCollection @CollectionTable(name = "post_category", joinColumns = @JoinColumn(name = "post_id")) - @Column(nullable = false) + @Column(nullable = false) + @Size(min = 1, message = "최소 하나의 카테고리가 필요합니다") private List<String> categoryList;또는 빌더에서 기본값을 설정하세요:
@ElementCollection @CollectionTable(name = "post_category", joinColumns = @JoinColumn(name = "post_id")) @Column(nullable = false) + @Builder.Default private List<String> categoryList = new ArrayList<>();src/main/java/com/sku/refit/domain/comment/service/CommentService.java (1)
22-28: 대량 댓글 처리를 위한 페이징을 고려하세요.
getAllCommentsByPostId메서드는 페이징 없이 특정 게시글의 모든 댓글을 반환합니다. 일반적으로 게시글당 댓글 수는 제한적이지만, 인기 게시글의 경우 수백~수천 개의 댓글이 달릴 수 있습니다.향후 확장성을 고려하여 페이징 지원 메서드를 추가하세요:
/** * 특정 게시글의 댓글을 페이징하여 조회 * * @param postId 게시글 ID * @param pageable 페이징 정보 * @return 페이징된 댓글 상세 정보 */ PageResponse<CommentDetailResponse> getCommentsByPostId(Long postId, Pageable pageable);현재 메서드는 유지하되, 댓글 수가 많을 때를 대비한 대안을 마련하세요.
src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java (2)
45-63: createPost에서 이미지 업로드 실패 시 예외 처리 누락
createPost메서드에서 이미지 업로드 중 예외가 발생하면, 이미 업로드된 이미지들이 S3에 남아 있게 됩니다.updatePost와 달리 try-catch 블록이 없어 일관성이 부족합니다.public PostDetailResponse createPost(PostRequest request, List<MultipartFile> images) { User user = userService.getCurrentUser(); List<String> imageUrlList = new ArrayList<>(); if (images != null && !images.isEmpty()) { + try { for (MultipartFile image : images) { String imageUrl = s3Service.uploadImage(PathName.POST, image).getImageUrl(); imageUrlList.add(imageUrl); } + } catch (Exception e) { + // 업로드 실패 시 이미 업로드된 이미지 정리 + for (String imageUrl : imageUrlList) { + s3Service.deleteFile(s3Service.extractKeyNameFromUrl(imageUrl)); + } + throw new CustomException(PostErrorCode.IMAGE_UPLOAD_FAILED); + } } Post post = postMapper.toPost(request, imageUrlList, user); postRepository.save(post); return postMapper.toDetailResponse(post, user); }
65-73: 전체 게시글 조회 시 페이지네이션 고려
findAll()은 데이터가 많아지면 성능 문제가 발생할 수 있습니다. 카테고리별 조회처럼 페이지네이션 적용을 고려해 보세요.src/main/java/com/sku/refit/domain/post/exception/PostErrorCode.java (1)
15-27: 에러 코드 정의 적절함, 권한 관련 에러 코드 추가 권장일관된 패턴으로 잘 구현되어 있습니다. 다만,
updatePost와deletePost에서 권한 검증 추가 시 사용할 권한 관련 에러 코드(예:POST_UNAUTHORIZED)를 추가하면 더 명확한 에러 처리가 가능합니다.POST_NOT_FOUND("POST001", "게시글이 존재하지 않습니다.", HttpStatus.NOT_FOUND), INVALID_CATEGORY("POST002", "유효하지 않은 카테고리입니다.", HttpStatus.BAD_REQUEST), IMAGE_UPLOAD_FAILED("POST003", "이미지 업로드 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), IMAGE_DELETE_FAILED("POST004", "이미지 삭제 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), POST_CREATE_FAILED("POST005", "게시글 생성 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), POST_UPDATE_FAILED("POST006", "게시글 수정 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), - POST_DELETE_FAILED("POST007", "게시글 삭제 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR); + POST_DELETE_FAILED("POST007", "게시글 삭제 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + POST_UNAUTHORIZED("POST008", "해당 게시글에 대한 권한이 없습니다.", HttpStatus.FORBIDDEN);src/main/java/com/sku/refit/domain/comment/controller/CommentController.java (1)
43-52: 경로 패턴 일관성 확인
@PutMapping("{id}")와@DeleteMapping("{id}")는 동작하지만,"/{id}"로 명시적으로 슬래시를 포함하면 다른 엔드포인트와 일관성이 높아집니다. 기능상 문제는 없습니다.- @PutMapping("{id}") + @PutMapping("/{id}") @Operation(summary = "특정 댓글 수정", description = "특정 댓글의 내용을 수정합니다.") ResponseEntity<BaseResponse<CommentDetailResponse>> updateComment( @Parameter(description = "댓글 식별자", example = "1") @PathVariable Long id, @RequestBody @Valid CommentRequest request); - @DeleteMapping("{id}") + @DeleteMapping("/{id}") @Operation(summary = "특정 댓글 삭제", description = "특정 댓글을 삭제합니다.(Hard Delete)") ResponseEntity<BaseResponse<Void>> deleteComment( @Parameter(description = "댓글 식별자", example = "1") @PathVariable Long id);src/main/java/com/sku/refit/domain/post/controller/PostController.java (1)
70-84: 파라미터 이름의 일관성을 개선하세요.Line 77의 파라미터 이름이
image(단수)로 되어 있는데, Line 48의createPost메서드에서는images(복수)를 사용하고 있습니다. 두 메서드 모두List<MultipartFile>을 받으므로, 일관성을 위해 복수형(images)을 사용하는 것이 좋습니다.다음과 같이 수정하는 것을 권장합니다:
@Parameter( description = "수정할 이미지 리스트", content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)) - @RequestPart(value = "image", required = false) - List<MultipartFile> image, + @RequestPart(value = "images", required = false) + List<MultipartFile> images,구현체인
PostControllerImpl.java와 서비스 계층에서도 동일하게 수정이 필요합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (30)
src/main/java/com/sku/refit/domain/auth/service/AuthServiceImpl.java(3 hunks)src/main/java/com/sku/refit/domain/comment/controller/CommentController.java(1 hunks)src/main/java/com/sku/refit/domain/comment/controller/CommentControllerImpl.java(1 hunks)src/main/java/com/sku/refit/domain/comment/dto/request/CommentRequest.java(1 hunks)src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java(1 hunks)src/main/java/com/sku/refit/domain/comment/entity/Comment.java(1 hunks)src/main/java/com/sku/refit/domain/comment/exception/CommentErrorCode.java(1 hunks)src/main/java/com/sku/refit/domain/comment/mapper/CommentMapper.java(1 hunks)src/main/java/com/sku/refit/domain/comment/repository/CommentRepository.java(1 hunks)src/main/java/com/sku/refit/domain/comment/service/CommentService.java(1 hunks)src/main/java/com/sku/refit/domain/comment/service/CommentServiceImpl.java(1 hunks)src/main/java/com/sku/refit/domain/post/controller/PostController.java(1 hunks)src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java(1 hunks)src/main/java/com/sku/refit/domain/post/dto/request/PostRequest.java(1 hunks)src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java(1 hunks)src/main/java/com/sku/refit/domain/post/entity/Post.java(1 hunks)src/main/java/com/sku/refit/domain/post/exception/PostErrorCode.java(1 hunks)src/main/java/com/sku/refit/domain/post/mapper/PostMapper.java(1 hunks)src/main/java/com/sku/refit/domain/post/repository/PostRepository.java(1 hunks)src/main/java/com/sku/refit/domain/post/service/PostService.java(1 hunks)src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java(1 hunks)src/main/java/com/sku/refit/global/config/S3Config.java(1 hunks)src/main/java/com/sku/refit/global/page/exception/PageErrorStatus.java(1 hunks)src/main/java/com/sku/refit/global/page/mapper/InfiniteMapper.java(1 hunks)src/main/java/com/sku/refit/global/page/mapper/PageMapper.java(1 hunks)src/main/java/com/sku/refit/global/page/response/InfiniteResponse.java(1 hunks)src/main/java/com/sku/refit/global/page/response/PageResponse.java(1 hunks)src/main/java/com/sku/refit/global/s3/entity/PathName.java(1 hunks)src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java(1 hunks)src/main/resources(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (14)
src/main/java/com/sku/refit/domain/comment/mapper/CommentMapper.java (1)
src/main/java/com/sku/refit/domain/post/mapper/PostMapper.java (1)
Component(16-42)
src/main/java/com/sku/refit/global/page/mapper/InfiniteMapper.java (1)
src/main/java/com/sku/refit/global/page/mapper/PageMapper.java (1)
Component(11-24)
src/main/java/com/sku/refit/global/page/mapper/PageMapper.java (1)
src/main/java/com/sku/refit/global/page/mapper/InfiniteMapper.java (1)
Component(12-24)
src/main/java/com/sku/refit/domain/post/dto/request/PostRequest.java (3)
src/main/java/com/sku/refit/domain/comment/dto/request/CommentRequest.java (1)
Getter(14-24)src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java (1)
Getter(12-28)src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java (1)
Getter(13-38)
src/main/java/com/sku/refit/domain/post/mapper/PostMapper.java (1)
src/main/java/com/sku/refit/domain/comment/mapper/CommentMapper.java (1)
Component(14-30)
src/main/java/com/sku/refit/domain/post/entity/Post.java (5)
src/main/java/com/sku/refit/domain/comment/entity/Comment.java (1)
Entity(26-52)src/main/java/com/sku/refit/domain/comment/dto/request/CommentRequest.java (1)
Getter(14-24)src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java (1)
Getter(12-28)src/main/java/com/sku/refit/domain/post/dto/request/PostRequest.java (1)
Getter(17-35)src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java (1)
Getter(13-38)
src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java (2)
src/main/java/com/sku/refit/domain/comment/service/CommentServiceImpl.java (1)
Service(27-97)src/main/java/com/sku/refit/domain/auth/service/AuthServiceImpl.java (1)
Slf4j(28-134)
src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java (2)
src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java (1)
Getter(12-28)src/main/java/com/sku/refit/domain/post/dto/request/PostRequest.java (1)
Getter(17-35)
src/main/java/com/sku/refit/global/page/response/InfiniteResponse.java (1)
src/main/java/com/sku/refit/global/page/response/PageResponse.java (1)
Getter(12-34)
src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java (2)
src/main/java/com/sku/refit/domain/comment/controller/CommentControllerImpl.java (1)
RestController(23-60)src/main/java/com/sku/refit/domain/auth/service/AuthServiceImpl.java (1)
Slf4j(28-134)
src/main/java/com/sku/refit/domain/comment/controller/CommentControllerImpl.java (1)
src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java (1)
RestController(23-75)
src/main/java/com/sku/refit/global/page/response/PageResponse.java (1)
src/main/java/com/sku/refit/global/page/response/InfiniteResponse.java (1)
Getter(12-28)
src/main/java/com/sku/refit/domain/comment/dto/request/CommentRequest.java (3)
src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java (1)
Getter(12-28)src/main/java/com/sku/refit/domain/post/dto/request/PostRequest.java (1)
Getter(17-35)src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java (1)
Getter(13-38)
src/main/java/com/sku/refit/domain/comment/entity/Comment.java (5)
src/main/java/com/sku/refit/domain/post/entity/Post.java (1)
Entity(33-78)src/main/java/com/sku/refit/domain/comment/dto/request/CommentRequest.java (1)
Getter(14-24)src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java (1)
Getter(12-28)src/main/java/com/sku/refit/domain/post/dto/request/PostRequest.java (1)
Getter(17-35)src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java (1)
Getter(13-38)
🔇 Additional comments (26)
src/main/resources (1)
1-1: 서브모듈 커밋 참조 업데이트 검증 필요
src/main/resources서브모듈의 커밋이 업데이트되었습니다 (f6f4348 → 547102). 이 변경이 PR의 커뮤니티 기능 구현과 어떻게 연관되는지, 그리고 새 커밋에서 어떤 변경사항이 포함되었는지 확인이 필요합니다.구체적으로 다음을 검증해 주세요:
- 새 커밋에서 어떤 리소스/구성이 변경되었는지
- 이 업데이트가 포스트/댓글 기능(S3 경로 설정, 카테고리 구조 등)과 어떤 관계가 있는지
- 빌드 또는 배포 시 필요한 추가 단계가 있는지
src/main/java/com/sku/refit/global/page/exception/PageErrorStatus.java (1)
13-23: LGTM!에러 코드 enum이 프로젝트의 다른 에러 코드들(PostErrorCode, CommentErrorCode)과 일관된 패턴으로 잘 구현되었습니다. 코드, 메시지, HTTP 상태 매핑이 적절합니다.
src/main/java/com/sku/refit/domain/comment/exception/CommentErrorCode.java (1)
15-17: LGTM!
BaseErrorCode인터페이스를 구현한 에러 코드 enum이 프로젝트 컨벤션에 맞게 잘 정의되었습니다.향후 댓글 권한 검증(작성자가 아닌 사용자의 수정/삭제 시도 등)을 위한
UNAUTHORIZED_COMMENT_ACCESS같은 에러 코드 추가를 고려해 볼 수 있습니다.src/main/java/com/sku/refit/global/s3/entity/PathName.java (1)
11-14: S3 경로 상수가 올바르게 구현되었습니다
POST와CLOTH상수가PathName열거형에 적절히 추가되었으며, 각각@Schema어노테이션으로 잘 문서화되어 있습니다.S3ServiceImpl.getPrefix()메서드에서 두 상수 모두 정확하게 처리되고 있으며(case POST -> s3Config.getPostPath(),case CLOTH -> s3Config.getClothPath()),PostServiceImpl에서도 게시글 이미지 업로드 시PathName.POST가 올바르게 사용되고 있습니다.src/main/java/com/sku/refit/global/s3/service/S3ServiceImpl.java (1)
195-196: PathName 매핑이 올바르게 업데이트되었습니다.새로운 POST 및 CLOTH PathName 케이스가 S3Config의 해당 경로 필드와 올바르게 매핑되었습니다.
src/main/java/com/sku/refit/global/page/mapper/PageMapper.java (1)
14-23: 페이지 매핑 로직이 올바르게 구현되었습니다.Spring Data의 0-based 페이지 번호를 1-based로 변환하는 로직이 적절하며, InfiniteMapper와 일관된 패턴을 따르고 있습니다.
src/main/java/com/sku/refit/domain/comment/mapper/CommentMapper.java (1)
17-29: 매퍼 구현이 올바르며 PostMapper와 일관된 패턴을 따릅니다.
toComment메서드는 요청 데이터로부터 Comment 엔티티를 올바르게 생성합니다.toDetailResponse의 isWriter 로직이 사용자 ID 비교를 통해 적절하게 구현되었습니다.src/main/java/com/sku/refit/global/config/S3Config.java (1)
42-46: S3 경로 구성이 올바르게 추가되었습니다.새로운
postPath및clothPath필드가 적절하게 추가되었으며, 기존profileImagePath패턴과 일관성을 유지합니다.src/main/java/com/sku/refit/global/page/mapper/InfiniteMapper.java (1)
15-23: 무한 스크롤 응답 매퍼가 올바르게 구현되었습니다.InfiniteResponse 생성 로직이 명확하며, PageMapper와 일관된 패턴을 따릅니다.
src/main/java/com/sku/refit/domain/post/mapper/PostMapper.java (1)
19-41: Post 매퍼가 올바르게 구현되었습니다.
toPost는 요청 데이터와 이미지 URL 목록으로부터 Post 엔티티를 적절히 생성합니다.toDetailResponse는 댓글 ID 목록을 스트림으로 추출하는 등 응답 데이터를 올바르게 조합합니다.- CommentMapper와 일관된 패턴을 따릅니다.
src/main/java/com/sku/refit/domain/comment/repository/CommentRepository.java (1)
14-16: Repository 인터페이스가 올바르게 구현되었습니다.Spring Data JPA의 메서드 이름 규칙을 따르며,
findAllByPostIdOrderByCreatedAtAsc쿼리 메서드가 적절하게 정의되었습니다.src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java (1)
30-36: 구현이 올바릅니다.컨트롤러 메서드가 적절하게
@Valid검증을 사용하고 있으며, 서비스 레이어로 위임한 후 표준화된BaseResponse로 래핑하여 반환합니다.src/main/java/com/sku/refit/domain/post/repository/PostRepository.java (1)
14-20: Repository 메서드가 적절하게 구성되었습니다.
findByCategoryListContaining메서드는@ElementCollection으로 매핑된categoryList와 함께 정상적으로 작동합니다. Spring Data JPA가 자동으로 적절한 조인 쿼리를 생성합니다.src/main/java/com/sku/refit/domain/post/dto/request/PostRequest.java (1)
17-35: DTO 구조와 검증이 적절합니다.
PostRequest는 적절한 검증 어노테이션을 사용하고 있습니다:
@NotEmpty로 빈 카테고리 리스트 방지@NotBlank로 제목과 내용의 빈 값/공백 방지Swagger 문서화도 잘 되어 있습니다.
src/main/java/com/sku/refit/domain/comment/dto/request/CommentRequest.java (1)
14-24: DTO 구조가 적절합니다.
CommentRequest는@NotBlank검증으로 빈 댓글 내용을 방지하며, 간결하고 명확한 구조를 가지고 있습니다.src/main/java/com/sku/refit/domain/post/service/PostService.java (2)
30-37: 메서드 시그니처가 적절합니다.이미지 업로드를 포함한 게시글 생성 메서드가 명확하게 정의되어 있습니다.
46-53: 카테고리 기반 무한 스크롤 지원이 적절합니다.
getPostsByCategory메서드는lastPostId와size를 사용하여 커서 기반 페이징을 구현하며, 무한 스크롤에 적합합니다.src/main/java/com/sku/refit/domain/comment/service/CommentService.java (2)
13-20: 메서드 시그니처가 적절합니다.댓글 생성 시
postId를 별도 파라미터로 받는 것은 명확하고 적절한 설계입니다.
39-44: Hard Delete 명시가 유용합니다.JavaDoc에 "Hard Delete"를 명시한 것은 좋은 관행입니다. 향후 소프트 삭제로 변경할 때 명확한 변경 지점이 됩니다.
src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java (1)
75-105: 커서 기반 페이지네이션 로직 구현 적절함
size + 1개를 조회하여 다음 페이지 존재 여부를 판단하는 방식이 올바르게 구현되어 있습니다.posts.getLast()호출 전isEmpty()체크로 안전하게 처리됩니다.src/main/java/com/sku/refit/global/page/response/PageResponse.java (1)
12-34: 페이지네이션 응답 DTO 구현 적절함
InfiniteResponse와 일관된 패턴으로 잘 구현되어 있습니다. Swagger 어노테이션과 Lombok 활용이 적절합니다.src/main/java/com/sku/refit/domain/comment/service/CommentServiceImpl.java (1)
38-53: 댓글 생성 로직 구현 적절함게시글 존재 여부 확인, 현재 사용자 조회, 댓글 저장 및 응답 변환이 잘 구현되어 있습니다.
src/main/java/com/sku/refit/domain/comment/controller/CommentController.java (1)
28-42: 컨트롤러 인터페이스 구현 적절함OpenAPI 어노테이션을 활용한 API 문서화와 Jakarta Validation을 통한 요청 검증이 잘 구현되어 있습니다.
src/main/java/com/sku/refit/domain/comment/controller/CommentControllerImpl.java (1)
23-59: 구현이 깔끔하고 일관성이 있습니다!컨트롤러 구현이 표준 Spring MVC 패턴을 잘 따르고 있으며, 다음 사항들이 적절하게 처리되었습니다:
@PathVariable과@RequestParam적절한 사용@Valid를 통한 입력 검증- 서비스 계층으로의 적절한 위임
BaseResponse를 사용한 일관된 응답 형식src/main/java/com/sku/refit/global/page/response/InfiniteResponse.java (1)
12-28: 무한 스크롤 응답 DTO가 잘 설계되었습니다!커서 기반 페이지네이션을 위한 필드 구성이 적절하며, 모든 필드가 Swagger 어노테이션으로 명확하게 문서화되어 있습니다.
PageResponse와 명확히 구분되는 역할을 가지고 있습니다.src/main/java/com/sku/refit/domain/post/controller/PostController.java (1)
55-63: パラメータ名を確認して修正してください。Line 59 のパラメータ
lastBookIdがlastPostIdに変更する必要があることを確認してください。この修正は、PostController、PostControllerImpl、PostService、および PostServiceImpl に一貫して適用する必要があります。
src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java
Outdated
Show resolved
Hide resolved
src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java
Outdated
Show resolved
Hide resolved
src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java
Outdated
Show resolved
Hide resolved
src/main/java/com/sku/refit/domain/comment/service/CommentServiceImpl.java
Show resolved
Hide resolved
src/main/java/com/sku/refit/domain/comment/service/CommentServiceImpl.java
Show resolved
Hide resolved
| /** | ||
| * 모든 게시글 목록을 조회합니다. | ||
| * | ||
| * @return 게시글 상세 응답 리스트 | ||
| */ | ||
| List<PostDetailResponse> getAllPosts(); |
There was a problem hiding this comment.
페이징 없이 모든 게시글을 조회하면 성능 문제가 발생합니다.
getAllPosts() 메서드는 페이징 없이 모든 게시글을 반환합니다. 게시글 수가 증가하면 다음 문제가 발생합니다:
- 메모리 부족: 수천 개의 게시글과 연관된 엔티티(User, Comment 등)를 한 번에 로드
- 응답 시간 증가: 대량의 데이터 직렬화 및 전송
- 데이터베이스 부하: 불필요한 대량 쿼리 실행
다음 중 하나를 적용하세요:
옵션 1: 페이징 추가 (권장)
/**
* 모든 게시글 목록을 페이징하여 조회합니다.
*
* @param pageable 페이징 정보
* @return 페이징된 게시글 상세 응답
*/
PageResponse<PostDetailResponse> getAllPosts(Pageable pageable);옵션 2: 메서드 제거
카테고리별 조회(getPostsByCategory)로 충분하다면 이 메서드를 제거하세요.
옵션 3: 최대 제한 추가
/**
* 최근 게시글 목록을 조회합니다 (최대 100개).
*
* @return 최근 게시글 상세 응답 리스트
*/
List<PostDetailResponse> getRecentPosts();🤖 Prompt for AI Agents
src/main/java/com/sku/refit/domain/post/service/PostService.java lines 39-44:
getAllPosts() currently returns all posts without paging which will cause
memory, latency, and DB-load issues; replace it with a paged API (preferred) by
changing the signature to accept a Pageable and return a paginated DTO (e.g.,
PageResponse<PostDetailResponse> getAllPosts(Pageable pageable)), then update
the implementation to call the repository method that returns a Page, map
entities to PostDetailResponse while preserving paging metadata, and update
controller endpoints and tests to pass/handle Pageable; alternatively, if global
listing is not needed remove this method or add a bounded recent-list method
(e.g., List<PostDetailResponse> getRecentPosts() limited to 100) and implement
repository query with ORDER BY createdAt DESC and LIMIT to enforce the cap.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/main/java/com/sku/refit/domain/comment/service/CommentServiceImpl.java (1)
87-105: orphanRemoval 설정 시 중복 삭제 호출 정리 가능
deleteComment에서 작성자 권한 체크까지 잘 들어간 상태입니다. 다만, 이전 리뷰 코멘트에서 언급된 것처럼Post엔티티의commentList에orphanRemoval = true가 설정되어 있다면:Post post = comment.getPost(); post.getCommentList().remove(comment);만으로도 JPA가 고아 엔티티를 삭제하므로, 아래
commentRepository.delete(comment);호출은 중복일 수 있습니다. 트랜잭션 안에서post.getCommentList().remove(comment);에만 의존하도록 정리하면 영속성 컨텍스트에서 한 번만 삭제가 일어나고, 코드 의도도 더 명확해집니다.- Post post = comment.getPost(); - post.getCommentList().remove(comment); - - commentRepository.delete(comment); + Post post = comment.getPost(); + post.getCommentList().remove(comment);(단, 실제 매핑이
orphanRemoval = true인지 한 번만 다시 확인해 주세요.)
🧹 Nitpick comments (1)
src/main/java/com/sku/refit/domain/post/entity/Post.java (1)
61-63: User 관계의 fetch 전략 검토를 권장합니다.
User에 대한FetchType.EAGER전략은 대부분의 게시글 조회 시 작성자 정보가 필요한 경우 적합할 수 있습니다. 그러나 특정 쿼리에서 사용자 정보가 불필요한 경우 성능 오버헤드가 발생할 수 있습니다.필요시
LAZY로 변경하고, 사용자 정보가 필요한 경우에만@EntityGraph또는JOIN FETCH를 사용하여 명시적으로 로드하는 것을 고려해보세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java(1 hunks)src/main/java/com/sku/refit/domain/comment/service/CommentServiceImpl.java(1 hunks)src/main/java/com/sku/refit/domain/post/controller/PostController.java(1 hunks)src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java(1 hunks)src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java(1 hunks)src/main/java/com/sku/refit/domain/post/entity/Post.java(1 hunks)src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java
- src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java
- src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/sku/refit/domain/post/entity/Post.java (5)
src/main/java/com/sku/refit/domain/comment/entity/Comment.java (1)
Entity(26-52)src/main/java/com/sku/refit/domain/comment/dto/response/CommentDetailResponse.java (1)
Getter(12-28)src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java (1)
Getter(13-41)src/main/java/com/sku/refit/domain/comment/dto/request/CommentRequest.java (1)
Getter(14-24)src/main/java/com/sku/refit/domain/post/dto/request/PostRequest.java (1)
Getter(17-35)
src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java (1)
src/main/java/com/sku/refit/domain/comment/controller/CommentControllerImpl.java (1)
RestController(23-60)
🔇 Additional comments (5)
src/main/java/com/sku/refit/domain/comment/service/CommentServiceImpl.java (3)
38-53: 댓글 생성 플로우와 트랜잭션 구성이 깔끔합니다현재 사용자 조회 → 게시글 존재 검증 → Comment 매핑/저장 → DTO 변환까지 흐름이 명확하고, 쓰기 메서드에만 별도
@Transactional을 두는 구조도 적절합니다. 이 구간은 그대로 두셔도 될 것 같습니다.
55-66: 게시글 존재 검증 후 댓글 리스트 조회 구현 무난
existsById로 게시글 존재 여부를 먼저 확인하고, 없으면POST_NOT_FOUND를 던지는 방식이 명확해서 API 관점에서 예측 가능한 동작을 제공합니다. 현재 요구사항 기준으로는 이대로도 충분해 보입니다.
68-85: 작성자 기준 댓글 수정 권한 체크 추가된 점 좋습니다
userService.getCurrentUser()로 현재 사용자 조회 후comment.getUser().getId()와 비교해 작성자만 수정 가능하도록 막은 부분이 이전 리뷰 피드백도 잘 반영하고 있고, 권한 실패 시COMMENT_NOT_FOUND로 존재를 감추는 전략도 일관된 설계라 보입니다.src/main/java/com/sku/refit/domain/post/entity/Post.java (1)
65-71: 이전 리뷰의 EAGER fetch 문제가 해결되었습니다.
commentList의 fetch 전략이FetchType.LAZY로 올바르게 변경되었습니다. 이를 통해 N+1 문제와 불필요한 데이터 로딩을 방지할 수 있습니다.src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java (1)
23-75: 구현이 깔끔하고 이전 리뷰의 문제점들이 모두 해결되었습니다.
lastPostId파라미터명이 올바르게 수정되었습니다 (line 47).deletePost메서드명이 올바르게 수정되었습니다 (line 70).- 서비스 레이어로의 위임 패턴이 일관되게 적용되어 있습니다.
- 응답 래핑이
BaseResponse를 통해 일관성 있게 처리되고 있습니다.
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/java/com/sku/refit/domain/post/controller/PostController.java(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build
🔇 Additional comments (4)
src/main/java/com/sku/refit/domain/post/controller/PostController.java (4)
1-30: 패키지 선언 및 임포트가 적절합니다.필요한 Spring, Jakarta Validation, Swagger, 그리고 도메인 관련 클래스들이 모두 적절하게 임포트되어 있습니다.
32-53: 인터페이스 선언 및 초기 엔드포인트들이 적절합니다.Swagger 태그와 경로 매핑이 올바르게 설정되어 있고, 게시글 작성 및 전체 조회 엔드포인트의 구조가 적절합니다. multipart 처리와 유효성 검사 어노테이션이 올바르게 적용되었습니다.
65-68: 게시글 단일 조회 엔드포인트가 적절합니다.표준 REST 패턴을 따르는 단일 리소스 조회 엔드포인트입니다.
86-89: 게시글 삭제 엔드포인트가 올바르게 수정되었습니다.이전 리뷰에서 지적된
deleteExhibitionLike메서드 이름이deletePost로 올바르게 수정되었습니다. 표준 REST 삭제 패턴을 잘 따르고 있습니다.
| @GetMapping | ||
| @Operation(summary = "카테코리별 게시글 전체 조회", description = "특정 카테고리의 게시글 리스트를 조회합니다.") | ||
| ResponseEntity<BaseResponse<InfiniteResponse<PostDetailResponse>>> getPostByCategory( | ||
| @RequestParam String category, | ||
| @Parameter(description = "마지막으로 조회한 게시글 식별자(첫 조회 시 생략)", example = "3") | ||
| @RequestParam(required = false) | ||
| Long lastPostId, | ||
| @Parameter(description = "한 번에 조회할 게시글 개수", example = "3") @RequestParam(defaultValue = "3") | ||
| Integer size); |
There was a problem hiding this comment.
Fix Korean typo in endpoint summary.
Line 56: "카테코리별" should be "카테고리별".
@GetMapping
- @Operation(summary = "카테코리별 게시글 전체 조회", description = "특정 카테고리의 게시글 리스트를 조회합니다.")
+ @Operation(summary = "카테고리별 게시글 전체 조회", description = "특정 카테고리의 게시글 리스트를 조회합니다.")
ResponseEntity<BaseResponse<InfiniteResponse<PostDetailResponse>>> getPostByCategory(Consider increasing the default page size. Line 62: The default size of 3 is unusually small for infinite scroll pagination. Verify whether this is intentional or should be adjusted to a more typical value like 10–20.
🤖 Prompt for AI Agents
In src/main/java/com/sku/refit/domain/post/controller/PostController.java around
lines 55 to 63, fix the Korean typo in the @Operation summary by changing
"카테코리별" to "카테고리별", and adjust the @RequestParam(defaultValue = "3") for size to
a more typical pagination default (e.g., "10" or another agreed value); update
the annotation defaultValue and any related Javadoc/comments if present to
reflect the new default.
| @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | ||
| @Operation(summary = "게시글 수정", description = "특정 게시글의 상세 내용을 수정합니다.") | ||
| ResponseEntity<BaseResponse<PostDetailResponse>> updatePostById( | ||
| @Parameter(description = "게시글 식별자", example = "1") @PathVariable Long id, | ||
| @Parameter( | ||
| description = "수정할 이미지 리스트", | ||
| content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)) | ||
| @RequestPart(value = "image", required = false) | ||
| List<MultipartFile> image, | ||
| @Parameter( | ||
| description = "게시글 수정 내용", | ||
| content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) | ||
| @Valid | ||
| @RequestPart(value = "request") | ||
| PostRequest request); |
There was a problem hiding this comment.
파라미터 이름의 일관성을 개선해야 합니다.
Line 77의 image 파라미터는 List<MultipartFile> 타입임에도 불구하고 단수형으로 명명되어 있습니다. Line 48의 createPost 메서드에서는 동일한 타입을 images (복수형)로 명명했습니다. API 일관성을 위해 복수형으로 통일하는 것이 좋습니다.
@PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "게시글 수정", description = "특정 게시글의 상세 내용을 수정합니다.")
ResponseEntity<BaseResponse<PostDetailResponse>> updatePostById(
@Parameter(description = "게시글 식별자", example = "1") @PathVariable Long id,
@Parameter(
description = "수정할 이미지 리스트",
content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE))
- @RequestPart(value = "image", required = false)
- List<MultipartFile> image,
+ @RequestPart(value = "images", required = false)
+ List<MultipartFile> images,
@Parameter(
description = "게시글 수정 내용",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE))
@Valid
@RequestPart(value = "request")
PostRequest request);참고: 구현체(PostControllerImpl)에서도 동일하게 파라미터 이름을 수정해야 합니다.
📝 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.
| @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | |
| @Operation(summary = "게시글 수정", description = "특정 게시글의 상세 내용을 수정합니다.") | |
| ResponseEntity<BaseResponse<PostDetailResponse>> updatePostById( | |
| @Parameter(description = "게시글 식별자", example = "1") @PathVariable Long id, | |
| @Parameter( | |
| description = "수정할 이미지 리스트", | |
| content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)) | |
| @RequestPart(value = "image", required = false) | |
| List<MultipartFile> image, | |
| @Parameter( | |
| description = "게시글 수정 내용", | |
| content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) | |
| @Valid | |
| @RequestPart(value = "request") | |
| PostRequest request); | |
| @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) | |
| @Operation(summary = "게시글 수정", description = "특정 게시글의 상세 내용을 수정합니다.") | |
| ResponseEntity<BaseResponse<PostDetailResponse>> updatePostById( | |
| @Parameter(description = "게시글 식별자", example = "1") @PathVariable Long id, | |
| @Parameter( | |
| description = "수정할 이미지 리스트", | |
| content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE)) | |
| @RequestPart(value = "images", required = false) | |
| List<MultipartFile> images, | |
| @Parameter( | |
| description = "게시글 수정 내용", | |
| content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) | |
| @Valid | |
| @RequestPart(value = "request") | |
| PostRequest request); |
🤖 Prompt for AI Agents
In src/main/java/com/sku/refit/domain/post/controller/PostController.java around
lines 70 to 84, the RequestPart parameter named "image" is singular despite
being a List<MultipartFile>, causing inconsistency with createPost which uses
"images"; rename the parameter to "images" (and update its @RequestPart value to
"images") to match naming and API contract, and apply the same change in the
implementation class PostControllerImpl to keep signatures consistent.
✨ 새로운 기능
🛠 개발 상세
🧪 테스트 방법
🧩 추가 고려사항
🔗 관련 문서 / 이슈
Summary by CodeRabbit
새로운 기능
개선
✏️ Tip: You can customize this high-level summary in your review settings.