feat(recommend): 홈 추천 조회 서비스를 추가한다

This commit is contained in:
2026-05-31 16:32:43 +09:00
parent 3cd4e689dc
commit 14822f351b
3 changed files with 604 additions and 2 deletions

View File

@@ -1,8 +1,91 @@
package kr.co.vividnext.sodalive.v2.recommend.application package kr.co.vividnext.sodalive.v2.recommend.application
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeAiCharacterRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeBannerRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeCheerCreatorRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeFirstAudioContentRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeLiveRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomePopularCommunityRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeRecommendationQueryPort
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentDebutCreatorRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentlyActiveCreatorRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotPort
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotRecord
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
@Service
@Transactional(readOnly = true)
class HomeRecommendationQueryService(
private val queryPort: HomeRecommendationQueryPort,
private val snapshotPort: RecommendationSnapshotPort
) {
fun findLiveRecommendations(limit: Int = DEFAULT_LIVE_LIMIT): List<HomeLiveRecommendationRecord> {
return queryPort.findLiveRecommendations(limit)
}
fun findHomeBanners(limit: Int = DEFAULT_BANNER_LIMIT): List<HomeBannerRecommendationRecord> {
return queryPort.findHomeBanners(limit)
}
fun findRecentlyActiveCreators(limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT): List<RecentlyActiveCreatorRecord> {
return queryPort.findRecentlyActiveCreators(limit)
}
fun findRecentDebutCreators(
now: LocalDateTime,
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT
): List<RecentDebutCreatorRecord> {
return queryPort.findRecentDebutCreators(now, limit)
}
fun findFirstAudioContents(
now: LocalDateTime,
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT
): List<HomeFirstAudioContentRecord> {
return queryPort.findFirstAudioContents(now, limit)
}
fun findAiCharacterRecommendations(
limit: Int = DEFAULT_AI_CHARACTER_LIMIT
): List<HomeAiCharacterRecommendationRecord> {
val snapshots = latestSnapshots(RecommendedSectionType.AI_CHARACTER, limit)
val detailsById = queryPort.findAiCharacterRecommendationDetails(snapshots.map { it.targetId })
.associateBy { it.characterId }
return snapshots.mapNotNull { detailsById[it.targetId] }
}
fun findCheerCreatorRecommendations(
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT
): List<HomeCheerCreatorRecommendationRecord> {
val snapshots = latestSnapshots(RecommendedSectionType.CHEER_CREATOR, limit)
val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId })
.associateBy { it.creatorId }
return snapshots.mapNotNull { detailsById[it.targetId] }
}
fun findPopularCommunityRecommendations(
limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT,
includeAdultCommunities: Boolean = false
): List<HomePopularCommunityRecommendationRecord> {
val snapshots = latestSnapshots(RecommendedSectionType.POPULAR_COMMUNITY, POPULAR_COMMUNITY_CANDIDATE_LIMIT)
val detailsById = queryPort.findPopularCommunityRecommendationDetails(
snapshots.map { it.targetId },
includeAdultCommunities
)
.associateBy { it.communityId }
val selectedCreatorIds = mutableSetOf<Long>()
return snapshots.mapNotNull { snapshot ->
detailsById[snapshot.targetId]?.takeIf { selectedCreatorIds.add(it.creatorId) }
}.take(limit)
}
class HomeRecommendationQueryService {
fun resolveAudioContentActivityType(theme: String): RecommendedActivityType { fun resolveAudioContentActivityType(theme: String): RecommendedActivityType {
return if (theme == LIVE_REPLAY_THEME) { return if (theme == LIVE_REPLAY_THEME) {
RecommendedActivityType.LIVE_REPLAY RecommendedActivityType.LIVE_REPLAY
@@ -11,7 +94,20 @@ class HomeRecommendationQueryService {
} }
} }
private fun latestSnapshots(sectionType: RecommendedSectionType, limit: Int): List<RecommendationSnapshotRecord> {
return snapshotPort.findLatestSnapshots(sectionType).take(limit)
}
companion object { companion object {
private const val DEFAULT_LIVE_LIMIT = 20
private const val DEFAULT_BANNER_LIMIT = 20
private const val DEFAULT_ACTIVE_CREATOR_LIMIT = 10
private const val DEFAULT_RECENT_DEBUT_CREATOR_LIMIT = 10
private const val DEFAULT_FIRST_AUDIO_CONTENT_LIMIT = 10
private const val DEFAULT_AI_CHARACTER_LIMIT = 10
private const val DEFAULT_CHEER_CREATOR_LIMIT = 8
private const val DEFAULT_POPULAR_COMMUNITY_LIMIT = 10
private const val POPULAR_COMMUNITY_CANDIDATE_LIMIT = 20
private const val LIVE_REPLAY_THEME = "다시듣기" private const val LIVE_REPLAY_THEME = "다시듣기"
} }
} }

View File

@@ -1,8 +1,19 @@
package kr.co.vividnext.sodalive.v2.recommend.port.out package kr.co.vividnext.sodalive.v2.recommend.port.out
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
import java.time.LocalDateTime import java.time.LocalDateTime
interface HomeRecommendationQueryPort { interface HomeRecommendationQueryPort {
fun findLiveRecommendations(limit: Int): List<HomeLiveRecommendationRecord>
fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord>
fun findRecentlyActiveCreators(limit: Int): List<RecentlyActiveCreatorRecord>
fun findRecentDebutCreators(now: LocalDateTime, limit: Int): List<RecentDebutCreatorRecord>
fun findFirstAudioContents(now: LocalDateTime, limit: Int): List<HomeFirstAudioContentRecord>
fun findAiCharacterSnapshots( fun findAiCharacterSnapshots(
windowStart: LocalDateTime, windowStart: LocalDateTime,
snapshotAt: LocalDateTime, snapshotAt: LocalDateTime,
@@ -20,4 +31,91 @@ interface HomeRecommendationQueryPort {
snapshotAt: LocalDateTime, snapshotAt: LocalDateTime,
limit: Int limit: Int
): List<RecommendationSnapshotRecord> ): List<RecommendationSnapshotRecord>
fun findAiCharacterRecommendationDetails(characterIds: List<Long>): List<HomeAiCharacterRecommendationRecord>
fun findCheerCreatorRecommendationDetails(creatorIds: List<Long>): List<HomeCheerCreatorRecommendationRecord>
fun findPopularCommunityRecommendationDetails(
communityIds: List<Long>,
includeAdultCommunities: Boolean
): List<HomePopularCommunityRecommendationRecord>
} }
data class HomeLiveRecommendationRecord(
val liveRoomId: Long,
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImage: String?,
val title: String,
val coverImage: String?,
val beginDateTime: LocalDateTime,
val channelName: String
)
data class HomeBannerRecommendationRecord(
val bannerId: Long,
val type: String,
val thumbnailImage: String,
val eventId: Long?,
val creatorId: Long?,
val seriesId: Long?,
val link: String?,
val orders: Int,
val randomTieBreaker: Double
)
data class RecentlyActiveCreatorRecord(
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImage: String?,
val activityType: RecommendedActivityType,
val activityAt: LocalDateTime,
val targetId: Long?
)
data class RecentDebutCreatorRecord(
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImage: String?,
val debutAt: LocalDateTime,
val score: Double,
val randomTieBreaker: Double
)
data class HomeFirstAudioContentRecord(
val contentId: Long,
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImage: String?,
val title: String,
val coverImage: String?,
val releaseDate: LocalDateTime,
val recencyScore: Int,
val randomTieBreaker: Double
)
data class HomeAiCharacterRecommendationRecord(
val characterId: Long,
val name: String,
val description: String,
val totalChatCount: Long,
val originalWorkTitle: String?
)
data class HomeCheerCreatorRecommendationRecord(
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImage: String?
)
data class HomePopularCommunityRecommendationRecord(
val communityId: Long,
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImage: String?,
val content: String,
val createdAt: LocalDateTime,
val likeCount: Long,
val commentCount: Long
)

View File

@@ -2,12 +2,26 @@ package kr.co.vividnext.sodalive.v2.recommend.application
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeAiCharacterRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeBannerRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeCheerCreatorRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeFirstAudioContentRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeLiveRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomePopularCommunityRecommendationRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeRecommendationQueryPort
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentDebutCreatorRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentlyActiveCreatorRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotPort
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotRecord
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 java.time.LocalDateTime
class HomeRecommendationQueryServiceTest { class HomeRecommendationQueryServiceTest {
private val service = HomeRecommendationQueryService() private val port = FakeHomeRecommendationQueryPort()
private val snapshotPort = FakeHomeRecommendationSnapshotPort()
private val service = HomeRecommendationQueryService(port, snapshotPort)
@Test @Test
@DisplayName("다시듣기 테마 콘텐츠는 AUDIO가 아니라 LIVE_REPLAY 활동으로 분류한다") @DisplayName("다시듣기 테마 콘텐츠는 AUDIO가 아니라 LIVE_REPLAY 활동으로 분류한다")
@@ -47,4 +61,398 @@ class HomeRecommendationQueryServiceTest {
assertEquals("CHEER_CREATOR", RecommendedSectionType.CHEER_CREATOR.code) assertEquals("CHEER_CREATOR", RecommendedSectionType.CHEER_CREATOR.code)
assertEquals("POPULAR_COMMUNITY", RecommendedSectionType.POPULAR_COMMUNITY.code) assertEquals("POPULAR_COMMUNITY", RecommendedSectionType.POPULAR_COMMUNITY.code)
} }
@Test
@DisplayName("홈 라이브 추천은 기본 20개를 최신순 조회 포트에 위임한다")
fun shouldFindLatestLiveRecommendationsWithDefaultLimit() {
val recommendations = service.findLiveRecommendations()
assertEquals(20, port.liveLimit)
assertEquals(port.liveRecommendations, recommendations)
}
@Test
@DisplayName("홈 배너 추천은 기본 20개를 활성 배너 조회 포트에 위임한다")
fun shouldFindHomeBannersWithDefaultLimit() {
val banners = service.findHomeBanners()
assertEquals(20, port.bannerLimit)
assertEquals(port.banners, banners)
}
@Test
@DisplayName("최근 활동 크리에이터는 기본 10개를 최신 활동 조회 포트에 위임한다")
fun shouldFindRecentlyActiveCreatorsWithDefaultLimit() {
val creators = service.findRecentlyActiveCreators()
assertEquals(10, port.activeCreatorLimit)
assertEquals(port.activeCreators, creators)
}
@Test
@DisplayName("최근 데뷔 크리에이터는 기본 10개를 기준 시각과 함께 조회 포트에 위임한다")
fun shouldFindRecentDebutCreatorsWithDefaultLimitAndNow() {
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
val creators = service.findRecentDebutCreators(now)
assertEquals(now, port.recentDebutNow)
assertEquals(10, port.recentDebutLimit)
assertEquals(port.recentDebutCreators, creators)
}
@Test
@DisplayName("첫 오디오 콘텐츠는 기본 10개를 기준 시각과 함께 조회 포트에 위임한다")
fun shouldFindFirstAudioContentsWithDefaultLimitAndNow() {
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
val contents = service.findFirstAudioContents(now)
assertEquals(now, port.firstAudioNow)
assertEquals(10, port.firstAudioLimit)
assertEquals(port.firstAudioContents, contents)
}
@Test
@DisplayName("AI 캐릭터 추천은 최신 스냅샷 10개를 기준으로 순서를 유지해 상세를 조립한다")
fun shouldFindAiCharactersFromLatestSnapshotsWithLimitAndDetails() {
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
val latestSnapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
snapshotPort.replaceSnapshots(
RecommendedSectionType.AI_CHARACTER,
oldSnapshotAt,
listOf(snapshot(RecommendedSectionType.AI_CHARACTER, 99L, 999.0, oldSnapshotAt))
)
snapshotPort.replaceSnapshots(
RecommendedSectionType.AI_CHARACTER,
latestSnapshotAt,
(1L..12L).map { targetId ->
snapshot(RecommendedSectionType.AI_CHARACTER, targetId, 100.0 - targetId, latestSnapshotAt)
}
)
port.aiCharacterDetails = listOf(
HomeAiCharacterRecommendationRecord(
characterId = 1L,
name = "character-1",
description = "description-1",
totalChatCount = 3L,
originalWorkTitle = "original-work"
),
HomeAiCharacterRecommendationRecord(
characterId = 2L,
name = "character-2",
description = "description-2",
totalChatCount = 0L,
originalWorkTitle = null
)
)
val characters = service.findAiCharacterRecommendations()
assertEquals((1L..10L).toList(), port.aiCharacterDetailIds)
assertEquals(listOf(1L, 2L), characters.map { it.characterId })
assertEquals("original-work", characters.first().originalWorkTitle)
assertEquals(null, characters.last().originalWorkTitle)
}
@Test
@DisplayName("최근 응원 크리에이터 추천은 최신 스냅샷 8명을 기준으로 닉네임과 프로필을 조립한다")
fun shouldFindCheerCreatorsFromLatestSnapshotsWithLimitAndDetails() {
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
snapshotPort.replaceSnapshots(
RecommendedSectionType.CHEER_CREATOR,
snapshotAt,
(1L..9L).map { targetId ->
snapshot(RecommendedSectionType.CHEER_CREATOR, targetId, 100.0 - targetId, snapshotAt)
}
)
port.cheerCreatorDetails = listOf(
HomeCheerCreatorRecommendationRecord(
creatorId = 1L,
creatorNickname = "creator-1",
creatorProfileImage = "profile-1.png"
),
HomeCheerCreatorRecommendationRecord(
creatorId = 2L,
creatorNickname = "creator-2",
creatorProfileImage = null
)
)
val creators = service.findCheerCreatorRecommendations()
assertEquals((1L..8L).toList(), port.cheerCreatorDetailIds)
assertEquals(listOf(1L, 2L), creators.map { it.creatorId })
assertEquals(listOf("creator-1", "creator-2"), creators.map { it.creatorNickname })
}
@Test
@DisplayName("인기 커뮤니티 추천은 최신 스냅샷 10개를 기준으로 크리에이터 중복을 제거하고 상세를 조립한다")
fun shouldFindPopularCommunitiesFromLatestSnapshotsWithLimitDetailsAndCreatorUniqueness() {
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
snapshotPort.replaceSnapshots(
RecommendedSectionType.POPULAR_COMMUNITY,
snapshotAt,
(1L..11L).map { targetId ->
snapshot(RecommendedSectionType.POPULAR_COMMUNITY, targetId, 100.0 - targetId, snapshotAt)
}
)
port.popularCommunityDetails = listOf(
HomePopularCommunityRecommendationRecord(
communityId = 1L,
creatorId = 10L,
creatorNickname = "creator-10",
creatorProfileImage = "profile-10.png",
content = "content-1",
createdAt = LocalDateTime.of(2026, 5, 29, 1, 0),
likeCount = 3L,
commentCount = 2L
),
HomePopularCommunityRecommendationRecord(
communityId = 2L,
creatorId = 10L,
creatorNickname = "creator-10",
creatorProfileImage = "profile-10.png",
content = "content-2",
createdAt = LocalDateTime.of(2026, 5, 29, 2, 0),
likeCount = 1L,
commentCount = 1L
),
HomePopularCommunityRecommendationRecord(
communityId = 3L,
creatorId = 11L,
creatorNickname = "creator-11",
creatorProfileImage = null,
content = "content-3",
createdAt = LocalDateTime.of(2026, 5, 29, 3, 0),
likeCount = 0L,
commentCount = 0L
)
)
val communities = service.findPopularCommunityRecommendations(includeAdultCommunities = true)
assertEquals((1L..11L).toList(), port.popularCommunityDetailIds)
assertEquals(true, port.popularCommunityIncludeAdultCommunities)
assertEquals(listOf(1L, 3L), communities.map { it.communityId })
assertEquals(listOf(10L, 11L), communities.map { it.creatorId })
assertEquals(LocalDateTime.of(2026, 5, 29, 1, 0), communities.first().createdAt)
}
@Test
@DisplayName("인기 커뮤니티 추천은 상위 스냅샷 중복 제거 후 뒤 후보로 기본 10개를 채운다")
fun shouldBackfillPopularCommunitiesAfterRemovingDuplicateCreators() {
val snapshotAt = LocalDateTime.of(2026, 5, 29, 23, 59, 59)
snapshotPort.replaceSnapshots(
RecommendedSectionType.POPULAR_COMMUNITY,
snapshotAt,
(1L..20L).map { targetId ->
snapshot(RecommendedSectionType.POPULAR_COMMUNITY, targetId, 100.0 - targetId, snapshotAt)
}
)
port.popularCommunityDetails = (1L..20L).map { communityId ->
HomePopularCommunityRecommendationRecord(
communityId = communityId,
creatorId = if (communityId <= 10L) 1L else communityId,
creatorNickname = "creator-$communityId",
creatorProfileImage = null,
content = "content-$communityId",
createdAt = LocalDateTime.of(2026, 5, 29, 1, 0).plusMinutes(communityId),
likeCount = 0L,
commentCount = 0L
)
}
val communities = service.findPopularCommunityRecommendations()
assertEquals(20, port.popularCommunityDetailIds.size)
assertEquals(10, communities.size)
assertEquals(listOf(1L) + (11L..19L).toList(), communities.map { it.communityId })
assertEquals(communities.size, communities.map { it.creatorId }.toSet().size)
}
@Test
@DisplayName("최신 스냅샷이 없으면 AI 캐릭터/최근 응원/인기 커뮤니티 추천은 빈 배열을 반환한다")
fun shouldReturnEmptyListWhenLatestSnapshotsDoNotExist() {
assertEquals(emptyList<HomeAiCharacterRecommendationRecord>(), service.findAiCharacterRecommendations())
assertEquals(emptyList<HomeCheerCreatorRecommendationRecord>(), service.findCheerCreatorRecommendations())
assertEquals(emptyList<HomePopularCommunityRecommendationRecord>(), service.findPopularCommunityRecommendations())
}
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
var liveLimit: Int? = null
var bannerLimit: Int? = null
var activeCreatorLimit: Int? = null
var recentDebutNow: LocalDateTime? = null
var recentDebutLimit: Int? = null
var firstAudioNow: LocalDateTime? = null
var firstAudioLimit: Int? = null
var aiCharacterDetailIds: List<Long> = emptyList()
var cheerCreatorDetailIds: List<Long> = emptyList()
var popularCommunityDetailIds: List<Long> = emptyList()
var popularCommunityIncludeAdultCommunities: Boolean? = null
val liveRecommendations = listOf(
HomeLiveRecommendationRecord(
liveRoomId = 1L,
creatorId = 10L,
creatorNickname = "creator",
creatorProfileImage = "profile.png",
title = "live",
coverImage = "cover.png",
beginDateTime = LocalDateTime.of(2026, 5, 31, 10, 0),
channelName = "channel"
)
)
val banners = listOf(
HomeBannerRecommendationRecord(
bannerId = 2L,
type = "LINK",
thumbnailImage = "banner.png",
eventId = null,
creatorId = null,
seriesId = null,
link = "https://example.com",
orders = 1,
randomTieBreaker = 0.1
)
)
val activeCreators = listOf(
RecentlyActiveCreatorRecord(
creatorId = 10L,
creatorNickname = "creator",
creatorProfileImage = "profile.png",
activityType = RecommendedActivityType.LIVE,
activityAt = LocalDateTime.of(2026, 5, 31, 10, 0),
targetId = null
)
)
val recentDebutCreators = listOf(
RecentDebutCreatorRecord(
creatorId = 11L,
creatorNickname = "debut-creator",
creatorProfileImage = "debut-profile.png",
debutAt = LocalDateTime.of(2026, 5, 20, 10, 0),
score = 1.2,
randomTieBreaker = 0.2
)
)
val firstAudioContents = listOf(
HomeFirstAudioContentRecord(
contentId = 21L,
creatorId = 11L,
creatorNickname = "debut-creator",
creatorProfileImage = "debut-profile.png",
title = "first-audio",
coverImage = "first-audio.png",
releaseDate = LocalDateTime.of(2026, 5, 30, 10, 0),
recencyScore = 100,
randomTieBreaker = 0.3
)
)
var aiCharacterDetails: List<HomeAiCharacterRecommendationRecord> = emptyList()
var cheerCreatorDetails: List<HomeCheerCreatorRecommendationRecord> = emptyList()
var popularCommunityDetails: List<HomePopularCommunityRecommendationRecord> = emptyList()
override fun findLiveRecommendations(limit: Int): List<HomeLiveRecommendationRecord> {
liveLimit = limit
return liveRecommendations
}
override fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord> {
bannerLimit = limit
return banners
}
override fun findRecentlyActiveCreators(limit: Int): List<RecentlyActiveCreatorRecord> {
activeCreatorLimit = limit
return activeCreators
}
override fun findRecentDebutCreators(now: LocalDateTime, limit: Int): List<RecentDebutCreatorRecord> {
recentDebutNow = now
recentDebutLimit = limit
return recentDebutCreators
}
override fun findFirstAudioContents(now: LocalDateTime, limit: Int): List<HomeFirstAudioContentRecord> {
firstAudioNow = now
firstAudioLimit = limit
return firstAudioContents
}
override fun findAiCharacterSnapshots(
windowStart: LocalDateTime,
snapshotAt: LocalDateTime,
limit: Int
): List<RecommendationSnapshotRecord> = emptyList()
override fun findCheerCreatorSnapshots(
windowStart: LocalDateTime,
snapshotAt: LocalDateTime,
limit: Int
): List<RecommendationSnapshotRecord> = emptyList()
override fun findPopularCommunitySnapshots(
windowStart: LocalDateTime,
snapshotAt: LocalDateTime,
limit: Int
): List<RecommendationSnapshotRecord> = emptyList()
override fun findAiCharacterRecommendationDetails(characterIds: List<Long>): List<HomeAiCharacterRecommendationRecord> {
aiCharacterDetailIds = characterIds
return aiCharacterDetails
}
override fun findCheerCreatorRecommendationDetails(creatorIds: List<Long>): List<HomeCheerCreatorRecommendationRecord> {
cheerCreatorDetailIds = creatorIds
return cheerCreatorDetails
}
override fun findPopularCommunityRecommendationDetails(
communityIds: List<Long>,
includeAdultCommunities: Boolean
): List<HomePopularCommunityRecommendationRecord> {
popularCommunityDetailIds = communityIds
popularCommunityIncludeAdultCommunities = includeAdultCommunities
return popularCommunityDetails
}
}
}
private class FakeHomeRecommendationSnapshotPort : RecommendationSnapshotPort {
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
override fun findLatestSnapshots(sectionType: RecommendedSectionType): List<RecommendationSnapshotRecord> {
val latestSnapshotAt = snapshots
.filter { it.sectionType == sectionType }
.maxOfOrNull { it.snapshotAt }
return snapshots
.filter { it.sectionType == sectionType && it.snapshotAt == latestSnapshotAt }
.sortedWith(compareByDescending<RecommendationSnapshotRecord> { it.score }.thenBy { it.randomTieBreaker })
}
override fun replaceSnapshots(
sectionType: RecommendedSectionType,
snapshotAt: LocalDateTime,
newSnapshots: List<RecommendationSnapshotRecord>
) {
snapshots.removeIf { it.sectionType == sectionType && it.snapshotAt == snapshotAt }
snapshots.addAll(newSnapshots)
}
}
private fun snapshot(
sectionType: RecommendedSectionType,
targetId: Long,
score: Double,
snapshotAt: LocalDateTime
): RecommendationSnapshotRecord {
return RecommendationSnapshotRecord(
sectionType = sectionType,
targetId = targetId,
score = score,
snapshotAt = snapshotAt,
randomTieBreaker = targetId.toDouble() / 100
)
} }