Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
context: .
file: ./Dockerfile
push: true
tags: woals2840/certi-server
tags: seongmin0229/certi-server

deploy-cd:
needs: build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ jobs:
context: .
file: ./Dockerfile
push: true
tags: woals2840/certi-server
tags: seongmin0229/certi-server

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.RequiredArgsConstructor;
import org.sopt.certi_server.domain.certification.dto.response.CertificationDetailResponse;
import org.sopt.certi_server.domain.certification.dto.response.CertificationListResponse;
import org.sopt.certi_server.domain.certification.dto.response.CertificationRankResponse;
import org.sopt.certi_server.domain.certification.dto.response.CertificationRecommendationListResponse;
import org.sopt.certi_server.domain.certification.dto.response.CertificationSimple;
import org.sopt.certi_server.domain.certification.service.CertificationService;
Expand Down Expand Up @@ -81,4 +82,24 @@ public ResponseEntity<SuccessResponse<CertificationRecommendationListResponse>>
return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certiRecommendListRes));
}

@GetMapping("/job")
@Operation(summary = "직무별 자격증 조회 API", description = "3순위 직무별 자격증을 조회합니다")
public ResponseEntity<SuccessResponse<?>> 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(
@AuthenticationPrincipal Long userId
) {
List<CertificationRankResponse> certificationRankResponseList = certificationService.getCertificationTrack(userId);
return ResponseEntity.ok(SuccessResponse.of(SuccessCode.SUCCESS_FETCH, certificationRankResponseList));

}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.sopt.certi_server.domain.certification.dto.response;

import org.sopt.certi_server.domain.certification.entity.Certification;

public record CertificationRankResponse(
int rank,
String certificationName,
String certificationType
) {
public CertificationRankResponse(int rank, Certification certification) {
this(
rank,
certification.getName(),
certification.getCertificationType() != null
? certification.getCertificationType().getKoreanName()
: null
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public record CertificationScoreDto(
String testType,
List<String> tags,
int recommendationScore,
boolean isFavorite) {
boolean isFavorite,
String description) {
public static CertificationScoreDto from(
Certification certification,
int recommendationScore,
Expand All @@ -24,7 +25,8 @@ public static CertificationScoreDto from(
certification.getTestType().getType(),
certification.getTags().stream().toList(),
recommendationScore,
isFavorite
isFavorite,
certification.getDescription()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class CertificationSimple {
private List<String> tags;
@JsonProperty(value = "isFavorite")
private boolean favorite;
private String description;

public CertificationSimple(
Certification certification,
Expand All @@ -29,5 +30,6 @@ public CertificationSimple(
certification.getTestType().getType() : null;
this.tags = certification.getTags();
this.favorite = favorite;
this.description = certification.getDescription();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@
import org.sopt.certi_server.domain.major.entity.Major;
import org.sopt.certi_server.domain.major.repository.MajorRepository;
import org.sopt.certi_server.domain.user.entity.User;
import org.sopt.certi_server.domain.user.entity.enums.TrackType;
import org.sopt.certi_server.domain.user.service.UserService;
import org.sopt.certi_server.global.error.code.ErrorCode;
import org.sopt.certi_server.global.error.exception.NotFoundException;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -45,6 +49,7 @@ public class CertificationService {
private final CertificationJobRepository certificationJobRepository;
private final CertificationRepositoryCustomImpl certificationRepositoryCustomImpl;
private final UserService userService;
private final FavoriteRepository favoriteRepository;


@Cacheable(
Expand Down Expand Up @@ -197,4 +202,36 @@ public CertificationListResponse getCertificationList(final Long userId, final b

}

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);

return certList.stream()
.map(cert -> new CertificationRankResponse(rank.getAndIncrement(), cert))
.toList();
}

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.


Pageable top3 = PageRequest.of(0, 3);

List<Certification> certificationList = favoriteRepository.findTopCertificationsByTrack(trackType, top3);

AtomicInteger rank = new AtomicInteger(1);

return certificationList.stream()
.map(c -> new CertificationRankResponse(rank.getAndIncrement(), c))
.toList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
import org.sopt.certi_server.domain.certification.entity.Certification;
import org.sopt.certi_server.domain.favorite.entity.Favorite;
import org.sopt.certi_server.domain.user.entity.User;
import org.sopt.certi_server.domain.user.entity.enums.TrackType;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;
Expand All @@ -29,4 +32,32 @@ public interface FavoriteRepository extends JpaRepository<Favorite, Long> {
boolean existsByUserAndCertification(User user, Certification certification);

void deleteAllByUser(User user);

@Query("""
select c
from Favorite f
join f.certification c
join CertificationJob cj on cj.certification = c
where cj.job.id = :jobId
group by c, cj.weight
order by count(f) desc, cj.weight desc
""")
List<Certification> findTopByJobOrderByFavoriteCount(
@Param("jobId") Long jobId,
Pageable pageable
);

@Query("""
select c
from Favorite f
join f.certification c
join f.user u
where u.track = :track
group by c
order by count(f) desc
""")
List<Certification> findTopCertificationsByTrack(
@Param("track") TrackType track,
Pageable pageable
);
}