Skip to content

Conversation

@seongmin0229
Copy link
Contributor

@seongmin0229 seongmin0229 commented Nov 15, 2025

📣 Related Issue

📝 Summary

자격증 댓글 기능(등록, 삭제, 조회, 좋아요)를 구현합니다.

🙏 Question & PR point

  • 댓글 좋아요 개수에 대한 동시성 테스트가 좀 더 필요합니다.
  • 신고 기능에 대해 와이어 프레임에 자세히 명시가 돼있지 않아 아직 구현하지 않았습니다.

📬 Postman

  • 댓글 등록 API
image
  • 댓글 조회 API(최신순)
image
  • 댓글 조회 API(인기순)
image
  • 댓글 좋아요/좋아요 취소 API
image
  • 댓글 삭제 API
image

Summary by CodeRabbit

  • 새로운 기능

    • 인증서 댓글 등록/삭제/목록 조회 및 댓글 좋아요 토글 API 추가
    • 댓글 응답에 페이징 포맷(PageResponse) 도입
  • 개선 사항

    • 댓글 목록에 작성자 직무·취득 상태·좋아요 여부 포함
    • 오류 코드에 댓글 관련 NOT_FOUND 항목 추가
  • 테스트

    • 댓글 서비스 통합 테스트 추가
  • 기타

    • 테스트 빌드 설정에 Lombok 지원 추가

@coderabbitai
Copy link

coderabbitai bot commented Nov 15, 2025

Walkthrough

자격증 댓글(엔티티·좋아요·리포지토리 쿼리·서비스·컨트롤러)과 페이징 DTO, 에러코드 항목 추가 및 테스트용 Lombok 의존성 추가가 적용되었습니다.

Changes

Cohort / File(s) 변경 내용
의존성 및 빌드 설정
build.gradle
테스트에서 Lombok 사용을 위한 testImplementation 'org.projectlombok:lombok:1.18.28'testAnnotationProcessor 'org.projectlombok:lombok' 추가
댓글 엔티티
src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationComment.java, src/main/java/org/sopt/certi_server/domain/comment/entity/CertificationCommentLike.java
CertificationCommentCertificationCommentLike 엔티티 추가(테이블 매핑, 관계, unique 제약 등)
댓글 리포지토리
src/main/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentRepository.java, src/main/java/org/sopt/certi_server/domain/comment/repository/CertificationCommentLikeRepository.java
댓글 페이징 조회(fetch join 포함), 좋아요 원자적 증감 @Modifying 쿼리 및 좋아요 조회/존재/삭제/사용자별 좋아요 ID 조회 쿼리 추가
댓글 서비스 및 컨트롤러
src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java, src/main/java/org/sopt/certi_server/domain/comment/controller/CertificationCommentController.java
댓글 등록/삭제/조회(페이지), 좋아요 토글 로직을 포함한 서비스 및 REST 컨트롤러 추가(엔드포인트: POST/DELETE/GET /api/v1/comments, POST /api/v1/comments/{id}/like)
댓글 DTO
src/main/java/org/sopt/certi_server/domain/comment/dto/request/CommentRegisterRequest.java, src/main/java/org/sopt/certi_server/domain/comment/dto/response/CertificationCommentResponse.java
요청 레코드(content, certificationId 유효성 포함) 및 응답 레코드(댓글 정보 + isLike 등) 추가
기존 리포지토리 확장
src/main/java/org/sopt/certi_server/domain/acquisition/repository/AcquisitionRepository.java, src/main/java/org/sopt/certi_server/domain/user/repository/UserJobRepository.java, src/main/java/org/sopt/certi_server/domain/userprecertification/repository/UserPreCertificationRepository.java
여러 사용자 대상 fetch-join JPQL 메서드(findByCertificationUserIn, findWithJobByUserIn) 및 @Param 추가
공통 응답 및 에러 코드
src/main/java/org/sopt/certi_server/global/error/dto/PageResponse.java, src/main/java/org/sopt/certi_server/global/error/code/ErrorCode.java
제네릭 페이징 DTO PageResponse<T> 추가 및 NOT_FOUND 계열 에러코드 항목(CERTIFICATION_NOT_FOUND, COMMENT_NOT_FOUND, COMMENT_LIKE_NOT_FOUND 등) 확장/재배치
기타 변경
src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java, src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java
tags 리스트를 스트림 복사로 생성(불변 사본), recommendCertifications(Long)에서 @Cacheable 제거
테스트
src/test/java/org/sopt/certi_server/domain/comment/service/CertificationCommentServiceTest.java
댓글 서비스 통합 테스트 추가(등록·삭제·좋아요 토글·페이징 조회 검증)

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • 주의 깊게 검토할 항목:
    • CertificationCommentService.getCommentsByCertification: 다중 리포지토리 조회·매핑·성능
    • CertificationCommentRepository의 @Modifying 업데이트 쿼리(동시성·원자성)
    • CertificationCommentLike 엔티티의 unique 제약 및 삭제 로직
    • ErrorCode enum에 새 코드 추가로 인한 코드값 일관성

Possibly related PRs

Suggested reviewers

  • woals2840

Poem

🐇 깡충깡충 코드밭에 왔네,
댓글 씨앗 심어 좋아요도 톡톡,
테스트로 다져진 길 따라 뛴다네,
DTO·쿼리·엔티티가 반짝 웃네,
햅삐 버니가 박수로 축하할게 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues check ❓ Inconclusive PR은 #166 이슈의 대부분 요구사항을 구현했습니다: 댓글 엔티티, CRUD 작업, 좋아요 기능, 페이징 처리. 단, 신고 기능은 미구현 상태입니다. 신고 기능 구현 상태를 명확히 하거나, 이번 PR에서 미포함된 이유를 문서화하세요.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 'CER-3 자격증 댓글 기능 구현'은 주요 변경사항인 인증서 댓글 기능 구현을 명확하게 요약하고 있습니다.
Out of Scope Changes check ✅ Passed Lombok 의존성 추가, ErrorCode 확장, 캐싱 제거 등의 변경사항이 포함되어 있으며, 이들은 댓글 기능 구현을 지원하는 보조적 범위 내 변경입니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch CER-3-자격증-댓글-API

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

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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.usernull 인 댓글(탈퇴 사용자, 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 기준으로 찾는 쪽을 권장합니다.

  • allCommentscontent = "새 댓글 내용" 인 요소를 필터링
  • 혹은 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);

다만 CertificationCommentCertificationCommentLike 연관관계에 이미 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8cc45d0 and d21b235.

📒 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;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

IN 파라미터 비어있는 경우 및 fetch join 사용 의도 확인 권장

ac.user IN :users 형태로 엔티티 리스트를 IN 조건에 그대로 넘기는 구현은 의도에 맞게 잘 작성된 것 같습니다. 다만, users가 빈 리스트가 되는 경우 JPA 구현체에 따라 예외가 발생하거나, 항상 false 조건으로 치환되는 등 동작이 달라질 수 있으니, 호출부에서 최소 한 명 이상의 유저가 담기도록 보장하거나, 비어 있을 때는 쿼리를 호출하지 않는 방어 로직을 두는 편이 안전합니다. 또한 현재는 userfetch join 하고 있어 Acquisition에서 getUser() 접근 시에는 N+1을 피할 수 있지만, 동일 결과에서 getCertification()까지 자주 접근한다면 certification에 대해서도 fetch join이 필요한지 한 번 점검해보면 좋겠습니다.

Also applies to: 37-43


🏁 Script executed:

#!/bin/bash
# findByCertificationUserIn 호출부와 users 전달 방식 확인
rg -n "findByCertificationUserIn" -C3

Length 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 -A2

Length 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 -20

Length 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 -A5

Length 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 -A2

Length 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 java

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

Comment on lines +8 to +40
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
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

응답 매핑은 명확하지만 userMajor NPE 가능성은 한 번 점검해 보는 것이 좋겠습니다

전체 필드 구성과 from 정적 메서드를 통한 매핑 방식은 직관적이고 사용하기 편해 보입니다. 다만:

  • userMajorcertificationComment.getUser().getMajor().getName()으로 바로 꺼내고 있어서, User.major가 nullable인 도메인이라면 여기서 NPE가 발생할 수 있습니다.
    • 해당 필드가 실제로 nullable일 수 있는지 한 번 확인해 보시고, nullable이라면 정책에 따라 기본값/빈 문자열/null 반환 중 하나로 처리하는 방어 로직을 추가하는 것을 권장드립니다.

예시(정책에 따라 반환값은 조정):

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.

Comment on lines +85 to +154
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);
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 -B5

Length 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 -50

Length of output: 2732


🏁 Script executed:

#!/bin/bash
# Check the CertificationCommentResponse.from method for nullability
rg -n "public static.*from.*jobName" --type java -B2 -A10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find all usages of userJobMap.getOrDefault to check patterns
rg -n "userJobMap\.getOrDefault" --type java -B3 -A3

Length of output: 926


🏁 Script executed:

#!/bin/bash
# Search for the actual from method signature in CertificationCommentResponse
rg -n "CertificationCommentResponse.*from" --type java -A15 | head -100

Length of output: 7435


🏁 Script executed:

#!/bin/bash
# Verify the repository query - does findWithJobByUserIn ever return incomplete data?
rg -n "findWithJobByUserIn" --type java -B5 -A10

Length 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 -A2

Length 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 -B5

Length 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 -A20

Length 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 -A2

Length 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 -B5

Length 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 -A5

Length of output: 1386


🏁 Script executed:

#!/bin/bash
# Check if the fix mentioned in review is already applied
rg -n "String jobName = jobNames.isEmpty" --type java

Length 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 -A2

Length 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 -50

Length of output: 892


🏁 Script executed:

#!/bin/bash
# Comprehensive search for the problematic code pattern
rg -nU "getOrDefault.*Collections\.emptyList.*jobNames\.get\(0\)" --type java

Length 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 -250

Length 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 -100

Length 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 -20

Length 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 -30

Length 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 -A50

Length 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 java

Length 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 -A5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search by the method name and response class
rg -n "getCommentsByCertification" --type java

Length 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 -100

Length 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 f

Length of output: 193


🏁 Script executed:

#!/bin/bash
# Find the project root and structure
find . -name "CertificationCommentService.java" -type f 2>/dev/null

Length 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 -20

Length of output: 864


🏁 Script executed:

#!/bin/bash
# Try another approach - find all service files
find . -name "*Service.java" -type f 2>/dev/null | grep comment

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Search in the entire repository for the code pattern
rg "jobNames\.get\(0\)" 2>/dev/null

Length of output: 186


🏁 Script executed:

#!/bin/bash
# List repository structure
ls -la 2>/dev/null | head -20

Length of output: 819


🏁 Script executed:

#!/bin/bash
# Try to find Java source files
find . -type f -name "*.java" 2>/dev/null | head -10

Length of output: 921


🏁 Script executed:

#!/bin/bash
# Check pwd and available files
pwd && ls -la

Length of output: 838


🏁 Script executed:

#!/bin/bash
# If repo exists, search for CertificationCommentService
find . -type d -name "comment" 2>/dev/null | head -5

Length 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 -50

Length 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/null

Length of output: 14


🏁 Script executed:

#!/bin/bash
# Just output current state
echo "Current directory:" && pwd && echo "Contents:" && ls -a

Length of output: 172


🏁 Script executed:

#!/bin/bash
# Final check: search by specific keyword in all files
grep -r "getCommentsByCertification" . 2>/dev/null | head -5

Length of output: 824


🏁 Script executed:

#!/bin/bash
# Search for jobNames anywhere
grep -r "jobNames\.get" . 2>/dev/null | head -10

Length of output: 188


🏁 Script executed:

#!/bin/bash
# Verify with basic file search
grep -r "CertificationCommentService" . 2>/dev/null | head -3

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

Comment on lines +6 to +7
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

IN :users 사용 시 빈 리스트/대용량 리스트에 대한 방어 로직 고려

UserJob에서 jobuser를 함께 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" -C3

Length 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 -50

Length of output: 1803


🏁 Script executed:

#!/bin/bash
# 빈 리스트 체크 또는 배치 처리 패턴이 있는지 확인
rg -n "isEmpty|size.*>\|batch" src/main/java/org/sopt/certi_server/domain/comment/service/CertificationCommentService.java -C3

Length 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 -A3

Length of output: 1484


users 리스트 방어 로직 추가 필요

commentPage.getContent()에서 추출한 users가 빈 리스트가 될 수 있습니다. 댓글이 없거나 모든 댓글의 user가 null인 경우 이 상황이 발생하며, IN :users 절에 빈 리스트를 전달하면 JPA 구현체에 따라 예외가 발생할 수 있습니다.

findWithJobByUserIn(users) 호출 전에 users.isEmpty() 체크를 추가하고, 비어있을 경우 빈 리스트를 반환하도록 처리해주세요. (같은 패턴이 findByCertificationUserInfindByCertificationUserIn 호출에도 적용될 수 있으므로 일괄 검토 권장)

🤖 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;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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" -C3

Length 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 -40

Length of output: 1067


CertificationCommentServicegetCommentsByCertification 메서드에서 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 추가
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (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

📥 Commits

Reviewing files that changed from the base of the PR and between d21b235 and 2ae4ed3.

📒 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", "이미 취소된 좋아요입니다."),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

상수명과 에러 메시지 간 의미 불일치

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.

Suggested change
COMMENT_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E40418", "이미 취소된 좋아요입니다."),
COMMENT_LIKE_ALREADY_CANCELLED(HttpStatus.BAD_REQUEST, "E40018", "이미 취소된 좋아요입니다."),
Suggested change
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 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의 응답 코드 검토 권장

toggleCommentLikeSUCCESS_UPDATE를 반환하고 있습니다. 토글 동작의 경우:

  • 좋아요 추가 시: 새 리소스 생성 (CREATE)
  • 좋아요 취소 시: 리소스 삭제 (DELETE)

따라서 SUCCESS_UPDATE보다는 일반적인 SUCCESS 코드를 사용하거나, 서비스 레이어에서 실제 수행된 동작(생성/삭제)을 반환받아 적절한 응답 코드를 선택하는 것이 의미상 더 명확할 수 있습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae4ed3 and 442c579.

📒 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

Comment on lines +28 to +36
@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));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

요청 바디 검증 추가 및 HTTP 상태 코드 개선 필요

  1. CommentRegisterRequest@Valid 어노테이션이 누락되어 있어 요청 데이터 검증이 수행되지 않을 수 있습니다.
  2. 리소스 생성 시 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.

Suggested change
@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.

Comment on lines +53 to +58
@PageableDefault(
page = 0,
size = 10,
sort = "id",
direction = Sort.Direction.DESC
) final Pageable pageable
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

정렬 기준 필드 검증 필요

기본 정렬 기준이 "id"로 설정되어 있습니다. PR 요구사항에 따르면 최신순과 인기순 조회가 필요한데:

  1. 최신순의 경우 "createdAt" 필드로 정렬하는 것이 더 명확합니다.
  2. 인기순의 경우 좋아요 수 필드(예: "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 -20

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

@seongmin0229 seongmin0229 merged commit a9a524d into develop Nov 24, 2025
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 자격증 댓글 API

2 participants