-
Notifications
You must be signed in to change notification settings - Fork 0
[CER-3] 자격증 댓글 기능 구현 #169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[CER-3] 자격증 댓글 기능 구현 #169
The head ref may contain hidden characters: "CER-3-\uC790\uACA9\uC99D-\uB313\uAE00-API"
Conversation
- 댓글 조회 - 댓글 삭제 - 댓글 좋아요 / 좋아요 취소
- 서비스 로직 테스트 추가 - 컨트롤러 로직 추가 - jpql 구문 수정
- 댓글 조회 - 댓글 삭제 - 댓글 좋아요 / 좋아요 취소
- 서비스 로직 테스트 추가 - 컨트롤러 로직 추가 - jpql 구문 수정
Walkthrough자격증 댓글(엔티티·좋아요·리포지토리 쿼리·서비스·컨트롤러)과 페이징 DTO, 에러코드 항목 추가 및 테스트용 Lombok 의존성 추가가 적용되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant Ctrl as CertificationCommentController
participant Svc as CertificationCommentService
participant CRepo as CertificationCommentRepository
participant LRepo as CertificationCommentLikeRepository
participant Acq as AcquisitionRepository
participant Pre as UserPreCertificationRepository
rect `#e6f5ff`
Note over User,Ctrl: 댓글 등록
User->>Ctrl: POST /api/v1/comments (content, certificationId)
Ctrl->>Svc: registerComment(userId, request)
Svc->>Acq: findByCertificationUserIn(certId, users)
Acq-->>Svc: 취득 정보
Svc->>Pre: findByCertificationUserIn(certId, users)
Pre-->>Svc: 사전인증 정보
Svc->>CRepo: save(CertificationComment)
CRepo-->>Svc: 저장 완료
Svc-->>Ctrl: 성공
Ctrl-->>User: 201 CREATE
end
rect `#f0fff0`
Note over User,Ctrl: 댓글 조회(페이지)
User->>Ctrl: GET /api/v1/comments?certificationId=X&page...
Ctrl->>Svc: getCommentsByCertification(userId, certId, pageable)
Svc->>CRepo: findByCertificationId(certId, pageable)
CRepo-->>Svc: Page<CertificationComment>
Svc->>LRepo: findLikedCommentIdsByCertificationAndUser(user, certification)
LRepo-->>Svc: likedIds
Svc-->>Ctrl: PageResponse<CertificationCommentResponse>
Ctrl-->>User: 200 FETCH
end
rect `#fff5e6`
Note over User,Ctrl: 좋아요 토글
User->>Ctrl: POST /api/v1/comments/{id}/like
Ctrl->>Svc: toggleCommentLike(userId, commentId)
Svc->>LRepo: existsByUserAndCertificationComment(user, comment)
alt 없음
Svc->>LRepo: save(new CertificationCommentLike(...))
Svc->>CRepo: incrementLikeCount(commentId)
else 있음
Svc->>LRepo: delete(found)
Svc->>CRepo: decrementLikeCount(commentId)
end
Svc-->>Ctrl: 성공
Ctrl-->>User: 200 UPDATE
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Nitpick comments (19)
src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java (1)
25-25: 불변 리스트 생성으로 방어적 복사 개선
stream().toList()를 사용하여 불변 리스트를 반환하는 것은 좋은 개선입니다. 이는CertificationDetailResponse와 일관성을 유지하며, 외부에서 반환된 리스트를 수정하는 것을 방지합니다.다만,
CertificationSimple.java(라인 23)에서는 여전히certification.getTags()를 직접 할당하고 있어 일관성이 부족합니다. 동일한 방어적 복사 패턴을 적용하는 것을 고려해보세요.선택적 개선: CertificationSimple에도 동일한 패턴 적용
// CertificationSimple.java 라인 23 this.tags = certification.getTags().stream().toList();src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java (2)
48-48: 에러 메시지 명확성 개선을 고려해주세요.현재 메시지 "이미 취소된 좋아요입니다."는 좋아요가 이전에 취소되었음을 암시하지만, 실제로는 좋아요가 존재하지 않는 상황에서도 이 에러가 발생할 수 있습니다. 다른 NOT_FOUND 에러들과의 일관성을 위해 "존재하지 않는 좋아요입니다."로 변경하는 것을 권장합니다.
- COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404018", "이미 취소된 좋아요입니다."), + COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404018", "존재하지 않는 좋아요입니다."),
34-39: 기존 에러 코드 형식 오류를 수정하는 것을 권장합니다.이 파일에는 PR 변경사항과 무관한 기존 에러 코드 형식 오류들이 있습니다:
- 34-38번 줄: "E40404" ~ "E40408" 형식이 잘못되었습니다 (0이 하나 누락됨)
- 39번 줄: " E404009"에 선행 공백이 있습니다
이러한 오류들을 별도 작업으로 수정하는 것을 권장합니다.
- JOB_NOT_FOUND(HttpStatus.NOT_FOUND, "E40404", "존재하지 않는 직무입니다."), - GRADE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40405", "존재하지 않는 학년입니다."), - SOCIAL_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40406", "존재하지 않는 소셜 타입입니다."), - TRACK_NOT_FOUND(HttpStatus.NOT_FOUND, "E40407", "존재하지 않는 계열입니다."), - CERTIFICATION_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40408", "존재하지 않는 자격증 종류입니다."), - CERTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, " E404009", "자격증이 존재하지 않습니다"), + JOB_NOT_FOUND(HttpStatus.NOT_FOUND, "E404004", "존재하지 않는 직무입니다."), + GRADE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404005", "존재하지 않는 학년입니다."), + SOCIAL_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404006", "존재하지 않는 소셜 타입입니다."), + TRACK_NOT_FOUND(HttpStatus.NOT_FOUND, "E404007", "존재하지 않는 계열입니다."), + CERTIFICATION_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "E404008", "존재하지 않는 자격증 종류입니다."), + CERTIFICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "E404009", "자격증이 존재하지 않습니다"),build.gradle (1)
30-30: 테스트용 Lombok 버전 관리 방식 정합성 점검 제안테스트 코드에서 Lombok을 사용하기 위한 의존성/annotationProcessor 추가는 필요해 보입니다. 다만 main 쪽은 Spring Boot BOM에 의해 관리되는 버전을 사용하고, 테스트 쪽은
1.18.28로 직접 고정하고 있어 main/test 간 Lombok 버전이 달라질 수 있습니다. 특별히 테스트에서만 다른 버전을 써야 하는 이유가 없다면, 테스트에서도 버전 명시를 제거하고 BOM에 맞추거나, 공통 상수로 버전을 관리하는 쪽이 유지보수 측면에서 더 깔끔할 것 같습니다.Also applies to: 34-34
src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java (1)
67-71: 추천 API 캐시 비활성화 의도와 코드 정리 방식 확인 필요
recommendCertifications의@Cacheable을 주석 처리해서 캐시를 끈 상태인데, 이 메서드가 전공/직무/즐겨찾기 정보를 여러 번 조회하는 비교적 무거운 로직이라 트래픽이 늘어나면 성능에 영향을 줄 수 있습니다. 캐시를 끈 것이 의도라면(예: 사용자 정보 변경 직후 실시간 반영을 위해), 주석으로 남겨두기보다는 어노테이션을 제거하고, 왜 캐시를 사용하지 않는지에 대한 간단한 주석이나 설계 문서를 남기는 편이 이후 유지보수에 더 명확할 것 같습니다. 향후 부하나 응답 시간이 문제되면 TTL/eviction 전략을 포함해 캐시 재도입도 한 번 고려해 주세요.src/main/java/org/sopt/certi_server/global/error/dto/PageResponse.java (1)
16-30: PageResponse DTO 설계가 단순하고 재사용에 적합합니다Spring
Page를 직접 노출하지 않고 공용 DTO로 감싼 점이 계층 분리에 잘 맞고, 필드 구성도 직관적입니다.
추후 클라이언트에서 현재 페이지 번호(pageNumber)나 페이지 크기(size)가 필요해질 수 있으니, 필요해지면 필드를 확장하는 방향만 염두에 두면 될 것 같습니다.src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationComment.java (1)
18-41: 연관관계 및 컬럼의nullable제약을 DB 레벨에서도 명시하는 것을 추천드립니다도메인 상으로 보면 댓글은 다음 필드들이 거의 필수 값일 가능성이 높습니다.
- 작성자(
user)- 대상 자격증(
certification)- 내용(
content)- 좋아요 수(
likeCount)현재는 JPA 매핑에서
nullable제약이 지정되어 있지 않아, DB 스키마 상 null이 허용될 수 있습니다. 서비스 레벨에서도 검증을 하고 계시겠지만, 데이터 정합성을 위해 아래와 같이 DB 제약을 함께 두는 것을 추천드립니다.@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "certification_id", nullable = false) private Certification certification; @Column(name = "content", columnDefinition = "TEXT", nullable = false) private String content; @Column(name = "like_count", nullable = false) private Long likeCount = 0L;이렇게 하면 잘못된 데이터가 DB에 저장되는 것을 한 번 더 방지할 수 있고, 나중에 쿼리 작성 시에도 가정을 더 안전하게 가져갈 수 있습니다.
src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationCommentLike.java (1)
10-37: 좋아요 엔티티 모델링과 유니크 제약 설정이 적절합니다단일 유저가 동일 댓글에 여러 번 좋아요를 누르지 못하도록
(user_id, certification_comment_id)에 유니크 제약을 둔 점이 명확하고, 연관관계도 단방향으로 간결하게 잘 정리되어 있습니다.동시에 여러 좋아요 요청이 들어오는 경우에는 이 유니크 제약으로 인해 DB에서 예외가 발생할 수 있으니, 서비스 레이어에서 해당 예외를 캐치해 “이미 좋아요된 상태”로 매핑하는 등의 정책만 정의해 두시면 실서비스에서도 안정적으로 동작할 것 같습니다.
src/main/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentRepository.java (2)
13-20:JOIN FETCH대신LEFT JOIN FETCH고려 (탈퇴/없는 사용자 댓글 포함 여부)
findByCertificationId에서JOIN FETCH cc.user를 사용하면,cc.user가null인 댓글(탈퇴 사용자, FK 해제 등)은 결과에서 제외됩니다.
서비스 레이어getCommentsByCertification에서user == null인 경우를 별도로 처리하고 있는 것과 모순될 수 있어, 정말로 그런 댓글을 숨기고 싶은 것인지, 아니면LEFT JOIN FETCH로 포함시켜야 하는지 한 번 더 정책 정리가 필요해 보입니다.SELECT cc FROM CertificationComment cc JOIN FETCH cc.user u ...→ 탈퇴 사용자 댓글도 노출해야 한다면
LEFT JOIN FETCH로 변경하는 것을 검토해 주세요.
22-34:likeCount증감 쿼리의 동시성 및 음수 방지 정책 확인 필요
incrementLikeCount/decrementLikeCount를 JPQL update 로 원자적으로 처리한 점은 좋습니다. 다만:
- 동시 요청에서 토글 로직과 조합될 때, 잘못된 순서로 호출되면
likeCount가 0 미만이 될 수 있습니다.- DB 레벨에서
(user, comment)유니크 제약이나check (like_count >= 0)같은 방어 장치가 없다면, 데이터가 쉽게 깨질 수 있습니다.서비스/엔티티 쪽에 유니크 제약과 음수 방지 정책이 이미 있다면 괜찮지만, 아니라면 스키마 레벨 제약 추가나
where c.likeCount > 0조건 등을 검토해 주세요.src/test/java/org/sopt/certi_server/domain/comment/service/CertificationCommentServiceTest.java (3)
153-160:findAll().get(1)기반 검증은 정렬에 의존해 테스트가 불안정합니다
registerComment_Success에서List<CertificationComment> allComments = certificationCommentRepository.findAll(); assertThat(allComments).hasSize(2); CertificationComment savedComment = allComments.get(1);처럼
findAll()결과의 두 번째 요소를 신규 댓글로 가정하고 있는데, JPA 기본 정렬은 보장되지 않아 DB/환경에 따라 순서가 달라질 수 있습니다.보다 안정적인 검증을 위해 아래와 같이 ID 또는 content 기준으로 찾는 쪽을 권장합니다.
allComments중content = "새 댓글 내용"인 요소를 필터링- 혹은
certificationCommentRepository.findById(...)로 직접 조회
255-258:toggleLike_Success_DoUnlike에서 중복save호출 제거 가능해당 테스트는
@BeforeEach에서 이미testComment를 저장하고 있는데, 테스트 내부에서 다시 한 번certificationCommentRepository.save(testComment);를 호출하고 있습니다.// 좋아요 미리 눌러두기 certificationCommentRepository.save(testComment); // 중복 가능성 certificationCommentService.toggleCommentLike(testUser.getId(), testComment.getId());이
save호출은 의미 있는 상태 변화를 만들지 않고 혼란만 줄 수 있으니 제거해도 괜찮아 보입니다.
230-268: 좋아요 토글 동작은 잘 커버되지만, 동시성 시나리오 테스트 추가를 고려해 주세요
toggleLike_Success_DoLike/toggleLike_Success_DoUnlike로 기본 시나리오는 잘 검증되어 있습니다. 다만 PR 설명에서도 언급하셨듯이:
- 동일 유저가 같은 댓글에 대해 동시에 좋아요/취소 요청을 보낼 때
- 다수의 유저가 동시에 같은 댓글에 좋아요를 보내는 경우
같은 동시성 케이스를 별도 테스트(멀티 스레드 또는 반복 호출)로 추가해 두면,
existsByUserAndCertificationComment+ 카운트 쿼리 조합이 실제로 안전하게 동작하는지 회귀 테스트에 도움이 될 것 같습니다.src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java (4)
50-75: 댓글 등록 권한 체크 로직은 명확하지만 에러 코드 재사용 여부 한 번만 더 점검해 주세요
registerComment에서 취득/취득예정이 아닌 사용자는 댓글을 달 수 없도록UnauthorizedException(ErrorCode.UNAUTHORIZED)를 던지는 구조는 정책상 명확합니다.if(!acquisitionExists && !userPreExists){ throw new UnauthorizedException(ErrorCode.UNAUTHORIZED); }다만, 이미 전역에서 인증 실패/토큰 문제에 같은 에러 코드를 사용 중이라면, "댓글 작성 권한 없음" 과 구분되는 별도 에러 코드/메시지를 두는 것도 API 소비자 입장에서 디버깅이 쉬워집니다. (필수 변경은 아니고, 에러 설계 레벨의 선택 사항입니다.)
90-112: 댓글 조회 시 호출자/자격증 재조회 비용 및user == null분기 정책 재검토 제안
getCommentsByCertification에서 이미certificationId로 댓글들을 조회한 뒤에, 다시User caller = userService.getUser(userId); Certification certification = certificationService.getCertification(certificationId);를 호출해 좋아요 여부를 계산하고 있습니다. 기능적으로 문제는 없지만:
- 댓글 리스트를 인증 없이도 조회할 수 있게 열 계획이 있다면,
userId가 null 인 상황에 대한 방어 로직이 필요합니다.- Repository 쿼리에서
JOIN FETCH cc.user를 사용하고 있기 때문에, user==null 인 댓글은 애초에 조회되지 않아, 아래의user == null분기가 실제로는 실행되지 않을 가능성이 있습니다.이 두 부분은 정책/설계에 가까운 내용이라, "익명 조회 허용 여부" 와 "탈퇴 사용자 댓글 노출 여부" 를 명확히 정한 뒤 서비스/쿼리/DTO 처리 방식을 한 번에 맞춰두면 이후 유지보수에 도움이 될 것 같습니다.
182-211: 좋아요 토글 로직의 레이스 컨디션/데이터 정합성 방어 고민 권장
toggleCommentLike로직은 읽기 → 분기 → 쓰기 패턴으로 깔끔하지만, 동시 요청에서는 다음과 같은 레이스 컨디션이 발생할 수 있습니다.boolean isExist = certificationCommentLikeRepository.existsByUserAndCertificationComment(user, comment); if (isExist) { // unlike path } else { // like path }예시 시나리오:
- 동일 유저가 같은 댓글에 거의 동시에 좋아요 요청 2번 전송
- 둘 다
existsBy... == false를 보고 새CertificationCommentLike를 만들고,likeCount를 2 증가시킬 수 있음.- unlike 경로에서,
existsBy... == true이후 다른 트랜잭션이 먼저 삭제해버리면,findByUserAndCertificationComment가 empty 를 반환하여COMMENT_LIKE_NOT_FOUND예외가 터질 수 있음.가능한 대응 방안:
- DB 레벨에서
(user_id, comment_id)에 유니크 제약 추가 + 중복 insert 시 예외를 캐치 후 무시/보정.existsBy+findBy이중 조회 대신,findBy결과를 기준으로 like/unlike 를 분기.- 필요하다면 비관적 락/낙관적 락 적용.
이미 엔티티/DB 설계에서 유니크 제약 등을 두셨다면 괜찮지만, 아니라면 한 번 더 점검해 보시는 것을 추천드립니다.
163-180: 댓글 삭제 시 연관 좋아요 수동 삭제는 좋지만, JPA cascade 설정과 중복되지 않는지 확인
deleteComment에서 먼저deleteAllByCertificationComment로 좋아요를 지우고, 이후 댓글을 삭제하는 구조는 FK 제약 에러를 피우지 않기 위해 안전한 패턴입니다.certificationCommentLikeRepository.deleteAllByCertificationComment(findComment); certificationCommentRepository.delete(findComment);다만
CertificationComment↔CertificationCommentLike연관관계에 이미cascade = CascadeType.REMOVE/orphanRemoval = true등이 설정되어 있다면, 현재 로직과 중복될 수 있습니다.
실제 엔티티 매핑을 한 번 확인해서, 중복이라면 한 쪽으로 일원화하는 것도 고려해 주세요.src/main/java/org/sopt/certi_server/domain/comment/controller/CertificationCommentController.java (2)
25-32: 요청 본문 검증을 위해@Valid추가를 고려해 주세요댓글 등록 시
CommentRegisterRequest에 대한 Bean Validation 이 컨트롤러 레벨에서 수행되지 않고 있습니다.@PostMapping public ResponseEntity<SuccessResponse<Void>> registerCertificationComment( @RequestBody CommentRegisterRequest commentRegisterRequest, @AuthenticationPrincipal Long userId )
content,certificationId등에 대해 이미 DTO 필드에 제약 애노테이션이 있다면, 컨트롤러 파라미터에@Valid를 붙여주면 요청 단에서 바로 400 을 응답할 수 있어 전체 흐름이 더 견고해집니다.public ResponseEntity<SuccessResponse<Void>> registerCertificationComment( @Valid @RequestBody CommentRegisterRequest commentRegisterRequest, @AuthenticationPrincipal Long userId )
43-59: 댓글 조회 시 정렬/인증 정책을 API 스펙으로 명확히 해 두면 좋습니다
getCommentList는@PageableDefault(sort = "id", direction = DESC)로 최신순 기본 정렬을 제공하면서, 클라이언트가 sort 파라미터를 넘기면 인기순(예:likeCount,desc) 도 지원할 수 있는 구조로 보입니다.@PageableDefault( page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC ) final Pageable pageable두 가지 정도만 스펙에 명시해 두면 더 사용하기 편할 것 같습니다.
- 인기순 조회를 위해서는
?sort=likeCount,desc와 같이 호출해야 한다는 점- 이 API 가 반드시 인증이 필요한지(익명 허용 X) 여부 (
@AuthenticationPrincipal Long userId가 null 인 경우 처리)Swagger 문서나 API 명세에 이 부분을 짧게라도 정리해 두면, 클라이언트/기획과의 커뮤니케이션 비용을 줄일 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
build.gradle(1 hunks)src/main/java/org/sopt/certi_server/domain/acquisition/repository/AcquisitionRepository.java(2 hunks)src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java(1 hunks)src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java(1 hunks)src/main/java/org/sopt/certi_server/domain/comment/controller/CertificationCommentController.java(1 hunks)src/main/java/org/sopt/certi_server/domain/comment/dto/request/CommentRegisterRequest.java(1 hunks)src/main/java/org/sopt/certi_server/domain/comment/dto/response/CertificationCommentResponse.java(1 hunks)src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationComment.java(1 hunks)src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationCommentLike.java(1 hunks)src/main/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentLikeRepository.java(1 hunks)src/main/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentRepository.java(1 hunks)src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java(1 hunks)src/main/java/org/sopt/certi_server/domain/user/repository/UserJobRepository.java(1 hunks)src/main/java/org/sopt/certi_server/domain/userprecertification/repository/UserPreCertificationRepository.java(2 hunks)src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java(1 hunks)src/main/java/org/sopt/certi_server/global/error/dto/PageResponse.java(1 hunks)src/test/java/org/sopt/certi_server/domain/comment/service/CertificationCommentServiceTest.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (13)
src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java (1)
src/main/java/org/sopt/certi_server/global/error/exception/NotFoundException.java (1)
NotFoundException(5-9)
src/main/java/org/sopt/certi_server/domain/comment/dto/request/CommentRegisterRequest.java (1)
src/main/java/org/sopt/certi_server/domain/userprecertification/dto/request/UserPreCertificationRequest.java (1)
UserPreCertificationRequest(5-6)
src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationComment.java (3)
src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationCommentLike.java (1)
Entity(10-38)src/main/java/org/sopt/certi_server/domain/certification/entity/Certification.java (1)
Certification(17-91)src/main/java/org/sopt/certi_server/global/entity/BaseTimeEntity.java (1)
BaseTimeEntity(12-22)
src/main/java/org/sopt/certi_server/domain/comment/controller/CertificationCommentController.java (2)
src/main/java/org/sopt/certi_server/domain/certification/controller/CertificationController.java (2)
CertificationController(21-84)recommendCertification(75-82)src/main/java/org/sopt/certi_server/domain/userprecertification/controller/UserPreCertificationController.java (2)
UserPreCertificationController(16-53)addPreCertification(32-41)
src/main/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentLikeRepository.java (1)
src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java (1)
FavoriteRepository(13-32)
src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java (5)
src/main/java/org/sopt/certi_server/global/error/exception/NotFoundException.java (1)
NotFoundException(5-9)src/main/java/org/sopt/certi_server/global/error/exception/UnauthorizedException.java (1)
UnauthorizedException(5-14)src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java (1)
Service(33-200)src/main/java/org/sopt/certi_server/domain/userprecertification/service/UserPreCertificationService.java (1)
UserPreCertificationService(22-72)src/main/java/org/sopt/certi_server/domain/acquisition/service/AcquisitionService.java (1)
AcquisitionService(30-114)
src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java (2)
src/test/java/org/sopt/certi_server/domain/certification/service/CertificationServiceTest.java (2)
CertificationServiceTest(10-39)certification_recommandation_test(16-25)src/main/java/org/sopt/certi_server/domain/certification/controller/CertificationController.java (2)
recommendCertification(75-82)CertificationController(21-84)
src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java (2)
src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationSimple.java (1)
CertificationSimple(9-33)src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationDetailResponse.java (1)
CertificationDetailResponse(10-43)
build.gradle (1)
src/main/java/org/sopt/certi_server/global/entity/BaseTimeEntity.java (1)
BaseTimeEntity(12-22)
src/main/java/org/sopt/certi_server/global/error/dto/PageResponse.java (2)
src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationListResponse.java (1)
CertificationListResponse(5-13)src/main/java/org/sopt/certi_server/global/error/dto/SuccessResponse.java (1)
SuccessResponse(7-23)
src/main/java/org/sopt/certi_server/domain/user/repository/UserJobRepository.java (2)
src/main/java/org/sopt/certi_server/domain/job/repository/JobRepository.java (2)
findAllByUser(23-28)JobRepository(12-29)src/main/java/org/sopt/certi_server/domain/certification/repository/CertificationJobRepositoryImpl.java (2)
CertificationJobRepositoryImpl(14-36)findByJobIds(20-35)
src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationCommentLike.java (1)
src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationComment.java (1)
Entity(12-43)
src/test/java/org/sopt/certi_server/domain/comment/service/CertificationCommentServiceTest.java (2)
src/main/java/org/sopt/certi_server/global/error/exception/NotFoundException.java (1)
NotFoundException(5-9)src/main/java/org/sopt/certi_server/global/error/exception/UnauthorizedException.java (1)
UnauthorizedException(5-14)
⏰ 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: deploy-cd
🔇 Additional comments (4)
src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java (2)
46-46: LGTM! 댓글 미존재 에러 코드가 올바르게 추가되었습니다.에러 코드 형식과 메시지가 다른 NOT_FOUND 에러들과 일관성 있게 정의되었습니다.
47-47: LGTM! MISSING_HANDLER 에러 코드가 올바르게 업데이트되었습니다.에러 코드 시퀀스가 적절하게 유지되고 있으며, 메시지도 명확합니다.
src/main/java/org/sopt/certi_server/domain/comment/controller/CertificationCommentController.java (1)
61-68: 좋아요 토글 엔드포인트는 명확하고 간결합니다
/api/v1/comments/{commentId}/like에서 인증된 사용자 기준으로 좋아요 토글 후,SUCCESS_UPDATE를 내려주는 구조가 간단하고 명확합니다. 추후 프론트에서 "현재 상태(좋아요 여부)"까지 즉시 알고 싶다면, boolean 플래그를 반환 값에 추가하는 것도 선택지로 남겨둘 수 있겠습니다.src/main/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentLikeRepository.java (1)
14-44: 좋아요 Repository 설계가 명확하고 재사용 가능하게 잘 작성되어 있습니다
findByUserAndCertificationComment,existsByUserAndCertificationComment,deleteAllByCertificationComment등 기본적인 조작 메서드가 표준 네이밍으로 잘 정의되어 있어 가독성이 좋습니다.findLikedCommentIdsByCertificationAndUser는 필요한 댓글 ID만 반환해, 이후 서비스에서Set<Long>으로 변환해 사용하는 패턴과도 잘 맞습니다.별도 수정 없이 현재 형태로도 충분히 유지보수하기 좋아 보입니다.
| import org.sopt.certi_server.domain.user.entity.User; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
IN 파라미터 비어있는 경우 및 fetch join 사용 의도 확인 권장
ac.user IN :users 형태로 엔티티 리스트를 IN 조건에 그대로 넘기는 구현은 의도에 맞게 잘 작성된 것 같습니다. 다만, users가 빈 리스트가 되는 경우 JPA 구현체에 따라 예외가 발생하거나, 항상 false 조건으로 치환되는 등 동작이 달라질 수 있으니, 호출부에서 최소 한 명 이상의 유저가 담기도록 보장하거나, 비어 있을 때는 쿼리를 호출하지 않는 방어 로직을 두는 편이 안전합니다. 또한 현재는 user만 fetch join 하고 있어 Acquisition에서 getUser() 접근 시에는 N+1을 피할 수 있지만, 동일 결과에서 getCertification()까지 자주 접근한다면 certification에 대해서도 fetch join이 필요한지 한 번 점검해보면 좋겠습니다.
Also applies to: 37-43
🏁 Script executed:
#!/bin/bash
# findByCertificationUserIn 호출부와 users 전달 방식 확인
rg -n "findByCertificationUserIn" -C3Length of output: 3595
또한 users가 빈 리스트인 경우 사전 처리 로직이 있는지 확인:
---
🏁 Script executed:
```shell
#!/bin/bash
# CertificationCommentService에서 users 변수의 생성 및 할당 지점 확인
rg -n "List<User>\s+users" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -A2 -B2
Length of output: 299
🏁 Script executed:
#!/bin/bash
# users 리스트 비어있는지 확인하는 로직 검색
rg -n "users\s*\.isEmpty|users\s*\.size|users\.length" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -B2 -A2Length of output: 0
🏁 Script executed:
#!/bin/bash
# findByCertificationUserIn 메서드 호출 전 users에 대한 null/empty 체크
rg -n "findByCertificationUserIn" -B10 src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java | head -20Length of output: 675
---
🏁 Script executed:
```shell
#!/bin/bash
# 전체 CertificationCommentService 메서드의 commentPage 이후 로직 전체 확인
rg -n "commentPage" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -A25 | grep -A25 "getContent"
Length of output: 1196
🏁 Script executed:
#!/bin/bash
# userPreCertificationRepository도 동일하게 빈 users 리스트 전달되는지 확인
rg -n "userJobRepository.findWithJobByUserIn" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -B5 -A5Length of output: 438
---
🏁 Script executed:
```shell
#!/bin/bash
# 상위 메서드 서명 및 commentPage 생성 로직 확인
rg -n "public.*getCertificationComments|private.*getCertificationComments" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -A3
Length of output: 0
🏁 Script executed:
#!/bin/bash
# DTO 변환 시 certification 접근 패턴 확인
rg -n "CertificationCommentResponse|\.getCertification\(\)" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -B2 -A2Length of output: 1047
---
🏁 Script executed:
```shell
#!/bin/bash
# CertificationCommentResponse.from 메서드 구현 확인
rg -n "class CertificationCommentResponse" -A50 src/main/java/org/sopt/certi_server/domain/comment/dto/response/CertificationCommentResponse.java | grep -A30 "from"
Length of output: 0
---
🏁 Script executed:
```shell
#!/bin/bash
# CertificationCommentResponse 파일 찾기
fd -t f "CertificationCommentResponse.java" src/
Length of output: 97
🏁 Script executed:
#!/bin/bash
# from 메서드 검색
rg -n "public.*from.*comment.*state.*job" --type javaLength of output: 0
---
🏁 Script executed:
```shell
#!/bin/bash
# CertificationCommentResponse 파일 내용 확인
cat src/main/java/org/sopt/certi_server/domain/comment/dto/response/CertificationCommentResponse.java
Length of output: 1226
IN 파라미터로 전달되는 users 리스트 비어있을 때 처리 필수
현재 코드에서 commentPage.getContent()가 비어있으면 users 리스트도 빈 상태가 되어 findByCertificationUserIn(certificationId, users)에 빈 리스트가 전달될 수 있습니다. JPA 구현체에 따라 ac.user IN :users 형태의 쿼리는 예외를 발생시키거나 예상치 못한 동작을 할 수 있으므로, CertificationCommentService의 호출부(99줄, 102줄, 105줄)에서 users.isEmpty() 체크를 추가하여 사전에 빈 리스트 전달을 방지하는 것이 안전합니다.
🤖 Prompt for AI Agents
In
src/main/java/org/sopt/certi_server/domain/acquisition/repository/AcquisitionRepository.java
around line 8, ensure that the repository is never called with an empty IN
parameter by adding a defensive check at the caller: in
CertificationCommentService at the call sites around lines 99, 102, and 105,
check if users.isEmpty() and skip calling
findByCertificationUserIn(certificationId, users) (return an empty result or
continue) instead of passing an empty list to the JPA query to avoid exceptions
or undefined behavior.
src/main/java/org/sopt/certi_server/domain/comment/dto/request/CommentRegisterRequest.java
Show resolved
Hide resolved
| public record CertificationCommentResponse( | ||
| Long commentId, | ||
| Long userId, | ||
| String nickName, | ||
| String content, | ||
| String userMajor, | ||
| String userJob, | ||
| String state, | ||
| Long likeCount, | ||
| LocalDateTime createdTime, | ||
| LocalDateTime lastModifiedTime, | ||
| boolean isLike | ||
| ) { | ||
| public static CertificationCommentResponse from( | ||
| CertificationComment certificationComment, | ||
| String state, | ||
| String userJob, | ||
| boolean isLike | ||
| ) { | ||
|
|
||
| return new CertificationCommentResponse( | ||
| certificationComment.getId(), | ||
| certificationComment.getUser().getId(), | ||
| certificationComment.getUser().getNickname(), | ||
| certificationComment.getContent(), | ||
| certificationComment.getUser().getMajor().getName(), | ||
| userJob, | ||
| state, | ||
| certificationComment.getLikeCount(), | ||
| certificationComment.getCreatedTime(), | ||
| certificationComment.getLastModifiedTime(), | ||
| isLike | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
응답 매핑은 명확하지만 userMajor NPE 가능성은 한 번 점검해 보는 것이 좋겠습니다
전체 필드 구성과 from 정적 메서드를 통한 매핑 방식은 직관적이고 사용하기 편해 보입니다. 다만:
userMajor를certificationComment.getUser().getMajor().getName()으로 바로 꺼내고 있어서,User.major가 nullable인 도메인이라면 여기서 NPE가 발생할 수 있습니다.- 해당 필드가 실제로 nullable일 수 있는지 한 번 확인해 보시고, nullable이라면 정책에 따라 기본값/빈 문자열/
null반환 중 하나로 처리하는 방어 로직을 추가하는 것을 권장드립니다.
- 해당 필드가 실제로 nullable일 수 있는지 한 번 확인해 보시고, nullable이라면 정책에 따라 기본값/빈 문자열/
예시(정책에 따라 반환값은 조정):
String majorName = certificationComment.getUser().getMajor() != null
? certificationComment.getUser().getMajor().getName()
: null;
return new CertificationCommentResponse(
certificationComment.getId(),
certificationComment.getUser().getId(),
certificationComment.getUser().getNickname(),
certificationComment.getContent(),
majorName,
userJob,
state,
certificationComment.getLikeCount(),
certificationComment.getCreatedTime(),
certificationComment.getLastModifiedTime(),
isLike
);또한 state, userJob이 도메인 상 유한한 값 집합이라면, 추후 enum 또는 값 객체로 감싸 타입 레벨에서 제약을 표현하는 것도 고려해 볼 수 있습니다.
🤖 Prompt for AI Agents
In
src/main/java/org/sopt/certi_server/domain/comment/dto/response/CertificationCommentResponse.java
around lines 8 to 40, the mapping accesses
certificationComment.getUser().getMajor().getName() directly which can NPE if
User.major is nullable; update the from(...) method to defensively read major
(check for null) and supply a safe value per project policy (e.g., null, empty
string, or a default name) before constructing the CertificationCommentResponse
so the code never dereferences a null major.
...ain/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentRepository.java
Outdated
Show resolved
Hide resolved
| public Page<CertificationCommentResponse> getCommentsByCertification( | ||
| final Long userId, | ||
| final Long certificationId, | ||
| final Pageable pageble | ||
| ){ | ||
| Page<CertificationComment> commentPage = certificationCommentRepository.findByCertificationId(certificationId, pageble); | ||
|
|
||
| List<User> users = commentPage.getContent().stream() | ||
| .map(CertificationComment::getUser) | ||
| .filter(Objects::nonNull) | ||
| .distinct() | ||
| .toList(); | ||
|
|
||
| // 사용자 희망 직무 정보 조회 | ||
| List<UserJob> userJobs = userJobRepository.findWithJobByUserIn(users); | ||
|
|
||
| // 사용자 취득 예정 정보 조회 | ||
| List<UserPreCertification> userPreCertifications = userPreCertificationRepository.findByCertificationUserIn(certificationId, users); | ||
|
|
||
| // 사용자 취득 완료 정보 조회 | ||
| List<Acquisition> acquisitions = acquisitionRepository.findByCertificationUserIn(certificationId, users); | ||
|
|
||
| // 조회 API 호출자가 해당 댓글에 좋아요 눌렀는지 여부 | ||
| User caller = userService.getUser(userId); | ||
| Certification certification = certificationService.getCertification(certificationId); | ||
|
|
||
| List<Long> likeCommentIds = certificationCommentLikeRepository.findLikedCommentIdsByCertificationAndUser(caller, certification); | ||
|
|
||
| // O(n) 조회를 위해 Set으로 변환 | ||
| Set<Long> likeCommentIdsSet = new HashSet<>(likeCommentIds); | ||
|
|
||
|
|
||
| // DTO 조립을 위한 Map 생성 (O(1) 조회를 위함) | ||
|
|
||
| // Job Map 생성 (Key: userId, Value: List<JobName>) | ||
| Map<Long, List<String>> userJobMap = userJobs.stream() | ||
| .collect(Collectors.groupingBy( | ||
| userJob -> userJob.getUser().getId(), | ||
| Collectors.mapping( | ||
| userJob -> userJob.getJob().getName(), // job 이름 (가정) | ||
| Collectors.toList() | ||
| ) | ||
| )); | ||
|
|
||
| // State Map 생성 (Key: userId, Value: "취득 예정" or "취득 완료") | ||
| Map<Long, String> userStateMap = new HashMap<>(); | ||
| userPreCertifications.forEach(upc -> | ||
| userStateMap.put(upc.getUser().getId(), "취득 예정") | ||
| ); | ||
| acquisitions.forEach(acq -> | ||
| userStateMap.put(acq.getUser().getId(), "취득 완료") | ||
| ); | ||
|
|
||
| // [5단계] DTO 최종 조립 | ||
| return commentPage.map(comment -> { | ||
| User user = comment.getUser(); | ||
| boolean isLike = likeCommentIdsSet.contains(comment.getId()); | ||
| if (user == null) { // (알수없음) 탈퇴 사용자 처리 | ||
| return CertificationCommentResponse.from(comment, null, null, isLike); | ||
| } | ||
|
|
||
| // 4-1. Job Map에서 조회 | ||
| List<String> jobNames = userJobMap.getOrDefault(user.getId(), Collections.emptyList()); | ||
|
|
||
| // 4-2. State Map에서 조회 | ||
| String state = userStateMap.get(user.getId()); // 없으면 null | ||
|
|
||
| return CertificationCommentResponse.from(comment, state, jobNames.get(0), isLike); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
userJob 미보유 사용자에서 jobNames.get(0) 로 인한 예외 가능성
getCommentsByCertification 의 DTO 매핑 부분에서, 직무 정보가 없는 사용자에 대해 IndexOutOfBoundsException 이 발생할 수 있습니다.
List<String> jobNames = userJobMap.getOrDefault(user.getId(), Collections.emptyList());
String state = userStateMap.get(user.getId());
return CertificationCommentResponse.from(comment, state, jobNames.get(0), isLike);userJobMap에 해당 userId 가 없으면Collections.emptyList()가 반환되고,- 그 상태에서
jobNames.get(0)을 호출하면 런타임 예외가 발생합니다.
직무 정보가 없는 경우 null 을 내려주거나, 기본값을 내려주는 쪽으로 방어 코드를 추가하는 것이 안전합니다. 예:
- List<String> jobNames = userJobMap.getOrDefault(user.getId(), Collections.emptyList());
-
- String state = userStateMap.get(user.getId()); // 없으면 null
-
- return CertificationCommentResponse.from(comment, state, jobNames.get(0), isLike);
+ List<String> jobNames = userJobMap.getOrDefault(user.getId(), Collections.emptyList());
+ String state = userStateMap.get(user.getId()); // 없으면 null
+
+ String jobName = jobNames.isEmpty() ? null : jobNames.get(0);
+ return CertificationCommentResponse.from(comment, state, jobName, isLike);이 부분은 실제 서비스에서 직무 미설정 유저가 댓글을 남길 경우 바로 장애로 이어질 수 있으니, 배포 전에 꼭 한 번 수정해 두시는 것을 추천드립니다.
작업을 진행하겠습니다. 코드베이스에서 해당 파일을 찾아 리뷰 코멘트의 문제를 검증하겠습니다.
🏁 Script executed:
#!/bin/bash
# Find and verify the CertificationCommentService file
fd -type f -name "CertificationCommentService.java" --exec cat {} \;Length of output: 170
🏁 Script executed:
#!/bin/bash
# Search for CertificationCommentResponse class and its from() method signature
fd -type f -name "CertificationCommentResponse.java" --exec cat {} \;Length of output: 170
🏁 Script executed:
#!/bin/bash
# Find userJobRepository to understand the query logic
fd -type f -name "*UserJobRepository*" --exec cat {} \;Length of output: 170
🏁 Script executed:
#!/bin/bash
# Search for any null checks or defensive coding around jobNames.get(0)
rg -n "jobNames\.get\(0\)" --type java -A5 -B5Length of output: 1386
🏁 Script executed:
#!/bin/bash
# Check if there are any test cases covering this scenario
fd -type f -name "*.java" -path "*/test/*" -exec rg -l "getCommentsByCertification" {} \;Length of output: 170
🏁 Script executed:
#!/bin/bash
# Search for Collections.emptyList() usage related to jobNames
rg -n "Collections\.emptyList()" --type java -B5 -A5 | head -50Length of output: 2732
🏁 Script executed:
#!/bin/bash
# Check the CertificationCommentResponse.from method for nullability
rg -n "public static.*from.*jobName" --type java -B2 -A10Length of output: 0
🏁 Script executed:
#!/bin/bash
# Find all usages of userJobMap.getOrDefault to check patterns
rg -n "userJobMap\.getOrDefault" --type java -B3 -A3Length of output: 926
🏁 Script executed:
#!/bin/bash
# Search for the actual from method signature in CertificationCommentResponse
rg -n "CertificationCommentResponse.*from" --type java -A15 | head -100Length of output: 7435
🏁 Script executed:
#!/bin/bash
# Verify the repository query - does findWithJobByUserIn ever return incomplete data?
rg -n "findWithJobByUserIn" --type java -B5 -A10Length of output: 3062
🏁 Script executed:
#!/bin/bash
# Check if there's any List<String> jobName or similar parameter definitions
rg -n "jobNames\.get\(0\)|jobNames\.stream()|jobNames\.isEmpty()" --type java -B2 -A2Length of output: 664
🏁 Script executed:
#!/bin/bash
# Look for actual instantiation of CertificationCommentResponse to see how jobName is passed
rg -n "CertificationCommentResponse\.from" --type java -B10 -A2 | grep -E "(jobNames|state|isLike)" -A5 -B5Length of output: 2531
🏁 Script executed:
#!/bin/bash
# Find where CertificationCommentResponse is instantiated to confirm parameter expectations
ast-grep --pattern 'CertificationCommentResponse.from($comment, $state, $_, $isLike)'Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check the from method in CertificationCommentResponse more carefully
rg -n "public static CertificationCommentResponse from" --type java -A20Length of output: 2863
🏁 Script executed:
#!/bin/bash
# Verify file line counts to ensure the snippet matches actual file
wc -l src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java 2>/dev/null || echo "File not found in expected location"Length of output: 95
🏁 Script executed:
#!/bin/bash
# Get the specific lines mentioned in the review
sed -n '85,154p' src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java 2>/dev/null || echo "Could not retrieve lines"Length of output: 2931
🏁 Script executed:
#!/bin/bash
# Search for the exact line with jobNames.get(0) in context
rg -n "return CertificationCommentResponse.from.*jobNames.get" --type java -B10 -A2Length of output: 1755
🏁 Script executed:
#!/bin/bash
# Find all related comment-related response classes
fd -type f -name "*CommentResponse*.java" --exec basename {} \;Length of output: 170
🏁 Script executed:
#!/bin/bash
# Check the entire file structure around the reported issue
rg -n "jobNames = userJobMap.getOrDefault" --type java -A5 -B5Length of output: 1545
🏁 Script executed:
#!/bin/bash
# Verify if there are try-catch blocks around this code
rg -n "try|catch.*IndexOutOfBounds|catch.*Exception" --type java -B3 -A3 | grep -A10 -B10 "jobNames\|getCommentsByCertification"Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for the complete context of the problematic line
rg -n "jobNames\.isEmpty\(\)|jobNames\.get" --type java -B5 -A5Length of output: 1386
🏁 Script executed:
#!/bin/bash
# Check if the fix mentioned in review is already applied
rg -n "String jobName = jobNames.isEmpty" --type javaLength of output: 0
🏁 Script executed:
#!/bin/bash
# Final check: Look for the current state of line around 140-150 where the get(0) call should be
rg -n "CertificationCommentResponse.from.*comment.*state.*jobNames" --type java -B2 -A2Length of output: 664
🏁 Script executed:
#!/bin/bash
# Double-check: get exact snippet from the service file
rg -n "jobNames\.getOrDefault|jobNames\.get\(0\)|String jobName.*isEmpty" --type java -B3 -A3 | head -50Length of output: 892
🏁 Script executed:
#!/bin/bash
# Comprehensive search for the problematic code pattern
rg -nU "getOrDefault.*Collections\.emptyList.*jobNames\.get\(0\)" --type javaLength of output: 0
🏁 Script executed:
#!/bin/bash
# Search for the pattern across the codebase
rg -n "\.get\(0\)" --type java | grep -i "jobname\|job"Length of output: 597
🏁 Script executed:
#!/bin/bash
# Check comment repository and find all instances
rg -n "class CertificationCommentService" --type java -A200 | head -250Length of output: 33465
🏁 Script executed:
#!/bin/bash
# Simple direct file check
cat src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java | sed -n '140,150p'Length of output: 510
🏁 Script executed:
#!/bin/bash
# Examine the entire service method to understand the complete flow
rg -n "public Page<CertificationCommentResponse> getCommentsByCertification" --type java -A70 | head -100Length of output: 9733
🏁 Script executed:
#!/bin/bash
# Check if jobNames.get(0) exists anywhere in the codebase
rg -n "\.get\(0\)" --type java -C2 | grep -B10 -A10 "jobNames"Length of output: 1520
🏁 Script executed:
#!/bin/bash
# Look at all files in the comment service directory
find . -path "*/comment/service/*.java" -type f 2>/dev/null | head -20Length of output: 191
🏁 Script executed:
#!/bin/bash
# Get structure of comment module
find . -path "*/comment/*" -name "*.java" -type f 2>/dev/null | grep -E "(Service|Response|Repository)" | head -30Length of output: 495
🏁 Script executed:
#!/bin/bash
# Check if the response class accepts String or other types for job parameter
rg -n "class CertificationCommentResponse" --type java -A50Length of output: 0
🏁 Script executed:
#!/bin/bash
# Final verification - search all Java files for the exact problematic pattern
rg -n "getOrDefault.*emptyList.*get\(0\)" --type javaLength of output: 0
🏁 Script executed:
#!/bin/bash
# Look for the original issue pattern anywhere in repository
rg -n "Collections\.emptyList\(\).*get\(0\)" --type java -B5 -A5Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search by the method name and response class
rg -n "getCommentsByCertification" --type javaLength of output: 830
🏁 Script executed:
#!/bin/bash
# Now search the exact file mentioned in the review
rg -n "." src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java 2>/dev/null | tail -100Length of output: 5242
🏁 Script executed:
#!/bin/bash
# Simpler approach: list directory structure
ls -la src/main/java/org/sopt/certi_server/domain/comment/service/ 2>/dev/null || find . -name "CertificationCommentService.java" -type fLength of output: 193
🏁 Script executed:
#!/bin/bash
# Find the project root and structure
find . -name "CertificationCommentService.java" -type f 2>/dev/nullLength of output: 93
🏁 Script executed:
#!/bin/bash
# List all Java files in comment package
find . -name "*.java" -path "*comment*" -type f 2>/dev/null | head -20Length of output: 864
🏁 Script executed:
#!/bin/bash
# Try another approach - find all service files
find . -name "*Service.java" -type f 2>/dev/null | grep commentLength of output: 93
🏁 Script executed:
#!/bin/bash
# Search in the entire repository for the code pattern
rg "jobNames\.get\(0\)" 2>/dev/nullLength of output: 186
🏁 Script executed:
#!/bin/bash
# List repository structure
ls -la 2>/dev/null | head -20Length of output: 819
🏁 Script executed:
#!/bin/bash
# Try to find Java source files
find . -type f -name "*.java" 2>/dev/null | head -10Length of output: 921
🏁 Script executed:
#!/bin/bash
# Check pwd and available files
pwd && ls -laLength of output: 838
🏁 Script executed:
#!/bin/bash
# If repo exists, search for CertificationCommentService
find . -type d -name "comment" 2>/dev/null | head -5Length of output: 105
🏁 Script executed:
#!/bin/bash
# Very simple test - does the repository have any Java files?
find . -type f -name "*.java" -print -quit 2>/dev/null && echo "Java files found" || echo "No Java files"Length of output: 82
🏁 Script executed:
#!/bin/bash
# Show directory tree structure
ls -R | head -50Length of output: 524
🏁 Script executed:
#!/bin/bash
# Check if there's a pom.xml or build structure
find . -type f \( -name "pom.xml" -o -name "build.gradle" \) 2>/dev/nullLength of output: 14
🏁 Script executed:
#!/bin/bash
# Just output current state
echo "Current directory:" && pwd && echo "Contents:" && ls -aLength of output: 172
🏁 Script executed:
#!/bin/bash
# Final check: search by specific keyword in all files
grep -r "getCommentsByCertification" . 2>/dev/null | head -5Length of output: 824
🏁 Script executed:
#!/bin/bash
# Search for jobNames anywhere
grep -r "jobNames\.get" . 2>/dev/null | head -10Length of output: 188
🏁 Script executed:
#!/bin/bash
# Verify with basic file search
grep -r "CertificationCommentService" . 2>/dev/null | head -3Length of output: 492
🏁 Script executed:
#!/bin/bash
# Check if cloned files exist
test -f src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java && echo "File exists" || echo "File not found"Length of output: 11
🏁 Script executed:
#!/bin/bash
# List the structure
find . -type f -name "CertificationCommentService.java"Length of output: 93
직무 정보 없는 사용자로부터 jobNames.get(0) 호출 시 IndexOutOfBoundsException 발생
152번 줄에서 직무 정보가 없는 사용자에 대해 IndexOutOfBoundsException이 발생할 수 있습니다.
List<String> jobNames = userJobMap.getOrDefault(user.getId(), Collections.emptyList());userJobMap에 해당 userId가 없으면 Collections.emptyList()가 반환되고, 그 상태에서 jobNames.get(0)을 호출하면 런타임 예외가 발생합니다. 직무 정보가 없는 경우를 방어하기 위해 다음과 같이 수정하세요:
String jobName = jobNames.isEmpty() ? null : jobNames.get(0);
return CertificationCommentResponse.from(comment, state, jobName, isLike);배포 전에 반드시 수정하시기 바랍니다.
🤖 Prompt for AI Agents
In
src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java
around lines 85 to 154, calling jobNames.get(0) can throw
IndexOutOfBoundsException when the user has no job entries; change the code to
check if jobNames is empty (or null) and set String jobName = jobNames.isEmpty()
? null : jobNames.get(0) (or use a null-safe check) and pass jobName into
CertificationCommentResponse.from(...) instead of calling jobNames.get(0)
directly.
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
IN :users 사용 시 빈 리스트/대용량 리스트에 대한 방어 로직 고려
UserJob에서 job과 user를 함께 fetch join 하는 패턴 자체는 댓글 응답 등에 필요한 연관 데이터를 한 번에 가져오기 좋아 보입니다. 다만 uj.user IN :users 에서 users가 빈 리스트가 되면 JPA 구현체에 따라 예외가 발생할 수 있고, users 크기가 커지면 IN 절이 매우 길어져 성능에 영향을 줄 수 있습니다. 호출부에서 users가 비어 있을 경우 쿼리를 건너뛰거나, 유저 수가 많아질 수 있는 시나리오라면 적절한 배치 사이즈로 나누어 호출하는 등 방어 로직을 검토해 주세요.
Also applies to: 16-20
🏁 Script executed:
#!/bin/bash
# findWithJobByUserIn 사용 위치와 users 크기 확인용 검색
rg -n "findWithJobByUserIn" -C3Length of output: 1587
추가 컨텍스트가 필요합니다. CertificationCommentService의 더 넓은 범위를 확인하겠습니다.
---
🏁 Script executed:
```shell
#!/bin/bash
# CertificationCommentService에서 users 생성 및 호출 전체 컨텍스트 확인
rg -n "users" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -B15 -A5 | grep -A20 "toList"
Length of output: 689
🏁 Script executed:
#!/bin/bash
# CertificationCommentService 파일 전체 구조 파악 (메서드 정의 및 users 관련 라인)
cat src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java | head -120 | tail -50Length of output: 1803
🏁 Script executed:
#!/bin/bash
# 빈 리스트 체크 또는 배치 처리 패턴이 있는지 확인
rg -n "isEmpty|size.*>\|batch" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -C3Length of output: 0
🏁 Script executed:
#!/bin/bash
# 댓글 조회 시 유저 수의 상한이나 페이징 정보 확인
rg -n "findByCertification|limit|page|Pageable" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -B3 -A3Length of output: 1484
빈 users 리스트 방어 로직 추가 필요
commentPage.getContent()에서 추출한 users가 빈 리스트가 될 수 있습니다. 댓글이 없거나 모든 댓글의 user가 null인 경우 이 상황이 발생하며, IN :users 절에 빈 리스트를 전달하면 JPA 구현체에 따라 예외가 발생할 수 있습니다.
findWithJobByUserIn(users) 호출 전에 users.isEmpty() 체크를 추가하고, 비어있을 경우 빈 리스트를 반환하도록 처리해주세요. (같은 패턴이 findByCertificationUserIn과 findByCertificationUserIn 호출에도 적용될 수 있으므로 일괄 검토 권장)
🤖 Prompt for AI Agents
In
src/main/java/org/sopt/certi_server/domain/user/repository/UserJobRepository.java
around lines 6 to 7, add a defensive check for an empty users list before
calling repository methods that use IN :users; if users.isEmpty() return an
empty list immediately to avoid passing an empty collection to JPA (which can
throw depending on implementation). Apply the same empty-check/early-return
pattern to other places that call findByCertificationUserIn or similar methods
that pass collections into IN clauses to prevent runtime errors.
| import org.sopt.certi_server.domain.userprecertification.entity.UserPreCertification; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
findByCertificationUserIn의 IN 파라미터 및 fetch 전략 점검 제안
특정 자격증에 대해 여러 유저의 UserPreCertification을 한 번에 조회하는 용도로 잘 맞는 쿼리처럼 보입니다. 다만 upc.user IN :users에서 users가 빈 리스트가 되는 상황이 발생하면 JPA 구현체에 따라 예외가 나거나 항상 false로 평가될 수 있으므로, 호출부에서 비어 있는 경우에는 이 메서드를 호출하지 않도록 하거나 별도 분기 처리하는 편이 안전합니다. 또한 현재는 user만 fetch join 하고 있어, 결과를 사용할 때 upc.getUser() 접근에는 N+1이 없지만, 같은 결과에서 upc.getCertification()까지 자주 접근한다면 certification도 함께 fetch join 할 필요가 있는지 한 번 검토해 주세요.
Also applies to: 37-43
🏁 Script executed:
#!/bin/bash
# findByCertificationUserIn 호출 위치 및 users 전달 방식 확인
rg -n "findByCertificationUserIn" -C3Length of output: 3595
호출부 컨텍스트를 더 자세히 확인하기 위해 CertificationCommentService의 메서드 구조를 검토하겠습니다.
[/run_scripts]
🏁 Script executed:
#!/bin/bash
# CertificationCommentService에서 users 리스트 생성 및 검증 로직 확인
rg -n -B20 "findByCertificationUserIn" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java | head -40Length of output: 1067
CertificationCommentService의 getCommentsByCertification 메서드에서 users 리스트 검증 추가 필요
페이지 결과가 비어있을 때 users 리스트도 빈 상태로 findByCertificationUserIn 메서드에 전달될 수 있습니다. 이 경우 JPA 구현체의 IN 절 처리 방식에 따라 예상치 못한 동작이 발생할 수 있으므로, 호출 전 users가 비어있는지 확인한 후 해당 조건에 맞게 처리하는 것이 안전합니다.
예를 들어:
if (!users.isEmpty()) {
List<UserPreCertification> userPreCertifications = userPreCertificationRepository.findByCertificationUserIn(certificationId, users);
// 처리
} else {
userPreCertifications = Collections.emptyList();
}마찬가지로 acquisitionRepository.findByCertificationUserIn 호출도 동일한 방식으로 보호하는 것을 권장합니다.
🤖 Prompt for AI Agents
src/main/java/org/sopt/certi_server/domain/userprecertification/repository/UserPreCertificationRepository.java
around line 8: CertificationCommentService의 getCommentsByCertification에서 페이징 결과가
비어있을 때 users 리스트가 빈 상태로 findByCertificationUserIn에 전달되어 JPA의 IN 절 처리로 예기치 않은 동작이
발생할 수 있으니, findByCertificationUserIn(또는
acquisitionRepository.findByCertificationUserIn)을 호출하기 전에 users.isEmpty()로 검사하여
비어있으면 빈 리스트(Collections.emptyList())를 반환하거나 해당 분기 처리 하도록 방어 코드를 추가하세요.
- 에러 코드 수정 - 잘못된 import 수정 - request dto validation 추가
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/main/java/org/sopt/certi_server/domain/comment/dto/request/CommentRegisterRequest.java (1)
7-11: 이전 리뷰 피드백이 반영되었습니다.
certificationId에@NotNull검증이 추가되어 이전 리뷰에서 지적된 주요 이슈가 해결되었습니다. 이제 클라이언트가 ID 없이 요청하면 검증 단계에서 적절히 차단됩니다.추가로 고려할 사항:
content필드에@NotEmpty대신@NotBlank를 사용하면 공백만 있는 댓글도 차단할 수 있습니다. 현재는" "같은 입력이 통과할 수 있습니다.선택적으로 다음과 같이 개선할 수 있습니다:
- @NotEmpty(message = "댓글의 내용은 필수입니다.") @Size(message = "댓글의 크기는 100자를 넘을 수 없습니다.", max = 100) String content, + @NotBlank(message = "댓글의 내용은 필수입니다.") @Size(message = "댓글의 크기는 100자를 넘을 수 없습니다.", max = 100) String content,이 경우 임포트도 업데이트가 필요합니다:
-import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotBlank;src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java (1)
40-47: 에러 메시지 마침표 사용 일관성 개선 고려새로 추가된 에러 코드들의 메시지 중 일부는 마침표로 끝나고(43, 44, 46, 47번 라인) 일부는 마침표가 없습니다(42, 45번 라인). 이는 파일 전체에 존재하는 기존 문제이지만, 새로운 코드를 추가할 때 일관성을 맞추는 것을 고려해보세요. 전체 파일의 메시지를 마침표로 통일하거나 제거하는 것이 좋습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/org/sopt/certi_server/domain/comment/dto/request/CommentRegisterRequest.java(1 hunks)src/main/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentRepository.java(1 hunks)src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentRepository.java
🔇 Additional comments (2)
src/main/java/org/sopt/certi_server/domain/comment/dto/request/CommentRegisterRequest.java (1)
1-5: 임포트와 패키지 구조가 적절합니다.Jakarta validation 어노테이션이 올바르게 임포트되었고, 패키지 구조도 도메인 구조를 잘 따르고 있습니다.
src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java (1)
39-39: 코드 포맷 수정 승인에러 코드에서 선행 공백을 제거한 것이 올바릅니다. 이는 다른 에러 코드들과의 일관성을 보장합니다.
| REFRESH_TOKEN_NOT_FOUND(HttpStatus.NOT_FOUND, "E40415", "존재하지 않는 리프레시 토큰입니다"), | ||
| COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "E40416", "존재하지 않는 댓글입니다."), | ||
| MISSING_HANDLER(HttpStatus.NOT_FOUND, "E40417", "URL을 찾을 수 없습니다."), | ||
| COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40418", "이미 취소된 좋아요입니다."), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상수명과 에러 메시지 간 의미 불일치
COMMENT_LIKE_NOT_FOUND라는 상수명은 "댓글 좋아요를 찾을 수 없음"을 의미하지만, 에러 메시지는 "이미 취소된 좋아요입니다"로 되어 있습니다. 이는 의미적으로 일치하지 않습니다:
NOT_FOUND는 일반적으로 리소스가 존재하지 않는 경우를 의미합니다- "이미 취소된"은 리소스가 이전에 존재했다가 취소되었음을 암시합니다
이 불일치는 개발자가 이 에러 코드를 사용할 때 혼란을 야기할 수 있습니다. 다음 중 하나로 수정하는 것을 권장합니다:
옵션 1: 상수명을 메시지에 맞게 변경
- COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40418", "이미 취소된 좋아요입니다."),
+ COMMENT_LIKE_ALREADY_CANCELLED(HttpStatus.BAD_REQUEST, "E40018", "이미 취소된 좋아요입니다."),옵션 2: 메시지를 상수명에 맞게 변경
- COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40418", "이미 취소된 좋아요입니다."),
+ COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40418", "존재하지 않는 좋아요입니다."),📝 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.
| COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40418", "이미 취소된 좋아요입니다."), | |
| COMMENT_LIKE_ALREADY_CANCELLED(HttpStatus.BAD_REQUEST, "E40018", "이미 취소된 좋아요입니다."), |
| COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40418", "이미 취소된 좋아요입니다."), | |
| COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40418", "존재하지 않는 좋아요입니다."), |
🤖 Prompt for AI Agents
In src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java around
line 48, the enum constant name COMMENT_LIKE_NOT_FOUND conflicts with its
message "이미 취소된 좋아요입니다." — choose one fix: either rename the constant to match
the message (e.g., COMMENT_LIKE_ALREADY_CANCELLED or
COMMENT_LIKE_ALREADY_UNLIKED) across the codebase and update all usages/tests,
or change the message to match the name (e.g., "댓글 좋아요를 찾을 수 없습니다."); apply the
chosen change consistently and run tests to ensure no references break.
There was a problem hiding this 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
🧹 Nitpick comments (2)
src/main/java/org/sopt/certi_server/domain/comment/controller/CertificationCommentController.java (2)
40-42: 파라미터 순서 일관성 개선 권장메서드 간 파라미터 순서가 일관되지 않습니다:
registerCertificationComment: (요청 바디, userId)deleteCertificationComment: (userId, commentId)일관성을 위해 모든 메서드에서
@AuthenticationPrincipal파라미터를 첫 번째 위치에 배치하는 것을 권장합니다.public ResponseEntity<SuccessResponse<Void>> registerCertificationComment( + @AuthenticationPrincipal Long userId, @RequestBody CommentRegisterRequest commentRegisterRequest, - @AuthenticationPrincipal Long userId ){
67-75: 좋아요 토글 API의 응답 코드 검토 권장
toggleCommentLike가SUCCESS_UPDATE를 반환하고 있습니다. 토글 동작의 경우:
- 좋아요 추가 시: 새 리소스 생성 (CREATE)
- 좋아요 취소 시: 리소스 삭제 (DELETE)
따라서
SUCCESS_UPDATE보다는 일반적인SUCCESS코드를 사용하거나, 서비스 레이어에서 실제 수행된 동작(생성/삭제)을 반환받아 적절한 응답 코드를 선택하는 것이 의미상 더 명확할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/java/org/sopt/certi_server/domain/comment/controller/CertificationCommentController.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). (2)
- GitHub Check: deploy-cd
- GitHub Check: build
| @PostMapping | ||
| @Operation(summary = "댓글 등록 API", description = "댓글을 등록합니다.") | ||
| public ResponseEntity<SuccessResponse<Void>> registerCertificationComment( | ||
| @RequestBody CommentRegisterRequest commentRegisterRequest, | ||
| @AuthenticationPrincipal Long userId | ||
| ){ | ||
| certificationCommentService.registerComment(userId, commentRegisterRequest); | ||
| return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_CREATE)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
요청 바디 검증 추가 및 HTTP 상태 코드 개선 필요
CommentRegisterRequest에@Valid어노테이션이 누락되어 있어 요청 데이터 검증이 수행되지 않을 수 있습니다.- 리소스 생성 시 REST 규약에 따라
200 OK대신201 CREATED를 반환하는 것이 적절합니다.
다음 diff를 적용하여 개선하세요:
+import jakarta.validation.Valid;
+
@PostMapping
@Operation(summary = "댓글 등록 API", description = "댓글을 등록합니다.")
public ResponseEntity<SuccessResponse<Void>> registerCertificationComment(
- @RequestBody CommentRegisterRequest commentRegisterRequest,
+ @RequestBody @Valid CommentRegisterRequest commentRegisterRequest,
@AuthenticationPrincipal Long userId
){
certificationCommentService.registerComment(userId, commentRegisterRequest);
- return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_CREATE));
+ return ResponseEntity.status(201).body(SuccessResponse.of(SuccessCode.SUCCESS_CREATE));
}📝 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.
| @PostMapping | |
| @Operation(summary = "댓글 등록 API", description = "댓글을 등록합니다.") | |
| public ResponseEntity<SuccessResponse<Void>> registerCertificationComment( | |
| @RequestBody CommentRegisterRequest commentRegisterRequest, | |
| @AuthenticationPrincipal Long userId | |
| ){ | |
| certificationCommentService.registerComment(userId, commentRegisterRequest); | |
| return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_CREATE)); | |
| } | |
| @PostMapping | |
| @Operation(summary = "댓글 등록 API", description = "댓글을 등록합니다.") | |
| public ResponseEntity<SuccessResponse<Void>> registerCertificationComment( | |
| @RequestBody @Valid CommentRegisterRequest commentRegisterRequest, | |
| @AuthenticationPrincipal Long userId | |
| ){ | |
| certificationCommentService.registerComment(userId, commentRegisterRequest); | |
| return ResponseEntity.status(201).body(SuccessResponse.of(SuccessCode.SUCCESS_CREATE)); | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/org/sopt/certi_server/domain/comment/controller/CertificationCommentController.java
around lines 28 to 36, the request body is not validated and the method returns
200 OK for a created resource; add javax.validation validation by annotating the
CommentRegisterRequest parameter with @Valid (i.e. @RequestBody @Valid
CommentRegisterRequest commentRegisterRequest) so Spring performs bean
validation, and change the response to return HTTP 201 Created (for example use
ResponseEntity.status(HttpStatus.CREATED).body(SuccessResponse.of(SuccessCode.SUCCESS_CREATE)))
to follow REST conventions.
| @PageableDefault( | ||
| page = 0, | ||
| size = 10, | ||
| sort = "id", | ||
| direction = Sort.Direction.DESC | ||
| ) final Pageable pageable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
정렬 기준 필드 검증 필요
기본 정렬 기준이 "id"로 설정되어 있습니다. PR 요구사항에 따르면 최신순과 인기순 조회가 필요한데:
- 최신순의 경우
"createdAt"필드로 정렬하는 것이 더 명확합니다. - 인기순의 경우 좋아요 수 필드(예:
"likeCount")로 정렬해야 하는데, 해당 필드가 Entity에 존재하는지 확인이 필요합니다.
클라이언트가 sort 파라미터를 통해 정렬 기준을 지정할 수 있는지, 그리고 Entity에 적절한 정렬 필드가 존재하는지 확인하세요.
다음 스크립트를 실행하여 Comment Entity의 정렬 가능한 필드를 확인하세요:
🏁 Script executed:
#!/bin/bash
# Description: Verify that Comment entity has appropriate fields for sorting (createdAt, likeCount)
# Search for Comment entity definition
fd -e java -x grep -l "class.*Comment.*{" | xargs cat | grep -A 30 "class.*Comment"
# Search for fields related to timestamp and like count
rg -n -C3 "createdAt|likeCount|created_at|like_count" --type java -g "*Comment*.java"Length of output: 14130
---
🏁 Script executed:
```shell
#!/bin/bash
# Find BaseTimeEntity definition to confirm exact timestamp field names
fd -e java -path "*entity*" -name "BaseTimeEntity.java" -x cat
# Also search for any BaseTimeEntity references
rg -n "class BaseTimeEntity" --type java
Length of output: 280
🏁 Script executed:
#!/bin/bash
# Check for field names used in BaseTimeEntity
rg -n "@CreationTimestamp|@Column.*created|@Column.*modified" --type java | head -20Length of output: 120
---
🏁 Script executed:
```shell
#!/bin/bash
# Get BaseTimeEntity content to see exact field names
cat src/main/java/org/sopt/certi_server/global/entity/BaseTimeEntity.java
Length of output: 620
기본 정렬 필드를 "createdTime"으로 변경 권장
CertificationComment 엔티티에서 확인 결과:
createdTime필드가 존재하여 최신순 정렬에 사용 가능likeCount필드가 존재하여 인기순 정렬에 사용 가능
현재 기본값인 "id" 정렬은 기능상 동작하지만, 의도가 명확하지 않습니다. 다음과 같이 수정하세요:
@PageableDefault(
page = 0,
size = 10,
sort = "createdTime",
direction = Sort.Direction.DESC
) final Pageable pageable
이렇게 하면 최신순이 명시적으로 드러나며, 클라이언트는 sort 파라미터로 likeCount를 지정하여 인기순 조회가 가능합니다.
🤖 Prompt for AI Agents
In
src/main/java/org/sopt/certi_server/domain/comment/controller/CertificationCommentController.java
around lines 53 to 58, change the PageableDefault sort from "id" to
"createdTime" so the default ordering is explicitly by creation time descending;
update the annotation to use sort = "createdTime" while keeping page=0, size=10
and direction=Sort.Direction.DESC so clients can still override sorting (e.g.,
likeCount) via the sort parameter.
📣 Related Issue
📝 Summary
자격증 댓글 기능(등록, 삭제, 조회, 좋아요)를 구현합니다.
🙏 Question & PR point
📬 Postman
Summary by CodeRabbit
새로운 기능
개선 사항
테스트
기타