From 68c7c1b55fda31dbe06626f7345ed1e817bed2b9 Mon Sep 17 00:00:00 2001 From: nYeonG4001 <2371324@hansung.ac.kr> Date: Sun, 10 May 2026 12:06:44 +0900 Subject: [PATCH] =?UTF-8?q?DP-470:=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EA=B3=B5=EA=B3=A0=20=EB=AA=A9=EB=A1=9D=20=EB=A7=A4=EC=B9=AD?= =?UTF-8?q?=EC=88=9C=20=EC=A0=95=EB=A0=AC=20=EC=B6=94=EA=B0=80=20(sort=3Dm?= =?UTF-8?q?atch)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../job/controller/JobBookmarkController.java | 15 ++- .../domain/job/service/JobService.java | 24 ++++- .../controller/JobBookmarkControllerTest.java | 19 +++- .../job/service/JobBookmarkServiceTest.java | 102 ++++++++++++++++-- 4 files changed, 145 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/devpick/domain/job/controller/JobBookmarkController.java b/src/main/java/com/devpick/domain/job/controller/JobBookmarkController.java index eccde1d..bd829ee 100644 --- a/src/main/java/com/devpick/domain/job/controller/JobBookmarkController.java +++ b/src/main/java/com/devpick/domain/job/controller/JobBookmarkController.java @@ -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; @@ -32,9 +33,15 @@ public ApiResponse 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)); } } diff --git a/src/main/java/com/devpick/domain/job/service/JobService.java b/src/main/java/com/devpick/domain/job/service/JobService.java index 538ffe9..45cd796 100644 --- a/src/main/java/com/devpick/domain/job/service/JobService.java +++ b/src/main/java/com/devpick/domain/job/service/JobService.java @@ -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 page = normalizedQ == null ? jobBookmarkRepository.findByUserIdWithPosting(userId, pageable) @@ -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 all = normalizedQ == null + ? jobBookmarkRepository.findByUserIdWithPosting(userId, Pageable.unpaged()) + : jobBookmarkRepository.findByUserIdWithPostingAndSearch(userId, normalizedQ, Pageable.unpaged()); + Map userSkills = loadUserSkillProfile(userId); + List 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 userSkills) { JobPosting p = b.getJobPosting(); String logo = p.getCompanyLogoUrl() != null && !p.getCompanyLogoUrl().isBlank() diff --git a/src/test/java/com/devpick/domain/job/controller/JobBookmarkControllerTest.java b/src/test/java/com/devpick/domain/job/controller/JobBookmarkControllerTest.java index d2ebe85..adf3894 100644 --- a/src/test/java/com/devpick/domain/job/controller/JobBookmarkControllerTest.java +++ b/src/test/java/com/devpick/domain/job/controller/JobBookmarkControllerTest.java @@ -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()) @@ -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()) @@ -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()); @@ -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)); + } } diff --git a/src/test/java/com/devpick/domain/job/service/JobBookmarkServiceTest.java b/src/test/java/com/devpick/domain/job/service/JobBookmarkServiceTest.java index 0afec69..9ca24bb 100644 --- a/src/test/java/com/devpick/domain/job/service/JobBookmarkServiceTest.java +++ b/src/test/java/com/devpick/domain/job/service/JobBookmarkServiceTest.java @@ -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("카카오"); @@ -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(); @@ -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(); } @@ -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("경기 성남시"); @@ -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); } @@ -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); + } }