Skip to content

Conversation

@woals2840
Copy link
Contributor

@woals2840 woals2840 commented Dec 30, 2025

📣 Related Issue

📝 Summary

  • 추천 자격증 조회 시 변경 사항 반영
  • 직무별/계열별 Top3자격증 조회 API 구현

🙏 Question & PR point

  • 현재 직무의 순위가 반영되지 않은 상태로 구현을 하였어서 추후에 성민님의 PR 반영 및 충돌 해결 후 직무의 순위를 반영해서 약간의 수정이 필요할 것 같습니다
  • 3순위 조회 시 JPQL을 사용하여서 limit 3이 적용이 안 되어 Pageable을 사용하였는데 JPQL을 그대로 가져갈지 QueryDSL을 쓰는 게 나을지 성민님의 의견이 궁금합니다!

Summary by CodeRabbit

  • 새로운 기능

    • 직무별 상위 자격증(Top3) 조회 기능이 추가되었습니다.
    • 트랙별 상위 자격증(Top3) 조회 기능이 추가되었습니다.
    • 자격증 응답에 순위 정보가 포함됩니다.
    • 자격증 정보에 설명(description) 필드가 추가되어 상세 내용을 제공합니다.
  • 잡무/인프라

    • CI/CD 워크플로우의 도커 이미지 태그가 업데이트되었습니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@woals2840 woals2840 linked an issue Dec 30, 2025 that may be closed by this pull request
1 task
@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

두 개의 엔드포인트(GET /api/v1/certification/job, GET /api/v1/certification/track)와 이를 지원하는 DTO·서비스 메서드·FavoriteRepository 쿼리가 추가되어 사용자 직무·트랙 기준 즐겨찾기 집계로 상위 3개 자격증을 순위화해 반환합니다. (50단어 이내)

Changes

Cohort / File(s) 요약
컨트롤러 업데이트
src/main/java/org/sopt/certi_server/domain/certification/controller/CertificationController.java
getTop3ByJob(@AuthenticationPrincipal Long userId)getTop3ByTrack(@AuthenticationPrincipal Long userId) 엔드포인트 추가. CertificationRankResponse 사용, SuccessResponse(SUCCESS_FETCH) 반환
신규 응답 DTO
src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationRankResponse.java
record CertificationRankResponse(int rank, String certificationName, String certificationType) 추가 및 Certification 기반 보조 생성자 구현
기존 DTO 확장
src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java, src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationSimple.java
두 DTO에 description 필드 추가 및 팩토리/생성자에서 certification.getDescription()으로 초기화
비즈니스 로직 추가
src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java
getCertificationJob(Long userId)getCertificationTrack(Long userId) 추가. FavoriteRepository 주입, PageRequest로 상위 3개 조회 후 AtomicInteger로 순위 부여, JOB_NOT_FOUND 예외 처리
레포지토리 쿼리 추가
src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java
findTopByJobOrderByFavoriteCount(Long jobId, Pageable pageable)findTopCertificationsByTrack(TrackType track, Pageable pageable) JPQL 쿼리 추가 (즐겨찾기 집계 기반 정렬)
CI/CD 워크플로우 변경
.github/workflows/ci.yml, .github/workflows/cd.yml
Docker 이미지 태그를 woals2840/certi-serverseongmin0229/certi-server로 변경 (이미지 명/태그만 변경)

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client
    participant Controller as CertificationController
    participant Service as CertificationService
    participant Repo as FavoriteRepository
    participant DB as Database

    Client->>Controller: GET /api/v1/certification/job (userId)
    Controller->>Service: getCertificationJob(userId)
    Service->>Repo: findTopByJobOrderByFavoriteCount(jobId, Pageable(0,3))
    Repo->>DB: JPQL 쿼리 실행 (join Favorite/CertificationJob, group by, order by favoriteCount)
    DB-->>Repo: List<Certification> (top 3)
    Repo-->>Service: certifications
    Service->>Service: AtomicInteger로 rank 부여 및 CertificationRankResponse 생성
    Service-->>Controller: List<CertificationRankResponse>
    Controller-->>Client: 200 SuccessResponse(SUCCESS_FETCH, data)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • cerdeuk/CERTI-BE#172: 동일 기능(직무·트랙 기준 Top-3) 관련 컨트롤러·서비스·레포지토리·DTO 변경과 직접 중첩 가능성.
  • cerdeuk/CERTI-BE#86: CertificationSimple DTO 변경(필드/직렬화 관련)과 중복 또는 충돌 가능.
  • cerdeuk/CERTI-BE#157: FavoriteRepository 사용/구조 변경 관련 — CertificationService와 레포지토리 변경 지점에서 충돌 가능.

Suggested labels

🧩Feat

Suggested reviewers

  • woals2840

Poem

🐰 깡충깡충 톱3 모아왔어요,
직무·트랙 따라 별빛 반짝,
즐겨찾기 수로 순위 매기고,
설명도 품어 더 풍성히 전해요. ✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Docker 이미지 태그 변경은 범위 밖의 행정적 변경으로, 카테고리 API 수정 목표와 직접적인 관련성이 없습니다. Docker 이미지 태그 변경(woals2840 → seongmin0229)은 별도의 PR로 분리하거나 이 PR의 범위 내에서 정당성을 명시하는 것을 권장합니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '#171 카테고리 API 수정'으로 전체 변경사항을 포괄하는 주요 변경사항을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed 링크된 이슈 #171은 구체적인 구현 요구사항 없이 템플릿 형식의 미완성 이슈이나, PR에서 직무별/계열별 Top3 자격증 조회 기능을 구현하여 요구사항을 충족합니다.

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between becfb70 and c08a34a.

📒 Files selected for processing (2)
  • src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java
  • src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java

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: 2

🧹 Nitpick comments (2)
src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java (1)

33-44: toString() 메서드에 새로운 필드 포함 권장

description 필드가 record에 추가되었지만 toString() 메서드에는 포함되지 않았습니다. 디버깅이나 로깅 시 일관성을 위해 추가하는 것을 권장합니다.

🔎 제안하는 수정
 @Override
 public String toString() {
     return "CertificationScoreDto{" +
             "certificationId=" + certificationId +
             ", certificationName='" + certificationName + '\'' +
             ", certificationType='" + certificationType + '\'' +
             ", testType='" + testType + '\'' +
             ", tags=" + tags +
             ", recommendationScore=" + recommendationScore +
             ", isFavorite=" + isFavorite +
+            ", description='" + description + '\'' +
             '}';
 }
src/main/java/org/sopt/certi_server/domain/certification/controller/CertificationController.java (1)

85-92: 제네릭 타입을 구체적으로 명시하는 것을 권장합니다

반환 타입에 와일드카드 <?>를 사용하는 대신 <List<CertificationRankResponse>>로 명시하면 타입 안정성과 API 문서 가독성이 향상됩니다.

🔎 제안하는 수정
 @GetMapping("/job")
 @Operation(summary = "직무별 자격증 조회 API", description = "3순위 직무별 자격증을 조회합니다")
-public ResponseEntity<SuccessResponse<?>> getTop3ByJob(
+public ResponseEntity<SuccessResponse<List<CertificationRankResponse>>> getTop3ByJob(
     @AuthenticationPrincipal Long userId
 ){
     List<CertificationRankResponse> certificationRankResponseList = certificationService.getCertificationJob(userId);
     return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certificationRankResponseList));
 }

 @GetMapping("/track")
 @Operation(summary = "계열별 자격증 조회 API", description = "3순위 계열별 자격증을 조회합니다")
-public ResponseEntity<SuccessResponse<?>> getTop3ByTrack(
+public ResponseEntity<SuccessResponse<List<CertificationRankResponse>>> getTop3ByTrack(
     @AuthenticationPrincipal Long userId
 ) {
     List<CertificationRankResponse> certificationRankResponseList = certificationService.getCertificationTrack(userId);
     return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certificationRankResponseList));
 }

Also applies to: 94-102

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9a524d and 85f0d76.

📒 Files selected for processing (6)
  • src/main/java/org/sopt/certi_server/domain/certification/controller/CertificationController.java
  • src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationRankResponse.java
  • src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java
  • src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationSimple.java
  • src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java
  • src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java (2)
src/main/java/org/sopt/certi_server/domain/certification/repository/CertificationRepositoryCustomImpl.java (2)
  • CertificationRepositoryCustomImpl (19-56)
  • findByJobAndFavorite (24-55)
src/main/java/org/sopt/certi_server/domain/certification/repository/CertificationRepositoryCustom.java (2)
  • CertificationRepositoryCustom (8-10)
  • findByJobAndFavorite (9-9)
⏰ 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/domain/certification/dto/response/CertificationSimple.java (1)

19-19: LGTM!

description 필드 추가가 적절하게 구현되었습니다. 기존 패턴을 잘 따르고 있습니다.

Also applies to: 33-33

src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationRankResponse.java (1)

1-19: LGTM!

새로운 응답 DTO가 잘 설계되었습니다. certificationType에 대한 null-safe 처리도 적절합니다.

src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java (1)

218-231: LGTM!

getCertificationTrack 메서드는 사용자로부터 직접 track 정보를 가져오므로 안전합니다. AtomicInteger를 사용한 순위 할당 로직도 적절합니다.

src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java (1)

50-62: LGTM!

Track별 조회 쿼리는 c로만 그룹화하여 중복 문제가 없습니다. 올바르게 구현되었습니다.

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

♻️ Duplicate comments (1)
src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java (1)

207-207: 빈 리스트 처리 누락으로 인한 IndexOutOfBoundsException 위험 (중복)

userService.getUserJob(userId).jobList().get(0) 호출 시 jobList가 비어있을 경우 IndexOutOfBoundsException이 발생할 수 있습니다. 이 문제는 이전 리뷰에서 이미 지적되었으나 아직 수정되지 않았습니다.

이전 리뷰 코멘트를 참고하여 jobList를 먼저 변수에 할당하고 isEmpty() 체크를 추가해주세요.

🧹 Nitpick comments (4)
.github/workflows/cd.yml (1)

45-45: Docker 이미지 태그에 버전 정보 추가를 권장합니다.

현재 Docker 이미지 태그에 버전 정보(예: :latest, :v1.0.0, 또는 커밋 SHA)가 없어 배포된 이미지의 버전 추적이 어려울 수 있습니다. 명시적인 태그를 추가하면 롤백 및 버전 관리가 용이합니다.

🔎 버전 태그 추가 예시
-          tags: seongmin0229/certi-server
+          tags: |
+            seongmin0229/certi-server:latest
+            seongmin0229/certi-server:${{ github.sha }}

또한, Docker Hub 계정 변경이 의도된 것인지 확인해 주세요.

.github/workflows/ci.yml (1)

45-45: CI와 CD 워크플로우 간 일관성이 유지되었습니다.

Docker 이미지 태그가 cd.yml과 동일하게 업데이트되어 일관성이 확보되었습니다. 다만, cd.yml의 리뷰 코멘트에서 언급한 것처럼 버전 태그 추가를 고려해 주세요.

src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java (2)

206-206: 불필요한 코드 및 단순화 가능한 부분

두 가지 개선 사항을 제안합니다:

  1. Line 206: user 변수를 가져오지만 실제로 사용하지 않습니다. userService.getUser(userId) 호출을 제거할 수 있습니다.
  2. Line 215: 단순 순차 카운팅에 AtomicInteger는 과합니다. 동시성 제어가 필요 없으므로 일반 int 변수로 충분합니다.
🔎 제안하는 개선안
 public List<CertificationRankResponse> getCertificationJob(final Long userId){
-    User user = userService.getUser(userId);
     String jobName = userService.getUserJob(userId).jobList().get(0);
     Job job = jobRepository.findByName(jobName)
         .orElseThrow(() -> new NotFoundException(ErrorCode.JOB_NOT_FOUND));
 
     Pageable top3 = PageRequest.of(0, 3);
     List<Certification> certList =
         favoriteRepository.findTopByJobOrderByFavoriteCount(job.getId(), top3);
 
-    AtomicInteger rank = new AtomicInteger(1);
+    int rank = 1;
 
-    return certList.stream()
-        .map(cert -> new CertificationRankResponse(rank.getAndIncrement(), cert))
-        .toList();
+    List<CertificationRankResponse> result = new ArrayList<>();
+    for (Certification cert : certList) {
+        result.add(new CertificationRankResponse(rank++, cert));
+    }
+    return result;

Also applies to: 215-215


230-230: AtomicInteger 단순화 가능

getCertificationJob 메서드와 동일하게, 단순 순차 카운팅에는 AtomicInteger 대신 일반 int 변수를 사용하는 것이 더 간결합니다.

🔎 제안하는 개선안
 public List<CertificationRankResponse> getCertificationTrack(final Long userId){
     User user = userService.getUser(userId);
     TrackType trackType = user.getTrack();
 
     Pageable top3 = PageRequest.of(0, 3);
 
     List<Certification> certificationList = favoriteRepository.findTopCertificationsByTrack(trackType, top3);
 
-    AtomicInteger rank = new AtomicInteger(1);
+    int rank = 1;
 
-    return certificationList.stream()
-        .map(c -> new CertificationRankResponse(rank.getAndIncrement(), c))
-        .toList();
+    List<CertificationRankResponse> result = new ArrayList<>();
+    for (Certification c : certificationList) {
+        result.add(new CertificationRankResponse(rank++, c));
+    }
+    return result;
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 85f0d76 and becfb70.

📒 Files selected for processing (4)
  • .github/workflows/cd.yml
  • .github/workflows/ci.yml
  • 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
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/sopt/certi_server/domain/certification/dto/response/CertificationScoreDto.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java (3)
src/main/java/org/sopt/certi_server/global/error/exception/NotFoundException.java (1)
  • NotFoundException (5-9)
src/main/java/org/sopt/certi_server/domain/user/service/UserService.java (1)
  • Service (26-108)
src/main/java/org/sopt/certi_server/domain/favorite/service/FavoriteService.java (1)
  • Service (19-51)
⏰ 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 (2)
src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java (2)

20-20: 새로운 의존성 및 임포트 추가 확인

새로운 기능을 지원하기 위한 임포트와 FavoriteRepository 의존성이 적절히 추가되었습니다.

Also applies to: 26-27, 32-32, 52-52


213-213: FavoriteRepository 메서드 확인 완료

findTopByJobOrderByFavoriteCountfindTopCertificationsByTrack 메서드가 FavoriteRepository에 올바르게 구현되어 있습니다. 두 메서드 모두 @query 애노테이션으로 작성된 쿼리를 사용하며, Pageable 파라미터를 통해 결과를 제한하여 Top 3 자격증을 반환합니다.


public List<CertificationRankResponse> getCertificationTrack(final Long userId){
User user = userService.getUser(userId);
TrackType trackType = user.getTrack();
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

🧩 Analysis chain

🏁 Script executed:

find src -name "User.java" -type f | head -20

Repository: cerdeuk/CERTI-BE

Length of output: 124


🏁 Script executed:

cat -n src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java | sed -n '220,235p'

Repository: cerdeuk/CERTI-BE

Length of output: 725


🏁 Script executed:

rg -A 10 "TrackType.*track" src/main/java/org/sopt/certi_server/domain/user/entity/User.java

Repository: cerdeuk/CERTI-BE

Length of output: 737


🏁 Script executed:

rg "findTopCertificationsByTrack" src/main/java/org/sopt/certi_server/domain/certification/repository/

Repository: cerdeuk/CERTI-BE

Length of output: 42


🏁 Script executed:

cat -n src/main/java/org/sopt/certi_server/domain/user/entity/User.java | sed -n '1,100p'

Repository: cerdeuk/CERTI-BE

Length of output: 3137


🏁 Script executed:

rg "private TrackType track" -A 5 -B 2 src/main/java/org/sopt/certi_server/domain/user/entity/User.java

Repository: cerdeuk/CERTI-BE

Length of output: 253


🏁 Script executed:

rg "findTopCertifications" src/main/java/ --type java

Repository: cerdeuk/CERTI-BE

Length of output: 405


🏁 Script executed:

rg "getUser" src/main/java/org/sopt/certi_server/domain/user/service/UserService.java -A 10

Repository: cerdeuk/CERTI-BE

Length of output: 1473


🏁 Script executed:

cat -n src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java | grep -A 15 "findTopCertificationsByTrack"

Repository: cerdeuk/CERTI-BE

Length of output: 224


🏁 Script executed:

cat src/main/java/org/sopt/certi_server/domain/favorite/repository/FavoriteRepository.java

Repository: cerdeuk/CERTI-BE

Length of output: 2065


트랙 타입이 null인 경우 쿼리가 올바르게 작동하지 않습니다

User 엔티티의 track 필드는 nullable=false 제약이 없어 null이 될 수 있습니다. user.getTrack()이 null을 반환하면 이를 그대로 저장소 메서드에 전달하는데, 쿼리의 where u.track = :track 조건에서 null 값과의 비교로 인해 예상치 못한 결과가 반환됩니다. SQL에서 NULL = NULL은 false이므로 해당 사용자의 인증들이 조회되지 않습니다.

trackType이 null인 경우를 처리하거나, 사용자의 track이 반드시 존재해야 한다면 명시적으로 검증하세요.

🤖 Prompt for AI Agents
In
src/main/java/org/sopt/certi_server/domain/certification/service/CertificationService.java
around line 224, user.getTrack() can be null which breaks the repository query
using "where u.track = :track"; update the service to handle nulls explicitly:
check if trackType == null and either (a) call a repository method that queries
for u.track IS NULL (or add a new repository method for that case) or (b)
validate and throw a clear exception (e.g., IllegalStateException or a custom
validation exception) if a track is required; ensure the chosen branch uses a
repository query that matches null correctly or fails fast with a descriptive
error.

Copy link
Contributor

@seongmin0229 seongmin0229 left a comment

Choose a reason for hiding this comment

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

수정사항 반영 후에 이 pr 먼저 머지하고 제 pr이랑 합쳐서 다시 머지하면 될 것 같습니다.

@woals2840 woals2840 merged commit 5499a47 into develop Jan 6, 2026
0 of 3 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 수정 [Fix] yml수정

3 participants