feat(recommendation): 인기 커뮤니티 본문 번역을 적용한다
This commit is contained in:
+15
-2
@@ -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.Lang
|
||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
|
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.RecommendationSnapshotWindowPolicy
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
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.HomeAiCharacterRecommendationRecord
|
||||||
@@ -31,7 +32,8 @@ class HomeRecommendationQueryService(
|
|||||||
private val cheerCreatorSnapshotFallbackService: CheerCreatorSnapshotFallbackPort? = null,
|
private val cheerCreatorSnapshotFallbackService: CheerCreatorSnapshotFallbackPort? = null,
|
||||||
private val popularCommunitySnapshotFallbackService: PopularCommunitySnapshotFallbackPort? = null,
|
private val popularCommunitySnapshotFallbackService: PopularCommunitySnapshotFallbackPort? = null,
|
||||||
private val clock: Clock = Clock.systemUTC(),
|
private val clock: Clock = Clock.systemUTC(),
|
||||||
private val langContext: LangContext
|
private val langContext: LangContext,
|
||||||
|
private val creatorCommunityTranslationService: CreatorCommunityTranslationService
|
||||||
) {
|
) {
|
||||||
private val snapshotWindowPolicy = RecommendationSnapshotWindowPolicy()
|
private val snapshotWindowPolicy = RecommendationSnapshotWindowPolicy()
|
||||||
|
|
||||||
@@ -154,9 +156,20 @@ class HomeRecommendationQueryService(
|
|||||||
.associateBy { it.communityId }
|
.associateBy { it.communityId }
|
||||||
val selectedCreatorIds = mutableSetOf<Long>()
|
val selectedCreatorIds = mutableSetOf<Long>()
|
||||||
|
|
||||||
return snapshots.mapNotNull { snapshot ->
|
val recommendations = snapshots.mapNotNull { snapshot ->
|
||||||
detailsById[snapshot.targetId]?.takeIf { selectedCreatorIds.add(it.creatorId) }
|
detailsById[snapshot.targetId]?.takeIf { selectedCreatorIds.add(it.creatorId) }
|
||||||
}.take(limit)
|
}.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<RecommendationSnapshotRecord> {
|
private fun findPopularCommunitySnapshotsWithFallback(): List<RecommendationSnapshotRecord> {
|
||||||
|
|||||||
+75
@@ -1,6 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.api.home
|
package kr.co.vividnext.sodalive.v2.api.home
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
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.live.room.LiveRoom
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
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.member.following.CreatorFollowingRepository
|
||||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||||
import kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacade
|
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.adapter.out.persistence.RecommendationSnapshot
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicy
|
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.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
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.CapturedOutput
|
||||||
import org.springframework.boot.test.system.OutputCaptureExtension
|
import org.springframework.boot.test.system.OutputCaptureExtension
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
@@ -53,6 +57,9 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
private val creatorFollowingRepository: CreatorFollowingRepository,
|
private val creatorFollowingRepository: CreatorFollowingRepository,
|
||||||
private val entityManager: EntityManager
|
private val entityManager: EntityManager
|
||||||
) {
|
) {
|
||||||
|
@MockBean
|
||||||
|
private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("추천 크리에이터 동시 팔로우 비로그인 요청은 Spring Security에서 거부한다")
|
@DisplayName("추천 크리에이터 동시 팔로우 비로그인 요청은 Spring Security에서 거부한다")
|
||||||
fun shouldRejectAnonymousFollowRequest() {
|
fun shouldRejectAnonymousFollowRequest() {
|
||||||
@@ -230,6 +237,60 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
assertTrue(output.out.contains("emptySections="))
|
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
|
@Test
|
||||||
@DisplayName("메인 홈 통합 조회는 인증 회원도 호출 가능하고 성공 응답한다")
|
@DisplayName("메인 홈 통합 조회는 인증 회원도 호출 가능하고 성공 응답한다")
|
||||||
fun shouldReturnHomeRecommendationsForMember() {
|
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(
|
private fun saveLiveRoom(
|
||||||
creator: Member,
|
creator: Member,
|
||||||
beginDateTime: LocalDateTime,
|
beginDateTime: LocalDateTime,
|
||||||
|
|||||||
+4
-1
@@ -4,6 +4,7 @@ import kr.co.vividnext.sodalive.i18n.Lang
|
|||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
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.application.HomeRecommendationQueryService
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
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.HomeAiCharacterRecommendationRecord
|
||||||
@@ -27,10 +28,12 @@ import java.time.LocalDateTime
|
|||||||
|
|
||||||
class HomeRecommendationFacadeTest {
|
class HomeRecommendationFacadeTest {
|
||||||
private val queryPort = FakeHomeRecommendationQueryPort()
|
private val queryPort = FakeHomeRecommendationQueryPort()
|
||||||
|
private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java)
|
||||||
private val queryService = HomeRecommendationQueryService(
|
private val queryService = HomeRecommendationQueryService(
|
||||||
queryPort,
|
queryPort,
|
||||||
EmptyRecommendationSnapshotPort(),
|
EmptyRecommendationSnapshotPort(),
|
||||||
langContext = LangContext()
|
langContext = LangContext(),
|
||||||
|
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||||
)
|
)
|
||||||
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||||
private val facade = HomeRecommendationFacade(queryService, preferenceService, "https://cdn.test")
|
private val facade = HomeRecommendationFacade(queryService, preferenceService, "https://cdn.test")
|
||||||
|
|||||||
+146
-15
@@ -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.Lang
|
||||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
|
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.domain.RecommendedSectionType
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord
|
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeBannerRecommendationRecord
|
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.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -29,7 +32,18 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
private val port = FakeHomeRecommendationQueryPort()
|
private val port = FakeHomeRecommendationQueryPort()
|
||||||
private val snapshotPort = FakeHomeRecommendationSnapshotPort()
|
private val snapshotPort = FakeHomeRecommendationSnapshotPort()
|
||||||
private val langContext = LangContext()
|
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
|
@Test
|
||||||
@DisplayName("다시듣기 테마 콘텐츠는 AUDIO가 아니라 LIVE_REPLAY 활동으로 분류한다")
|
@DisplayName("다시듣기 테마 콘텐츠는 AUDIO가 아니라 LIVE_REPLAY 활동으로 분류한다")
|
||||||
@@ -162,7 +176,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
@DisplayName("AI 캐릭터 추천은 최신 스냅샷 20개를 기준으로 순서를 유지해 상세를 조립한다")
|
@DisplayName("AI 캐릭터 추천은 최신 스냅샷 20개를 기준으로 순서를 유지해 상세를 조립한다")
|
||||||
fun shouldFindAiCharactersFromLatestSnapshotsWithLimitAndDetails() {
|
fun shouldFindAiCharactersFromLatestSnapshotsWithLimitAndDetails() {
|
||||||
val fallback = FakeAiCharacterSnapshotFallbackPort(emptyList())
|
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 oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
|
||||||
val latestSnapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
val latestSnapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
@@ -264,7 +284,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
val fallback = FakeAiCharacterSnapshotFallbackPort(
|
val fallback = FakeAiCharacterSnapshotFallbackPort(
|
||||||
listOf(snapshot(RecommendedSectionType.AI_CHARACTER, 10L, 10.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59)))
|
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(
|
port.aiCharacterDetails = listOf(
|
||||||
HomeAiCharacterRecommendationRecord(
|
HomeAiCharacterRecommendationRecord(
|
||||||
characterId = 10L,
|
characterId = 10L,
|
||||||
@@ -289,7 +315,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
@DisplayName("AI 캐릭터 추천은 fallback 후에도 스냅샷이 없으면 빈 배열을 반환한다")
|
@DisplayName("AI 캐릭터 추천은 fallback 후에도 스냅샷이 없으면 빈 배열을 반환한다")
|
||||||
fun shouldReturnEmptyAiCharactersWhenFallbackDoesNotCreateSnapshots() {
|
fun shouldReturnEmptyAiCharactersWhenFallbackDoesNotCreateSnapshots() {
|
||||||
val fallback = FakeAiCharacterSnapshotFallbackPort(emptyList())
|
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()
|
val characters = service.findAiCharacterRecommendations()
|
||||||
|
|
||||||
@@ -305,7 +337,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
val fallback = FakeAiCharacterSnapshotFallbackPort(
|
val fallback = FakeAiCharacterSnapshotFallbackPort(
|
||||||
listOf(snapshot(RecommendedSectionType.AI_CHARACTER, 99L, 99.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59)))
|
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(
|
snapshotPort.replaceSnapshots(
|
||||||
RecommendedSectionType.AI_CHARACTER,
|
RecommendedSectionType.AI_CHARACTER,
|
||||||
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
||||||
@@ -328,7 +366,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
val fallback = FakeAiCharacterSnapshotFallbackPort(
|
val fallback = FakeAiCharacterSnapshotFallbackPort(
|
||||||
listOf(snapshot(RecommendedSectionType.AI_CHARACTER, 99L, 99.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59)))
|
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)
|
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
RecommendedSectionType.AI_CHARACTER,
|
RecommendedSectionType.AI_CHARACTER,
|
||||||
@@ -355,7 +399,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
val fallback = FakeAiCharacterSnapshotFallbackPort(
|
val fallback = FakeAiCharacterSnapshotFallbackPort(
|
||||||
listOf(snapshot(RecommendedSectionType.AI_CHARACTER, 99L, 99.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59)))
|
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)
|
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
RecommendedSectionType.AI_CHARACTER,
|
RecommendedSectionType.AI_CHARACTER,
|
||||||
@@ -380,7 +430,8 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
snapshotPort,
|
snapshotPort,
|
||||||
cheerCreatorSnapshotFallbackService = fallback,
|
cheerCreatorSnapshotFallbackService = fallback,
|
||||||
clock = fixedClock(),
|
clock = fixedClock(),
|
||||||
langContext = langContext
|
langContext = langContext,
|
||||||
|
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||||
)
|
)
|
||||||
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
@@ -424,7 +475,8 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
snapshotPort,
|
snapshotPort,
|
||||||
cheerCreatorSnapshotFallbackService = fallback,
|
cheerCreatorSnapshotFallbackService = fallback,
|
||||||
clock = fixedClock(),
|
clock = fixedClock(),
|
||||||
langContext = langContext
|
langContext = langContext,
|
||||||
|
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||||
)
|
)
|
||||||
port.cheerCreatorDetails = listOf(
|
port.cheerCreatorDetails = listOf(
|
||||||
HomeCheerCreatorRecommendationRecord(
|
HomeCheerCreatorRecommendationRecord(
|
||||||
@@ -454,7 +506,8 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
snapshotPort,
|
snapshotPort,
|
||||||
cheerCreatorSnapshotFallbackService = fallback,
|
cheerCreatorSnapshotFallbackService = fallback,
|
||||||
clock = fixedClock(),
|
clock = fixedClock(),
|
||||||
langContext = langContext
|
langContext = langContext,
|
||||||
|
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||||
)
|
)
|
||||||
val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59)
|
val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
@@ -490,7 +543,8 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
snapshotPort,
|
snapshotPort,
|
||||||
cheerCreatorSnapshotFallbackService = fallback,
|
cheerCreatorSnapshotFallbackService = fallback,
|
||||||
clock = fixedClock(),
|
clock = fixedClock(),
|
||||||
langContext = langContext
|
langContext = langContext,
|
||||||
|
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||||
)
|
)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
RecommendedSectionType.CHEER_CREATOR,
|
RecommendedSectionType.CHEER_CREATOR,
|
||||||
@@ -509,7 +563,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("인기 커뮤니티 추천은 대상일 스냅샷 10개를 기준으로 크리에이터 중복을 제거하고 상세를 조립한다")
|
@DisplayName("인기 커뮤니티 추천은 대상일 스냅샷 10개를 기준으로 크리에이터 중복을 제거하고 상세를 조립한다")
|
||||||
fun shouldFindPopularCommunitiesFromTargetDaySnapshotsWithLimitDetailsAndCreatorUniqueness() {
|
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)
|
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
RecommendedSectionType.POPULAR_COMMUNITY,
|
||||||
@@ -583,7 +643,13 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("인기 커뮤니티 추천은 상위 스냅샷 중복 제거 후 뒤 후보로 기본 10개를 채운다")
|
@DisplayName("인기 커뮤니티 추천은 상위 스냅샷 중복 제거 후 뒤 후보로 기본 10개를 채운다")
|
||||||
fun shouldBackfillPopularCommunitiesAfterRemovingDuplicateCreators() {
|
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)
|
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
RecommendedSectionType.POPULAR_COMMUNITY,
|
||||||
@@ -618,6 +684,47 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
assertEquals(communities.size, communities.map { it.creatorId }.toSet().size)
|
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
|
@Test
|
||||||
@DisplayName("인기 커뮤니티 추천은 대상일 스냅샷이 없고 오래된 스냅샷만 있으면 fallback refresh를 호출한다")
|
@DisplayName("인기 커뮤니티 추천은 대상일 스냅샷이 없고 오래된 스냅샷만 있으면 fallback refresh를 호출한다")
|
||||||
fun shouldFallbackWhenOnlyStalePopularCommunitySnapshotsExist() {
|
fun shouldFallbackWhenOnlyStalePopularCommunitySnapshotsExist() {
|
||||||
@@ -629,7 +736,8 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
snapshotPort,
|
snapshotPort,
|
||||||
popularCommunitySnapshotFallbackService = fallback,
|
popularCommunitySnapshotFallbackService = fallback,
|
||||||
clock = fixedClock(),
|
clock = fixedClock(),
|
||||||
langContext = langContext
|
langContext = langContext,
|
||||||
|
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||||
)
|
)
|
||||||
val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59)
|
val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
@@ -675,7 +783,8 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
snapshotPort,
|
snapshotPort,
|
||||||
popularCommunitySnapshotFallbackService = fallback,
|
popularCommunitySnapshotFallbackService = fallback,
|
||||||
clock = fixedClock(),
|
clock = fixedClock(),
|
||||||
langContext = langContext
|
langContext = langContext,
|
||||||
|
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||||
)
|
)
|
||||||
snapshotPort.replaceSnapshots(
|
snapshotPort.replaceSnapshots(
|
||||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
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 class FakeHomeRecommendationSnapshotPort : RecommendationSnapshotPort {
|
||||||
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
|
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user