From da5937318470dbc6be7e69ae8876473279670357 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 10 Sep 2026 15:14:44 +0900 Subject: [PATCH] =?UTF-8?q?feat(recommendation):=20=EC=9D=B8=EA=B8=B0=20?= =?UTF-8?q?=EC=BB=A4=EB=AE=A4=EB=8B=88=ED=8B=B0=20=EB=B3=B8=EB=AC=B8=20?= =?UTF-8?q?=EB=B2=88=EC=97=AD=EC=9D=84=20=EC=A0=81=EC=9A=A9=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeRecommendationQueryService.kt | 17 +- .../home/HomeRecommendationControllerTest.kt | 75 ++++++++ .../HomeRecommendationFacadeTest.kt | 5 +- .../HomeRecommendationQueryServiceTest.kt | 161 ++++++++++++++++-- 4 files changed, 240 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt index 8611c9c8..d9c42f1c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.recommendation.application import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType +import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicy import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord @@ -31,7 +32,8 @@ class HomeRecommendationQueryService( private val cheerCreatorSnapshotFallbackService: CheerCreatorSnapshotFallbackPort? = null, private val popularCommunitySnapshotFallbackService: PopularCommunitySnapshotFallbackPort? = null, private val clock: Clock = Clock.systemUTC(), - private val langContext: LangContext + private val langContext: LangContext, + private val creatorCommunityTranslationService: CreatorCommunityTranslationService ) { private val snapshotWindowPolicy = RecommendationSnapshotWindowPolicy() @@ -154,9 +156,20 @@ class HomeRecommendationQueryService( .associateBy { it.communityId } val selectedCreatorIds = mutableSetOf() - return snapshots.mapNotNull { snapshot -> + val recommendations = snapshots.mapNotNull { snapshot -> detailsById[snapshot.targetId]?.takeIf { selectedCreatorIds.add(it.creatorId) } }.take(limit) + if (recommendations.isEmpty()) return emptyList() + + val displayContents = creatorCommunityTranslationService.findDisplayContents( + recommendations.map { it.communityId }, + langContext.lang.code + ) + return recommendations.map { recommendation -> + displayContents[recommendation.communityId] + ?.let { displayContent -> recommendation.copy(content = displayContent.content) } + ?: recommendation + } } private fun findPopularCommunitySnapshotsWithFallback(): List { diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt index a32db47b..26c98681 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.v2.api.home import kr.co.vividnext.sodalive.chat.character.ChatCharacter +import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberAdapter @@ -12,6 +13,8 @@ import kr.co.vividnext.sodalive.member.following.CreatorFollowing import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer import kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacade +import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityDisplayContent +import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService import kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshot import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicy @@ -27,6 +30,7 @@ import org.mockito.Mockito import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.boot.test.system.CapturedOutput import org.springframework.boot.test.system.OutputCaptureExtension import org.springframework.http.MediaType @@ -53,6 +57,9 @@ class HomeRecommendationControllerTest @Autowired constructor( private val creatorFollowingRepository: CreatorFollowingRepository, private val entityManager: EntityManager ) { + @MockBean + private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService + @Test @DisplayName("추천 크리에이터 동시 팔로우 비로그인 요청은 Spring Security에서 거부한다") fun shouldRejectAnonymousFollowRequest() { @@ -230,6 +237,60 @@ class HomeRecommendationControllerTest @Autowired constructor( assertTrue(output.out.contains("emptySections=")) } + @Test + @DisplayName("비회원 홈 추천은 반환된 인기 커뮤니티 게시물만 일본어 표시 본문으로 반환한다") + fun shouldReturnJapanesePopularCommunityTranslationForAnonymous() { + val translatedCreator = saveMember("popular-community-translated-creator", MemberRole.CREATOR) + val fallbackCreator = saveMember("popular-community-fallback-creator", MemberRole.CREATOR) + val translatedPost = saveCommunityPost(translatedCreator, "historical translated popular body") + val fallbackPost = saveCommunityPost(fallbackCreator, "historical fallback popular body") + val snapshotAt = RecommendationSnapshotWindowPolicy() + .previousKstDayUtcWindow(LocalDateTime.now(ZoneOffset.UTC)) + .snapshotAt + entityManager.persist( + RecommendationSnapshot( + sectionType = RecommendedSectionType.POPULAR_COMMUNITY, + targetId = translatedPost.id!!, + score = 100.0, + snapshotAt = snapshotAt, + randomTieBreaker = 0.1 + ) + ) + entityManager.persist( + RecommendationSnapshot( + sectionType = RecommendedSectionType.POPULAR_COMMUNITY, + targetId = fallbackPost.id!!, + score = 90.0, + snapshotAt = snapshotAt, + randomTieBreaker = 0.2 + ) + ) + entityManager.flush() + entityManager.clear() + Mockito.`when`( + creatorCommunityTranslationService.findDisplayContents( + listOf(translatedPost.id!!, fallbackPost.id!!), + "ja" + ) + ).thenReturn( + mapOf( + translatedPost.id!! to CreatorCommunityDisplayContent("translated popular body ja", true), + fallbackPost.id!! to CreatorCommunityDisplayContent("current fallback popular body ja", false) + ) + ) + + mockMvc.perform(get("/api/v2/home/recommendations").header("Accept-Language", "ja")) + .andExpect(status().isOk) + .andExpect(jsonPath("$.data.popularCommunityPosts[0].content").value("translated popular body ja")) + .andExpect(jsonPath("$.data.popularCommunityPosts[1].content").value("current fallback popular body ja")) + + Mockito.verify(creatorCommunityTranslationService).findDisplayContents( + listOf(translatedPost.id!!, fallbackPost.id!!), + "ja" + ) + Mockito.verifyNoMoreInteractions(creatorCommunityTranslationService) + } + @Test @DisplayName("메인 홈 통합 조회는 인증 회원도 호출 가능하고 성공 응답한다") fun shouldReturnHomeRecommendationsForMember() { @@ -590,6 +651,20 @@ class HomeRecommendationControllerTest @Autowired constructor( ) } + private fun saveCommunityPost(creator: Member, content: String): CreatorCommunity { + val post = CreatorCommunity( + content = content, + price = 0, + isCommentAvailable = true, + isAdult = false, + isActive = true + ).apply { + member = creator + } + entityManager.persist(post) + return post + } + private fun saveLiveRoom( creator: Member, beginDateTime: LocalDateTime, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt index 56ad10cf..398e70a1 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacadeTest.kt @@ -4,6 +4,7 @@ import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService +import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord @@ -27,10 +28,12 @@ import java.time.LocalDateTime class HomeRecommendationFacadeTest { private val queryPort = FakeHomeRecommendationQueryPort() + private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java) private val queryService = HomeRecommendationQueryService( queryPort, EmptyRecommendationSnapshotPort(), - langContext = LangContext() + langContext = LangContext(), + creatorCommunityTranslationService = creatorCommunityTranslationService ) private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java) private val facade = HomeRecommendationFacade(queryService, preferenceService, "https://cdn.test") diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt index 2d462146..6ad3d381 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt @@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.v2.recommendation.application import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType +import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityDisplayContent +import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeBannerRecommendationRecord @@ -20,6 +22,7 @@ import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapsho import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.mockito.Mockito import java.time.Clock import java.time.Instant import java.time.LocalDateTime @@ -29,7 +32,18 @@ class HomeRecommendationQueryServiceTest { private val port = FakeHomeRecommendationQueryPort() private val snapshotPort = FakeHomeRecommendationSnapshotPort() private val langContext = LangContext() - private val service = HomeRecommendationQueryService(port, snapshotPort, langContext = langContext) + private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java) + private val service = HomeRecommendationQueryService( + port, + snapshotPort, + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) + + init { + Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(Mockito.anyList(), Mockito.anyString())) + .thenReturn(emptyMap()) + } @Test @DisplayName("다시듣기 테마 콘텐츠는 AUDIO가 아니라 LIVE_REPLAY 활동으로 분류한다") @@ -162,7 +176,13 @@ class HomeRecommendationQueryServiceTest { @DisplayName("AI 캐릭터 추천은 최신 스냅샷 20개를 기준으로 순서를 유지해 상세를 조립한다") fun shouldFindAiCharactersFromLatestSnapshotsWithLimitAndDetails() { val fallback = FakeAiCharacterSnapshotFallbackPort(emptyList()) - val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) + val service = HomeRecommendationQueryService( + port, + snapshotPort, + fallback, + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59) val latestSnapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59) snapshotPort.replaceSnapshots( @@ -264,7 +284,13 @@ class HomeRecommendationQueryServiceTest { val fallback = FakeAiCharacterSnapshotFallbackPort( listOf(snapshot(RecommendedSectionType.AI_CHARACTER, 10L, 10.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59))) ) - val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) + val service = HomeRecommendationQueryService( + port, + snapshotPort, + fallback, + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) port.aiCharacterDetails = listOf( HomeAiCharacterRecommendationRecord( characterId = 10L, @@ -289,7 +315,13 @@ class HomeRecommendationQueryServiceTest { @DisplayName("AI 캐릭터 추천은 fallback 후에도 스냅샷이 없으면 빈 배열을 반환한다") fun shouldReturnEmptyAiCharactersWhenFallbackDoesNotCreateSnapshots() { val fallback = FakeAiCharacterSnapshotFallbackPort(emptyList()) - val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) + val service = HomeRecommendationQueryService( + port, + snapshotPort, + fallback, + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) val characters = service.findAiCharacterRecommendations() @@ -305,7 +337,13 @@ class HomeRecommendationQueryServiceTest { val fallback = FakeAiCharacterSnapshotFallbackPort( listOf(snapshot(RecommendedSectionType.AI_CHARACTER, 99L, 99.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59))) ) - val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) + val service = HomeRecommendationQueryService( + port, + snapshotPort, + fallback, + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) snapshotPort.replaceSnapshots( RecommendedSectionType.AI_CHARACTER, LocalDateTime.of(2026, 7, 9, 14, 59, 59), @@ -328,7 +366,13 @@ class HomeRecommendationQueryServiceTest { val fallback = FakeAiCharacterSnapshotFallbackPort( listOf(snapshot(RecommendedSectionType.AI_CHARACTER, 99L, 99.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59))) ) - val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) + val service = HomeRecommendationQueryService( + port, + snapshotPort, + fallback, + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( RecommendedSectionType.AI_CHARACTER, @@ -355,7 +399,13 @@ class HomeRecommendationQueryServiceTest { val fallback = FakeAiCharacterSnapshotFallbackPort( listOf(snapshot(RecommendedSectionType.AI_CHARACTER, 99L, 99.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59))) ) - val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) + val service = HomeRecommendationQueryService( + port, + snapshotPort, + fallback, + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( RecommendedSectionType.AI_CHARACTER, @@ -380,7 +430,8 @@ class HomeRecommendationQueryServiceTest { snapshotPort, cheerCreatorSnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( @@ -424,7 +475,8 @@ class HomeRecommendationQueryServiceTest { snapshotPort, cheerCreatorSnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService ) port.cheerCreatorDetails = listOf( HomeCheerCreatorRecommendationRecord( @@ -454,7 +506,8 @@ class HomeRecommendationQueryServiceTest { snapshotPort, cheerCreatorSnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService ) val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59) snapshotPort.replaceSnapshots( @@ -490,7 +543,8 @@ class HomeRecommendationQueryServiceTest { snapshotPort, cheerCreatorSnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService ) snapshotPort.replaceSnapshots( RecommendedSectionType.CHEER_CREATOR, @@ -509,7 +563,13 @@ class HomeRecommendationQueryServiceTest { @Test @DisplayName("인기 커뮤니티 추천은 대상일 스냅샷 10개를 기준으로 크리에이터 중복을 제거하고 상세를 조립한다") fun shouldFindPopularCommunitiesFromTargetDaySnapshotsWithLimitDetailsAndCreatorUniqueness() { - val service = HomeRecommendationQueryService(port, snapshotPort, clock = fixedClock(), langContext = langContext) + val service = HomeRecommendationQueryService( + port, + snapshotPort, + clock = fixedClock(), + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( RecommendedSectionType.POPULAR_COMMUNITY, @@ -583,7 +643,13 @@ class HomeRecommendationQueryServiceTest { @Test @DisplayName("인기 커뮤니티 추천은 상위 스냅샷 중복 제거 후 뒤 후보로 기본 10개를 채운다") fun shouldBackfillPopularCommunitiesAfterRemovingDuplicateCreators() { - val service = HomeRecommendationQueryService(port, snapshotPort, clock = fixedClock(), langContext = langContext) + val service = HomeRecommendationQueryService( + port, + snapshotPort, + clock = fixedClock(), + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( RecommendedSectionType.POPULAR_COMMUNITY, @@ -618,6 +684,47 @@ class HomeRecommendationQueryServiceTest { assertEquals(communities.size, communities.map { it.creatorId }.toSet().size) } + @Test + @DisplayName("인기 커뮤니티 추천은 최종 노출 게시물만 일본어 표시 본문으로 일괄 치환하고 원문 폴백을 유지한다") + fun shouldTranslateOnlyFinalPopularCommunityRecommendationsInOneJapaneseBatch() { + langContext.setLang(Lang.JA) + val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) + snapshotPort.replaceSnapshots( + RecommendedSectionType.POPULAR_COMMUNITY, + snapshotAt, + (1L..4L).map { targetId -> + snapshot(RecommendedSectionType.POPULAR_COMMUNITY, targetId, 100.0 - targetId, snapshotAt) + } + ) + port.popularCommunityDetails = listOf( + popularCommunity(1L, creatorId = 10L, content = "original-1"), + popularCommunity(2L, creatorId = 10L, content = "original-2"), + popularCommunity(3L, creatorId = 11L, content = "historical-original-3"), + popularCommunity(4L, creatorId = 12L, content = "original-4") + ) + Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(1L, 3L), "ja")) + .thenReturn( + mapOf( + 1L to CreatorCommunityDisplayContent("translated-1-ja", true), + 3L to CreatorCommunityDisplayContent("current-original-3", false) + ) + ) + val translatedService = HomeRecommendationQueryService( + port, + snapshotPort, + clock = fixedClock(), + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService + ) + + val communities = translatedService.findPopularCommunityRecommendations(limit = 2) + + assertEquals(listOf(1L, 3L), communities.map { it.communityId }) + assertEquals(listOf("translated-1-ja", "current-original-3"), communities.map { it.content }) + Mockito.verify(creatorCommunityTranslationService).findDisplayContents(listOf(1L, 3L), "ja") + Mockito.verifyNoMoreInteractions(creatorCommunityTranslationService) + } + @Test @DisplayName("인기 커뮤니티 추천은 대상일 스냅샷이 없고 오래된 스냅샷만 있으면 fallback refresh를 호출한다") fun shouldFallbackWhenOnlyStalePopularCommunitySnapshotsExist() { @@ -629,7 +736,8 @@ class HomeRecommendationQueryServiceTest { snapshotPort, popularCommunitySnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService ) val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59) snapshotPort.replaceSnapshots( @@ -675,7 +783,8 @@ class HomeRecommendationQueryServiceTest { snapshotPort, popularCommunitySnapshotFallbackService = fallback, clock = fixedClock(), - langContext = langContext + langContext = langContext, + creatorCommunityTranslationService = creatorCommunityTranslationService ) snapshotPort.replaceSnapshots( RecommendedSectionType.POPULAR_COMMUNITY, @@ -1212,6 +1321,28 @@ class HomeRecommendationQueryServiceTest { } } +private fun popularCommunity( + communityId: Long, + creatorId: Long, + content: String +): HomePopularCommunityRecommendationRecord { + return HomePopularCommunityRecommendationRecord( + communityId = communityId, + creatorId = creatorId, + creatorNickname = "creator-$creatorId", + creatorProfileImage = null, + imagePath = null, + audioPath = null, + content = content, + price = 0, + createdAt = LocalDateTime.of(2026, 7, 9, 1, 0), + likeCount = 0L, + commentCount = 0L, + existOrdered = false, + isLiked = false + ) +} + private class FakeHomeRecommendationSnapshotPort : RecommendationSnapshotPort { private val snapshots = mutableListOf()