From a44d3164275fb22bb57ed2b3eef21c80e4555c68 Mon Sep 17 00:00:00 2001 From: nYeonG4001 <2371324@hansung.ac.kr> Date: Tue, 26 May 2026 13:58:54 +0900 Subject: [PATCH] =?UTF-8?q?DP-513:=20=ED=99=88=ED=94=BC=EB=93=9C=20?= =?UTF-8?q?=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=20=ED=95=99=EC=8A=B5=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/service/ContentService.java | 11 ++++- .../content/service/ContentServiceTest.java | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) 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 ac3dd2e..294671d 100644 --- a/src/main/java/com/devpick/domain/content/service/ContentService.java +++ b/src/main/java/com/devpick/domain/content/service/ContentService.java @@ -74,6 +74,7 @@ public class ContentService { private final AiSummaryService aiSummaryService; private final ContentViewLogService contentViewLogService; private final SimilarContentClient similarContentClient; + private final RecommendService recommendService; private final StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper; @@ -110,7 +111,7 @@ public ContentListResponse getFeed(UUID userId, Pageable pageable) { } List tagIds = loggedIn - ? feedTagIds(userId, profileUser.map(User::getJob).orElse(null)) + ? resolveTagIds(userId, profileUser.map(User::getJob).orElse(null)) : List.of(); Page page; @@ -189,6 +190,14 @@ private String publicFeedCacheKey(UUID userId, Pageable pageable) { + ":sort:" + pageable.getSort(); } + private List resolveTagIds(UUID userId, Job job) { + List historyTagIds = recommendService.getOrCacheTagIds(userId); + if (!historyTagIds.isEmpty()) { + return historyTagIds; + } + return feedTagIds(userId, job); + } + private List feedTagIds(UUID userId, Job job) { List tagIds = new ArrayList<>(userTagRepository.findByUser_Id(userId).stream() .map(ut -> ut.getTag().getId()) 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 4d15d5f..c4e58de 100644 --- a/src/test/java/com/devpick/domain/content/service/ContentServiceTest.java +++ b/src/test/java/com/devpick/domain/content/service/ContentServiceTest.java @@ -80,6 +80,8 @@ class ContentServiceTest { private ContentViewLogService contentViewLogService; @Mock private com.devpick.domain.content.client.SimilarContentClient similarContentClient; + @Mock + private RecommendService recommendService; private UUID userId; private UUID contentId; @@ -111,6 +113,7 @@ void setUp() { .build(); lenient().when(userRepository.findByIdAndIsActiveTrue(userId)).thenReturn(Optional.of(user)); lenient().when(tagRepository.findByNameIgnoreCaseIn(any())).thenReturn(List.of()); + lenient().when(recommendService.getOrCacheTagIds(any())).thenReturn(List.of()); } @Test @@ -646,6 +649,48 @@ void getDetail_viewLogThrows_stillReturnsDetail() { assertThat(response.title()).isEqualTo("Spring Boot 가이드"); } + // ── getFeed 학습 이력 기반 개인화 ───────────────────────────────────────── + + @Test + @DisplayName("getFeed — 학습 이력 태그 있으면 이력 기반 태그로 랭킹, userTagRepository 미조회") + void getFeed_withHistoryTags_usesHistoryTagsForRanking() { + UUID historyTagId = UUID.randomUUID(); + given(recommendService.getOrCacheTagIds(userId)).willReturn(List.of(historyTagId)); + given(contentRepository.findAllRankedByTagIds(any(), any())) + .willReturn(new PageImpl<>(List.of(content))); + given(scrapRepository.findScrappedContentIds(any(), any())).willReturn(List.of()); + given(likeRepository.findLikedContentIds(any(), any())).willReturn(List.of()); + given(aiSummaryService.findCachedCoreSummaries(any(), any())).willReturn(Map.of()); + + ContentListResponse response = contentService.getFeed(userId, PageRequest.of(0, 20)); + + assertThat(response.contents()).hasSize(1); + verify(contentRepository).findAllRankedByTagIds(any(), any()); + verify(userTagRepository, never()).findByUser_Id(any()); + } + + @Test + @DisplayName("getFeed — 학습 이력 태그 없으면 회원가입 태그로 폴백하여 랭킹") + void getFeed_historyTagsEmpty_fallsBackToUserTags() { + given(recommendService.getOrCacheTagIds(userId)).willReturn(List.of()); + UserTag userTag = UserTag.builder() + .user(user) + .tag(Tag.builder().name("Java").build()) + .build(); + given(userTagRepository.findByUser_Id(userId)).willReturn(List.of(userTag)); + given(contentRepository.findAllRankedByTagIds(any(), any())) + .willReturn(new PageImpl<>(List.of(content))); + given(scrapRepository.findScrappedContentIds(any(), any())).willReturn(List.of()); + given(likeRepository.findLikedContentIds(any(), any())).willReturn(List.of()); + given(aiSummaryService.findCachedCoreSummaries(any(), any())).willReturn(Map.of()); + + ContentListResponse response = contentService.getFeed(userId, PageRequest.of(0, 20)); + + assertThat(response.contents()).hasSize(1); + verify(contentRepository).findAllRankedByTagIds(any(), any()); + verify(userTagRepository).findByUser_Id(userId); + } + // ── getFeed 플랜 제한 ──────────────────────────────────────────────────── @Test