From 14394b82638772a10c0957962fa322a5e439309a Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Sep 2026 23:21:15 +0900 Subject: [PATCH] =?UTF-8?q?feat(home-recommendation):=20=EC=B2=AB=20?= =?UTF-8?q?=EC=98=A4=EB=94=94=EC=98=A4=20=EC=A1=B0=ED=9A=8C=20=EC=96=B8?= =?UTF-8?q?=EC=96=B4=EB=A5=BC=20=EC=A0=84=EB=8B=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeRecommendationQueryService.kt | 6 ++- .../port/out/HomeRecommendationQueryPort.kt | 3 +- .../HomeRecommendationQueryServiceTest.kt | 44 ++++++++++++------- 3 files changed, 34 insertions(+), 19 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 f225999d..8611c9c8 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 @@ -1,6 +1,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.recommendation.domain.RecommendationSnapshotWindowPolicy import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType @@ -29,7 +30,8 @@ class HomeRecommendationQueryService( private val aiCharacterSnapshotFallbackService: AiCharacterSnapshotFallbackPort? = null, private val cheerCreatorSnapshotFallbackService: CheerCreatorSnapshotFallbackPort? = null, private val popularCommunitySnapshotFallbackService: PopularCommunitySnapshotFallbackPort? = null, - private val clock: Clock = Clock.systemUTC() + private val clock: Clock = Clock.systemUTC(), + private val langContext: LangContext ) { private val snapshotWindowPolicy = RecommendationSnapshotWindowPolicy() @@ -76,7 +78,7 @@ class HomeRecommendationQueryService( memberId: Long? = null, includeAdultContents: Boolean = false ): List { - return queryPort.findFirstAudioContents(now, offset, limit, memberId, includeAdultContents) + return queryPort.findFirstAudioContents(now, offset, limit, memberId, includeAdultContents, langContext.lang.code) } fun findAiCharacterRecommendations( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt index 26578c8e..bd4d6fed 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt @@ -38,7 +38,8 @@ interface HomeRecommendationQueryPort { offset: Long = 0, limit: Int, memberId: Long? = null, - includeAdultContents: Boolean = false + includeAdultContents: Boolean = false, + locale: String ): List fun findAiCharacterSnapshots( 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 ddd59d3c..2d462146 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 @@ -1,6 +1,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.recommendation.domain.RecommendedSectionType import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord @@ -27,7 +28,8 @@ import java.time.ZoneOffset class HomeRecommendationQueryServiceTest { private val port = FakeHomeRecommendationQueryPort() private val snapshotPort = FakeHomeRecommendationSnapshotPort() - private val service = HomeRecommendationQueryService(port, snapshotPort) + private val langContext = LangContext() + private val service = HomeRecommendationQueryService(port, snapshotPort, langContext = langContext) @Test @DisplayName("다시듣기 테마 콘텐츠는 AUDIO가 아니라 LIVE_REPLAY 활동으로 분류한다") @@ -152,6 +154,7 @@ class HomeRecommendationQueryServiceTest { assertEquals(now, port.firstAudioNow) assertEquals(103L, port.firstAudioMemberId) assertEquals(10, port.firstAudioLimit) + assertEquals(Lang.KO.code, port.firstAudioLocale) assertEquals(port.firstAudioContents, contents) } @@ -159,7 +162,7 @@ class HomeRecommendationQueryServiceTest { @DisplayName("AI 캐릭터 추천은 최신 스냅샷 20개를 기준으로 순서를 유지해 상세를 조립한다") fun shouldFindAiCharactersFromLatestSnapshotsWithLimitAndDetails() { val fallback = FakeAiCharacterSnapshotFallbackPort(emptyList()) - val service = HomeRecommendationQueryService(port, snapshotPort, fallback) + val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59) val latestSnapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59) snapshotPort.replaceSnapshots( @@ -261,7 +264,7 @@ 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) + val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) port.aiCharacterDetails = listOf( HomeAiCharacterRecommendationRecord( characterId = 10L, @@ -286,7 +289,7 @@ class HomeRecommendationQueryServiceTest { @DisplayName("AI 캐릭터 추천은 fallback 후에도 스냅샷이 없으면 빈 배열을 반환한다") fun shouldReturnEmptyAiCharactersWhenFallbackDoesNotCreateSnapshots() { val fallback = FakeAiCharacterSnapshotFallbackPort(emptyList()) - val service = HomeRecommendationQueryService(port, snapshotPort, fallback) + val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) val characters = service.findAiCharacterRecommendations() @@ -302,7 +305,7 @@ 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) + val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) snapshotPort.replaceSnapshots( RecommendedSectionType.AI_CHARACTER, LocalDateTime.of(2026, 7, 9, 14, 59, 59), @@ -325,7 +328,7 @@ 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) + val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( RecommendedSectionType.AI_CHARACTER, @@ -352,7 +355,7 @@ 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) + val service = HomeRecommendationQueryService(port, snapshotPort, fallback, langContext = langContext) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( RecommendedSectionType.AI_CHARACTER, @@ -376,7 +379,8 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, cheerCreatorSnapshotFallbackService = fallback, - clock = fixedClock() + clock = fixedClock(), + langContext = langContext ) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( @@ -419,7 +423,8 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, cheerCreatorSnapshotFallbackService = fallback, - clock = fixedClock() + clock = fixedClock(), + langContext = langContext ) port.cheerCreatorDetails = listOf( HomeCheerCreatorRecommendationRecord( @@ -448,7 +453,8 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, cheerCreatorSnapshotFallbackService = fallback, - clock = fixedClock() + clock = fixedClock(), + langContext = langContext ) val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59) snapshotPort.replaceSnapshots( @@ -483,7 +489,8 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, cheerCreatorSnapshotFallbackService = fallback, - clock = fixedClock() + clock = fixedClock(), + langContext = langContext ) snapshotPort.replaceSnapshots( RecommendedSectionType.CHEER_CREATOR, @@ -502,7 +509,7 @@ class HomeRecommendationQueryServiceTest { @Test @DisplayName("인기 커뮤니티 추천은 대상일 스냅샷 10개를 기준으로 크리에이터 중복을 제거하고 상세를 조립한다") fun shouldFindPopularCommunitiesFromTargetDaySnapshotsWithLimitDetailsAndCreatorUniqueness() { - val service = HomeRecommendationQueryService(port, snapshotPort, clock = fixedClock()) + val service = HomeRecommendationQueryService(port, snapshotPort, clock = fixedClock(), langContext = langContext) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( RecommendedSectionType.POPULAR_COMMUNITY, @@ -576,7 +583,7 @@ class HomeRecommendationQueryServiceTest { @Test @DisplayName("인기 커뮤니티 추천은 상위 스냅샷 중복 제거 후 뒤 후보로 기본 10개를 채운다") fun shouldBackfillPopularCommunitiesAfterRemovingDuplicateCreators() { - val service = HomeRecommendationQueryService(port, snapshotPort, clock = fixedClock()) + val service = HomeRecommendationQueryService(port, snapshotPort, clock = fixedClock(), langContext = langContext) val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59) snapshotPort.replaceSnapshots( RecommendedSectionType.POPULAR_COMMUNITY, @@ -621,7 +628,8 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, popularCommunitySnapshotFallbackService = fallback, - clock = fixedClock() + clock = fixedClock(), + langContext = langContext ) val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59) snapshotPort.replaceSnapshots( @@ -666,7 +674,8 @@ class HomeRecommendationQueryServiceTest { port, snapshotPort, popularCommunitySnapshotFallbackService = fallback, - clock = fixedClock() + clock = fixedClock(), + langContext = langContext ) snapshotPort.replaceSnapshots( RecommendedSectionType.POPULAR_COMMUNITY, @@ -991,6 +1000,7 @@ class HomeRecommendationQueryServiceTest { var firstAudioOffset: Long? = null var firstAudioMemberId: Long? = null var firstAudioIncludeAdultContents: Boolean? = null + var firstAudioLocale: String? = null var aiCharacterDetailIds: List = emptyList() val aiCharacterDetailIdRequests = mutableListOf>() var randomAiCharacterExcludeIds: List? = null @@ -1121,13 +1131,15 @@ class HomeRecommendationQueryServiceTest { offset: Long, limit: Int, memberId: Long?, - includeAdultContents: Boolean + includeAdultContents: Boolean, + locale: String ): List { firstAudioNow = now firstAudioOffset = offset firstAudioLimit = limit firstAudioMemberId = memberId firstAudioIncludeAdultContents = includeAdultContents + firstAudioLocale = locale return firstAudioContents }