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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -32,9 +33,15 @@ public ApiResponse<JobBookmarkListResponse> getBookmarks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "newest") String sort) {
Sort jpaSort = "oldest".equalsIgnoreCase(sort)
? Sort.by(Sort.Direction.ASC, "createdAt")
: Sort.by(Sort.Direction.DESC, "createdAt");
return ApiResponse.ok(jobService.getBookmarkedJobs(userId, q, PageRequest.of(page, size, jpaSort)));
Pageable pageable;
if ("match".equalsIgnoreCase(sort)) {
pageable = PageRequest.of(page, size);
} else {
Sort jpaSort = "oldest".equalsIgnoreCase(sort)
? Sort.by(Sort.Direction.ASC, "createdAt")
: Sort.by(Sort.Direction.DESC, "createdAt");
pageable = PageRequest.of(page, size, jpaSort);
}
return ApiResponse.ok(jobService.getBookmarkedJobs(userId, q, pageable, sort));
}
}
24 changes: 23 additions & 1 deletion src/main/java/com/devpick/domain/job/service/JobService.java
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,10 @@ public void unbookmark(UUID userId, UUID jobId) {
}

@Transactional(readOnly = true)
public JobBookmarkListResponse getBookmarkedJobs(UUID userId, String q, Pageable pageable) {
public JobBookmarkListResponse getBookmarkedJobs(UUID userId, String q, Pageable pageable, String sortType) {
if ("match".equalsIgnoreCase(sortType)) {
return getBookmarkedJobsSortedByMatch(userId, q, pageable);
}
String normalizedQ = (q != null && !q.isBlank()) ? q.trim() : null;
Page<JobBookmark> page = normalizedQ == null
? jobBookmarkRepository.findByUserIdWithPosting(userId, pageable)
Expand All @@ -331,6 +334,25 @@ public JobBookmarkListResponse getBookmarkedJobs(UUID userId, String q, Pageable
return new JobBookmarkListResponse(items, page.getNumber(), page.getSize(), page.getTotalElements(), page.getTotalPages());
}

private JobBookmarkListResponse getBookmarkedJobsSortedByMatch(UUID userId, String q, Pageable pageable) {
String normalizedQ = (q != null && !q.isBlank()) ? q.trim() : null;
Page<JobBookmark> all = normalizedQ == null
? jobBookmarkRepository.findByUserIdWithPosting(userId, Pageable.unpaged())
: jobBookmarkRepository.findByUserIdWithPostingAndSearch(userId, normalizedQ, Pageable.unpaged());
Map<String, Integer> userSkills = loadUserSkillProfile(userId);
List<JobBookmarkItemResponse> sorted = all.getContent().stream()
.map(b -> toBookmarkItem(b, userSkills))
.sorted(Comparator.comparingInt(JobBookmarkItemResponse::matchScore).reversed())
.toList();
long total = sorted.size();
int pageNum = pageable.getPageNumber();
int pageSize = pageable.getPageSize();
int totalPages = pageSize > 0 ? (int) Math.ceil((double) total / pageSize) : 1;
int from = Math.min(pageNum * pageSize, (int) total);
int to = Math.min(from + pageSize, (int) total);
return new JobBookmarkListResponse(sorted.subList(from, to), pageNum, pageSize, total, totalPages);
}

private JobBookmarkItemResponse toBookmarkItem(JobBookmark b, Map<String, Integer> userSkills) {
JobPosting p = b.getJobPosting();
String logo = p.getCompanyLogoUrl() != null && !p.getCompanyLogoUrl().isBlank()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ void getBookmarks_success_returns200() throws Exception {
"서울", "2026-06-30", List.of("Java", "Spring"), 75, Instant.now()
);
JobBookmarkListResponse response = new JobBookmarkListResponse(List.of(item), 0, 20, 1L, 1);
given(jobService.getBookmarkedJobs(eq(userId), isNull(), any())).willReturn(response);
given(jobService.getBookmarkedJobs(eq(userId), isNull(), any(), eq("newest"))).willReturn(response);

mockMvc.perform(get("/users/me/bookmarks"))
.andExpect(status().isOk())
Expand All @@ -84,7 +84,7 @@ void getBookmarks_success_returns200() throws Exception {
@DisplayName("GET /users/me/bookmarks - 북마크 없을 시 빈 배열 반환")
void getBookmarks_empty_returns200WithEmptyList() throws Exception {
JobBookmarkListResponse response = new JobBookmarkListResponse(List.of(), 0, 20, 0L, 0);
given(jobService.getBookmarkedJobs(any(), any(), any())).willReturn(response);
given(jobService.getBookmarkedJobs(any(), any(), any(), any())).willReturn(response);

mockMvc.perform(get("/users/me/bookmarks"))
.andExpect(status().isOk())
Expand All @@ -96,7 +96,7 @@ void getBookmarks_empty_returns200WithEmptyList() throws Exception {
@DisplayName("GET /users/me/bookmarks - q 파라미터 전달 시 서비스에 전달됨")
void getBookmarks_withQuery_passesToService() throws Exception {
JobBookmarkListResponse response = new JobBookmarkListResponse(List.of(), 0, 20, 0L, 0);
given(jobService.getBookmarkedJobs(eq(userId), eq("카카오"), any())).willReturn(response);
given(jobService.getBookmarkedJobs(eq(userId), eq("카카오"), any(), eq("newest"))).willReturn(response);

mockMvc.perform(get("/users/me/bookmarks").param("q", "카카오"))
.andExpect(status().isOk());
Expand All @@ -106,9 +106,20 @@ void getBookmarks_withQuery_passesToService() throws Exception {
@DisplayName("GET /users/me/bookmarks - sort=oldest 파라미터 전달 시 200 반환")
void getBookmarks_withOldestSort_returns200() throws Exception {
JobBookmarkListResponse response = new JobBookmarkListResponse(List.of(), 0, 20, 0L, 0);
given(jobService.getBookmarkedJobs(eq(userId), isNull(), any())).willReturn(response);
given(jobService.getBookmarkedJobs(eq(userId), isNull(), any(), eq("oldest"))).willReturn(response);

mockMvc.perform(get("/users/me/bookmarks").param("sort", "oldest"))
.andExpect(status().isOk());
}

@Test
@DisplayName("GET /users/me/bookmarks - sort=match 파라미터 전달 시 matchScore 내림차순 정렬 결과 반환")
void getBookmarks_withMatchSort_returns200() throws Exception {
JobBookmarkListResponse response = new JobBookmarkListResponse(List.of(), 0, 20, 0L, 0);
given(jobService.getBookmarkedJobs(eq(userId), isNull(), any(), eq("match"))).willReturn(response);

mockMvc.perform(get("/users/me/bookmarks").param("sort", "match"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void getBookmarkedJobs_withBookmarks_returnsItems() throws Exception {
given(jobBookmarkRepository.findByUserIdWithPosting(eq(userId), any()))
.willReturn(new PageImpl<>(List.of(bookmark)));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20));
JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20), "newest");

assertThat(result.bookmarks()).hasSize(1);
assertThat(result.bookmarks().get(0).companyName()).isEqualTo("카카오");
Expand All @@ -80,7 +80,7 @@ void getBookmarkedJobs_noBookmarks_returnsEmpty() {
given(jobBookmarkRepository.findByUserIdWithPosting(eq(userId), any()))
.willReturn(new PageImpl<>(List.of()));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20));
JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20), "newest");

assertThat(result.bookmarks()).isEmpty();
assertThat(result.totalElements()).isZero();
Expand All @@ -93,7 +93,7 @@ void getBookmarkedJobs_withQuery_callsSearchRepository() {
given(jobBookmarkRepository.findByUserIdWithPostingAndSearch(eq(userId), eq("카카오"), any()))
.willReturn(new PageImpl<>(List.of()));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, "카카오", PageRequest.of(0, 20));
JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, "카카오", PageRequest.of(0, 20), "newest");

assertThat(result.bookmarks()).isEmpty();
}
Expand All @@ -119,7 +119,7 @@ void getBookmarkedJobs_withLogoAndLocation_returnsValues() throws Exception {
given(jobBookmarkRepository.findByUserIdWithPosting(eq(userId), any()))
.willReturn(new PageImpl<>(List.of(bookmark)));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20));
JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20), "newest");

assertThat(result.bookmarks().get(0).companyLogo()).isEqualTo("https://logo.naver.com/logo.png");
assertThat(result.bookmarks().get(0).location()).isEqualTo("경기 성남시");
Expand All @@ -144,7 +144,7 @@ void getBookmarkedJobs_returnsMatchScore() throws Exception {
given(jobBookmarkRepository.findByUserIdWithPosting(eq(userId), any()))
.willReturn(new PageImpl<>(List.of(bookmark)));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20));
JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20), "newest");

assertThat(result.bookmarks().get(0).matchScore()).isGreaterThanOrEqualTo(0);
}
Expand All @@ -169,8 +169,98 @@ void getBookmarkedJobs_rollingDeadline_returnsLabel() throws Exception {
given(jobBookmarkRepository.findByUserIdWithPosting(eq(userId), any()))
.willReturn(new PageImpl<>(List.of(bookmark)));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20));
JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20), "newest");

assertThat(result.bookmarks().get(0).deadline()).isEqualTo("채용 시 마감");
}

@Test
@DisplayName("북마크 목록 조회 - sort=match 전달 시 matchScore 내림차순 정렬 후 반환")
void getBookmarkedJobs_matchSort_returnsSortedByMatchScoreDesc() throws Exception {
UUID userId = UUID.randomUUID();
JobPosting posting1 = JobPosting.builder()
.companyName("카카오")
.title("백엔드 개발자")
.employmentType(EmploymentType.FULL_TIME)
.experienceLevel(PostingExperienceLevel.JUNIOR)
.build();
JobPosting posting2 = JobPosting.builder()
.companyName("네이버")
.title("프론트엔드 개발자")
.employmentType(EmploymentType.FULL_TIME)
.experienceLevel(PostingExperienceLevel.MIDDLE)
.build();
JobBookmark bookmark1 = JobBookmark.builder().userId(userId).jobPosting(posting1).build();
JobBookmark bookmark2 = JobBookmark.builder().userId(userId).jobPosting(posting2).build();
setCreatedAt(bookmark1, LocalDateTime.now());
setCreatedAt(bookmark2, LocalDateTime.now());

given(jobBookmarkRepository.findByUserIdWithPosting(eq(userId), any()))
.willReturn(new PageImpl<>(List.of(bookmark1, bookmark2)));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20), "match");

assertThat(result.bookmarks()).hasSize(2);
assertThat(result.totalElements()).isEqualTo(2);
// matchScore 내림차순 정렬 확인
assertThat(result.bookmarks().get(0).matchScore())
.isGreaterThanOrEqualTo(result.bookmarks().get(1).matchScore());
}

@Test
@DisplayName("북마크 목록 조회 - sort=match이고 북마크 없을 때 빈 배열 반환")
void getBookmarkedJobs_matchSort_emptyBookmarks_returnsEmpty() {
UUID userId = UUID.randomUUID();
given(jobBookmarkRepository.findByUserIdWithPosting(eq(userId), any()))
.willReturn(new PageImpl<>(List.of()));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 20), "match");

assertThat(result.bookmarks()).isEmpty();
assertThat(result.totalElements()).isZero();
}

@Test
@DisplayName("북마크 목록 조회 - sort=match + 검색어 전달 시 검색 쿼리 호출됨")
void getBookmarkedJobs_matchSort_withQuery_callsSearchRepository() {
UUID userId = UUID.randomUUID();
given(jobBookmarkRepository.findByUserIdWithPostingAndSearch(eq(userId), eq("카카오"), any()))
.willReturn(new PageImpl<>(List.of()));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, "카카오", PageRequest.of(0, 20), "match");

assertThat(result.bookmarks()).isEmpty();
assertThat(result.totalElements()).isZero();
}

@Test
@DisplayName("북마크 목록 조회 - sort=match 페이징: 2개 아이템에서 size=1 첫 페이지 요청 시 1개 반환")
void getBookmarkedJobs_matchSort_pagination_returnsCorrectPage() throws Exception {
UUID userId = UUID.randomUUID();
JobPosting posting1 = JobPosting.builder()
.companyName("카카오")
.title("백엔드 개발자")
.employmentType(EmploymentType.FULL_TIME)
.experienceLevel(PostingExperienceLevel.JUNIOR)
.build();
JobPosting posting2 = JobPosting.builder()
.companyName("네이버")
.title("프론트엔드 개발자")
.employmentType(EmploymentType.FULL_TIME)
.experienceLevel(PostingExperienceLevel.MIDDLE)
.build();
JobBookmark bookmark1 = JobBookmark.builder().userId(userId).jobPosting(posting1).build();
JobBookmark bookmark2 = JobBookmark.builder().userId(userId).jobPosting(posting2).build();
setCreatedAt(bookmark1, LocalDateTime.now());
setCreatedAt(bookmark2, LocalDateTime.now());

given(jobBookmarkRepository.findByUserIdWithPosting(eq(userId), any()))
.willReturn(new PageImpl<>(List.of(bookmark1, bookmark2)));

JobBookmarkListResponse result = jobService.getBookmarkedJobs(userId, null, PageRequest.of(0, 1), "match");

assertThat(result.bookmarks()).hasSize(1);
assertThat(result.totalElements()).isEqualTo(2);
assertThat(result.totalPages()).isEqualTo(2);
}
}
Loading