From 99c6e64a96ab1c0b73a927a07b39e8b7e1cd4156 Mon Sep 17 00:00:00 2001 From: nYeonG4001 <2371324@hansung.ac.kr> Date: Sun, 10 May 2026 13:30:57 +0900 Subject: [PATCH] =?UTF-8?q?DP-473:=20=ED=99=88=20=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EB=9E=AD=ED=82=B9=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=E2=80=94=20=EA=B4=80=EC=8B=AC=20=ED=83=9C=EA=B7=B8=20=EC=9A=B0?= =?UTF-8?q?=EC=84=A0=20=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit content_tags 매핑이 없는 글이 피드에서 탈락하던 문제를 해결. 태그 필터링 대신 CASE WHEN EXISTS 기반 랭킹으로 관심 태그 일치 글을 상단에, 나머지 글을 publishedAt DESC 순으로 하단에 노출. --- .../content/repository/ContentRepository.java | 11 ++++++++++ .../content/service/ContentService.java | 5 +---- .../content/service/ContentServiceTest.java | 20 +++++++++---------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/devpick/domain/content/repository/ContentRepository.java b/src/main/java/com/devpick/domain/content/repository/ContentRepository.java index 77375f3..96b4e50 100644 --- a/src/main/java/com/devpick/domain/content/repository/ContentRepository.java +++ b/src/main/java/com/devpick/domain/content/repository/ContentRepository.java @@ -21,6 +21,17 @@ public interface ContentRepository extends JpaRepository { @Query("SELECT DISTINCT c FROM Content c JOIN c.contentTags ct WHERE ct.tag.id IN :tagIds AND c.isAvailable = true AND c.source.name <> 'YouTube' ORDER BY c.publishedAt DESC") Page findByTagIdsAndIsAvailableTrue(@Param("tagIds") List tagIds, Pageable pageable); + @Query(""" + SELECT c FROM Content c + WHERE c.isAvailable = true AND c.source.name <> 'YouTube' + ORDER BY + CASE WHEN EXISTS ( + SELECT ct FROM ContentTag ct WHERE ct.content = c AND ct.tag.id IN :tagIds + ) THEN 0 ELSE 1 END, + c.publishedAt DESC + """) + Page findAllRankedByTagIds(@Param("tagIds") List tagIds, Pageable pageable); + @Query("SELECT DISTINCT c FROM Content c LEFT JOIN c.contentTags ct LEFT JOIN ct.tag t " + "WHERE c.isAvailable = true " + "AND c.source.name <> 'YouTube' " + diff --git a/src/main/java/com/devpick/domain/content/service/ContentService.java b/src/main/java/com/devpick/domain/content/service/ContentService.java index c0793f7..9c927bc 100644 --- a/src/main/java/com/devpick/domain/content/service/ContentService.java +++ b/src/main/java/com/devpick/domain/content/service/ContentService.java @@ -72,10 +72,7 @@ public ContentListResponse getFeed(UUID userId, Pageable pageable) { if (tagIds.isEmpty()) { page = contentRepository.findByIsAvailableTrueOrderByPublishedAtDesc(pageable); } else { - page = contentRepository.findByTagIdsAndIsAvailableTrue(tagIds, pageable); - if (page.getTotalElements() == 0) { - page = contentRepository.findByIsAvailableTrueOrderByPublishedAtDesc(pageable); - } + page = contentRepository.findAllRankedByTagIds(tagIds, pageable); } List contents = page.getContent().stream() diff --git a/src/test/java/com/devpick/domain/content/service/ContentServiceTest.java b/src/test/java/com/devpick/domain/content/service/ContentServiceTest.java index 10c4e9f..441be2c 100644 --- a/src/test/java/com/devpick/domain/content/service/ContentServiceTest.java +++ b/src/test/java/com/devpick/domain/content/service/ContentServiceTest.java @@ -135,14 +135,14 @@ void getFeed_anonymous_skipsUserScopedQueries() { } @Test - @DisplayName("getFeed — 태그 있으면 태그 필터링된 콘텐츠 반환") - void getFeed_withUserTags_returnsFilteredFeed() { + @DisplayName("getFeed — 태그 있으면 랭킹 방식으로 전체 콘텐츠 반환 (관심 태그 우선)") + void getFeed_withUserTags_returnsRankedFeed() { UserTag userTag = UserTag.builder() .user(user) .tag(Tag.builder().name("Spring").build()) .build(); given(userTagRepository.findByUser_Id(userId)).willReturn(List.of(userTag)); - given(contentRepository.findByTagIdsAndIsAvailableTrue(any(), any())) + given(contentRepository.findAllRankedByTagIds(any(), any())) .willReturn(new PageImpl<>(List.of(content))); given(scrapRepository.existsByUser_IdAndContent_Id(any(), any())).willReturn(false); given(likeRepository.existsByUser_IdAndContent_Id(any(), any())).willReturn(false); @@ -151,21 +151,19 @@ void getFeed_withUserTags_returnsFilteredFeed() { ContentListResponse response = contentService.getFeed(userId, PageRequest.of(0, 20)); assertThat(response.contents()).hasSize(1); - verify(contentRepository).findByTagIdsAndIsAvailableTrue(any(), any()); + verify(contentRepository).findAllRankedByTagIds(any(), any()); verify(contentRepository, never()).findByIsAvailableTrueOrderByPublishedAtDesc(any()); } @Test - @DisplayName("getFeed — 태그 필터 결과가 비면 전체 공개 글로 폴백") - void getFeed_tagFilterEmpty_fallsBackToAllAvailable() { + @DisplayName("getFeed — 관심 태그와 일치하는 글 없어도 전체 글 반환 (랭킹 방식)") + void getFeed_withUserTags_noTagMatch_stillReturnsAllContents() { UserTag userTag = UserTag.builder() .user(user) .tag(Tag.builder().name("Rust").build()) .build(); given(userTagRepository.findByUser_Id(userId)).willReturn(List.of(userTag)); - given(contentRepository.findByTagIdsAndIsAvailableTrue(any(), any())) - .willReturn(new PageImpl<>(List.of())); - given(contentRepository.findByIsAvailableTrueOrderByPublishedAtDesc(any())) + given(contentRepository.findAllRankedByTagIds(any(), any())) .willReturn(new PageImpl<>(List.of(content))); given(scrapRepository.existsByUser_IdAndContent_Id(any(), any())).willReturn(false); given(likeRepository.existsByUser_IdAndContent_Id(any(), any())).willReturn(false); @@ -174,8 +172,8 @@ void getFeed_tagFilterEmpty_fallsBackToAllAvailable() { ContentListResponse response = contentService.getFeed(userId, PageRequest.of(0, 20)); assertThat(response.contents()).hasSize(1); - verify(contentRepository).findByTagIdsAndIsAvailableTrue(any(), any()); - verify(contentRepository).findByIsAvailableTrueOrderByPublishedAtDesc(any()); + verify(contentRepository).findAllRankedByTagIds(any(), any()); + verify(contentRepository, never()).findByIsAvailableTrueOrderByPublishedAtDesc(any()); } @Test