Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,40 @@ public CertificationListResponse getCertificationList(final Long userId, final b

}

public List<CertificationRankResponse> getCertificationJob(final Long userId){
User user = userService.getUser(userId);
List<String> jobList = userService.getUserJob(userId).jobList();
if (jobList.isEmpty()) {
throw new NotFoundException(ErrorCode.JOB_NOT_FOUND);
}
String jobName = 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 distinct c
from Favorite f
join f.certification c
join CertificationJob cj on cj.certification = c
where cj.job.id = :jobId
group by c
order by count(f) desc, max(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
);
}
Loading