test #433
@@ -27,6 +27,7 @@ class HomeRecommendationQueryService(
|
||||
private val snapshotPort: RecommendationSnapshotPort,
|
||||
private val aiCharacterSnapshotFallbackService: AiCharacterSnapshotFallbackPort? = null,
|
||||
private val cheerCreatorSnapshotFallbackService: CheerCreatorSnapshotFallbackPort? = null,
|
||||
private val popularCommunitySnapshotFallbackService: PopularCommunitySnapshotFallbackPort? = null,
|
||||
private val clock: Clock = Clock.systemUTC()
|
||||
) {
|
||||
private val snapshotWindowPolicy = RecommendationSnapshotWindowPolicy()
|
||||
@@ -129,8 +130,7 @@ class HomeRecommendationQueryService(
|
||||
memberId: Long? = null,
|
||||
includeAdultCommunities: Boolean = false
|
||||
): List<HomePopularCommunityRecommendationRecord> {
|
||||
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.POPULAR_COMMUNITY)
|
||||
.take(POPULAR_COMMUNITY_CANDIDATE_LIMIT)
|
||||
val snapshots = findPopularCommunitySnapshotsWithFallback()
|
||||
val detailsById = queryPort.findPopularCommunityRecommendationDetails(
|
||||
snapshots.map { it.targetId },
|
||||
memberId,
|
||||
@@ -144,6 +144,21 @@ class HomeRecommendationQueryService(
|
||||
}.take(limit)
|
||||
}
|
||||
|
||||
private fun findPopularCommunitySnapshotsWithFallback(): List<RecommendationSnapshotRecord> {
|
||||
val nowUtc = LocalDateTime.now(clock)
|
||||
val snapshotAt = snapshotWindowPolicy.previousKstDayUtcWindow(nowUtc).snapshotAt
|
||||
val snapshots = snapshotPort.findSnapshots(
|
||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
||||
snapshotAt,
|
||||
limit = POPULAR_COMMUNITY_CANDIDATE_LIMIT
|
||||
)
|
||||
if (snapshots.isNotEmpty()) return snapshots
|
||||
if (snapshotPort.existsSnapshot(RecommendedSectionType.POPULAR_COMMUNITY, snapshotAt)) return emptyList()
|
||||
return popularCommunitySnapshotFallbackService
|
||||
?.refreshPopularCommunityIfMissing(offset = 0, limit = POPULAR_COMMUNITY_CANDIDATE_LIMIT, nowUtc = nowUtc)
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
fun findGenreCreatorRecommendations(
|
||||
memberId: Long?,
|
||||
includeAdultGenres: Boolean,
|
||||
|
||||
@@ -412,9 +412,10 @@ class HomeRecommendationQueryServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("인기 커뮤니티 추천은 최신 스냅샷 10개를 기준으로 크리에이터 중복을 제거하고 상세를 조립한다")
|
||||
fun shouldFindPopularCommunitiesFromLatestSnapshotsWithLimitDetailsAndCreatorUniqueness() {
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
@DisplayName("인기 커뮤니티 추천은 대상일 스냅샷 10개를 기준으로 크리에이터 중복을 제거하고 상세를 조립한다")
|
||||
fun shouldFindPopularCommunitiesFromTargetDaySnapshotsWithLimitDetailsAndCreatorUniqueness() {
|
||||
val service = HomeRecommendationQueryService(port, snapshotPort, clock = fixedClock())
|
||||
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||
snapshotPort.replaceSnapshots(
|
||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
||||
snapshotAt,
|
||||
@@ -487,7 +488,8 @@ class HomeRecommendationQueryServiceTest {
|
||||
@Test
|
||||
@DisplayName("인기 커뮤니티 추천은 상위 스냅샷 중복 제거 후 뒤 후보로 기본 10개를 채운다")
|
||||
fun shouldBackfillPopularCommunitiesAfterRemovingDuplicateCreators() {
|
||||
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
|
||||
val service = HomeRecommendationQueryService(port, snapshotPort, clock = fixedClock())
|
||||
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
|
||||
snapshotPort.replaceSnapshots(
|
||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
||||
snapshotAt,
|
||||
@@ -521,6 +523,77 @@ class HomeRecommendationQueryServiceTest {
|
||||
assertEquals(communities.size, communities.map { it.creatorId }.toSet().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("인기 커뮤니티 추천은 대상일 스냅샷이 없고 오래된 스냅샷만 있으면 fallback refresh를 호출한다")
|
||||
fun shouldFallbackWhenOnlyStalePopularCommunitySnapshotsExist() {
|
||||
val fallback = FakePopularCommunitySnapshotFallbackPort(
|
||||
listOf(snapshot(RecommendedSectionType.POPULAR_COMMUNITY, 10L, 10.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59)))
|
||||
)
|
||||
val service = HomeRecommendationQueryService(
|
||||
port,
|
||||
snapshotPort,
|
||||
popularCommunitySnapshotFallbackService = fallback,
|
||||
clock = fixedClock()
|
||||
)
|
||||
val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59)
|
||||
snapshotPort.replaceSnapshots(
|
||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
||||
staleSnapshotAt,
|
||||
listOf(snapshot(RecommendedSectionType.POPULAR_COMMUNITY, 99L, 99.0, staleSnapshotAt))
|
||||
)
|
||||
port.popularCommunityDetails = listOf(
|
||||
HomePopularCommunityRecommendationRecord(
|
||||
communityId = 10L,
|
||||
creatorId = 110L,
|
||||
creatorNickname = "fallback-community-creator",
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
val communities = service.findPopularCommunityRecommendations(memberId = 105L)
|
||||
|
||||
assertEquals(0L, fallback.offset)
|
||||
assertEquals(20, fallback.limit)
|
||||
assertEquals(LocalDateTime.of(2026, 7, 9, 21, 0), fallback.nowUtc)
|
||||
assertEquals(listOf(10L), port.popularCommunityDetailIds)
|
||||
assertEquals(listOf(10L), communities.map { it.communityId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("인기 커뮤니티 추천은 빈 스냅샷 refresh 완료 marker가 있으면 fallback refresh를 반복하지 않는다")
|
||||
fun shouldNotFallbackWhenEmptyPopularCommunitySnapshotCycleExists() {
|
||||
val fallback = FakePopularCommunitySnapshotFallbackPort(
|
||||
listOf(snapshot(RecommendedSectionType.POPULAR_COMMUNITY, 99L, 99.0, LocalDateTime.of(2026, 7, 9, 14, 59, 59)))
|
||||
)
|
||||
val service = HomeRecommendationQueryService(
|
||||
port,
|
||||
snapshotPort,
|
||||
popularCommunitySnapshotFallbackService = fallback,
|
||||
clock = fixedClock()
|
||||
)
|
||||
snapshotPort.replaceSnapshots(
|
||||
RecommendedSectionType.POPULAR_COMMUNITY,
|
||||
LocalDateTime.of(2026, 7, 9, 14, 59, 59),
|
||||
emptyList()
|
||||
)
|
||||
|
||||
val communities = service.findPopularCommunityRecommendations(memberId = 105L)
|
||||
|
||||
assertEquals(null, fallback.offset)
|
||||
assertEquals(null, fallback.limit)
|
||||
assertEquals(emptyList<Long>(), port.popularCommunityDetailIds)
|
||||
assertEquals(emptyList<HomePopularCommunityRecommendationRecord>(), communities)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최신 스냅샷이 없으면 AI 캐릭터/최근 응원/인기 커뮤니티 추천은 빈 배열을 반환한다")
|
||||
fun shouldReturnEmptyListWhenLatestSnapshotsDoNotExist() {
|
||||
@@ -969,7 +1042,7 @@ class HomeRecommendationQueryServiceTest {
|
||||
|
||||
override fun findPopularCommunitySnapshots(
|
||||
windowStart: LocalDateTime,
|
||||
snapshotAt: LocalDateTime,
|
||||
windowEndExclusive: LocalDateTime,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> = emptyList()
|
||||
|
||||
@@ -1071,7 +1144,9 @@ private class FakeHomeRecommendationSnapshotPort : RecommendationSnapshotPort {
|
||||
}
|
||||
|
||||
private fun supportsEmptySnapshotMarker(sectionType: RecommendedSectionType): Boolean {
|
||||
return sectionType == RecommendedSectionType.AI_CHARACTER || sectionType == RecommendedSectionType.CHEER_CREATOR
|
||||
return sectionType == RecommendedSectionType.AI_CHARACTER ||
|
||||
sectionType == RecommendedSectionType.CHEER_CREATOR ||
|
||||
sectionType == RecommendedSectionType.POPULAR_COMMUNITY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1098,6 +1173,25 @@ private class FakeCheerCreatorSnapshotFallbackPort(
|
||||
}
|
||||
}
|
||||
|
||||
private class FakePopularCommunitySnapshotFallbackPort(
|
||||
private val snapshots: List<RecommendationSnapshotRecord>
|
||||
) : PopularCommunitySnapshotFallbackPort {
|
||||
var offset: Long? = null
|
||||
var limit: Int? = null
|
||||
var nowUtc: LocalDateTime? = null
|
||||
|
||||
override fun refreshPopularCommunityIfMissing(
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
nowUtc: LocalDateTime
|
||||
): List<RecommendationSnapshotRecord> {
|
||||
this.offset = offset
|
||||
this.limit = limit
|
||||
this.nowUtc = nowUtc
|
||||
return snapshots
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeAiCharacterSnapshotFallbackPort(
|
||||
private val snapshots: List<RecommendationSnapshotRecord>
|
||||
) : AiCharacterSnapshotFallbackPort {
|
||||
|
||||
Reference in New Issue
Block a user