feat(recommend): 홈 추천 전체보기 페이징 조회를 추가한다

This commit is contained in:
2026-06-01 13:54:40 +09:00
parent f77bd7b8e2
commit 1f3a38a404
5 changed files with 111 additions and 29 deletions

View File

@@ -13,7 +13,6 @@ import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeRecommendationQueryPor
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
@@ -24,36 +23,48 @@ class HomeRecommendationQueryService(
private val queryPort: HomeRecommendationQueryPort,
private val snapshotPort: RecommendationSnapshotPort
) {
fun findLiveRecommendations(limit: Int = DEFAULT_LIVE_LIMIT): List<HomeLiveRecommendationRecord> {
return queryPort.findLiveRecommendations(limit)
fun findLiveRecommendations(
offset: Int = 0,
limit: Int = DEFAULT_LIVE_LIMIT,
includeAdultLives: Boolean = false
): List<HomeLiveRecommendationRecord> {
return queryPort.findLiveRecommendations(offset, limit, includeAdultLives)
}
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 findRecentlyActiveCreators(
limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT,
includeAdultActivities: Boolean = false
): List<RecentlyActiveCreatorRecord> {
return queryPort.findRecentlyActiveCreators(limit, includeAdultActivities)
}
fun findRecentDebutCreators(
now: LocalDateTime,
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT
offset: Int = 0,
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT,
includeAdultContents: Boolean = false
): List<RecentDebutCreatorRecord> {
return queryPort.findRecentDebutCreators(now, limit)
return queryPort.findRecentDebutCreators(now, offset, limit, includeAdultContents)
}
fun findFirstAudioContents(
now: LocalDateTime,
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT
offset: Int = 0,
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT,
includeAdultContents: Boolean = false
): List<HomeFirstAudioContentRecord> {
return queryPort.findFirstAudioContents(now, limit)
return queryPort.findFirstAudioContents(now, offset, limit, includeAdultContents)
}
fun findAiCharacterRecommendations(
offset: Int = 0,
limit: Int = DEFAULT_AI_CHARACTER_LIMIT
): List<HomeAiCharacterRecommendationRecord> {
val snapshots = latestSnapshots(RecommendedSectionType.AI_CHARACTER, limit)
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.AI_CHARACTER, offset, limit)
val detailsById = queryPort.findAiCharacterRecommendationDetails(snapshots.map { it.targetId })
.associateBy { it.characterId }
@@ -63,7 +74,7 @@ class HomeRecommendationQueryService(
fun findCheerCreatorRecommendations(
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT
): List<HomeCheerCreatorRecommendationRecord> {
val snapshots = latestSnapshots(RecommendedSectionType.CHEER_CREATOR, limit)
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.CHEER_CREATOR).take(limit)
val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId })
.associateBy { it.creatorId }
@@ -74,7 +85,8 @@ class HomeRecommendationQueryService(
limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT,
includeAdultCommunities: Boolean = false
): List<HomePopularCommunityRecommendationRecord> {
val snapshots = latestSnapshots(RecommendedSectionType.POPULAR_COMMUNITY, POPULAR_COMMUNITY_CANDIDATE_LIMIT)
val snapshots = snapshotPort.findLatestSnapshots(RecommendedSectionType.POPULAR_COMMUNITY)
.take(POPULAR_COMMUNITY_CANDIDATE_LIMIT)
val detailsById = queryPort.findPopularCommunityRecommendationDetails(
snapshots.map { it.targetId },
includeAdultCommunities
@@ -112,10 +124,6 @@ class HomeRecommendationQueryService(
}
}
private fun latestSnapshots(sectionType: RecommendedSectionType, limit: Int): List<RecommendationSnapshotRecord> {
return snapshotPort.findLatestSnapshots(sectionType).take(limit)
}
companion object {
private const val DEFAULT_LIVE_LIMIT = 20
private const val DEFAULT_BANNER_LIMIT = 20

View File

@@ -4,15 +4,29 @@ import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
import java.time.LocalDateTime
interface HomeRecommendationQueryPort {
fun findLiveRecommendations(limit: Int): List<HomeLiveRecommendationRecord>
fun findLiveRecommendations(
offset: Int = 0,
limit: Int,
includeAdultLives: Boolean = false
): List<HomeLiveRecommendationRecord>
fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord>
fun findRecentlyActiveCreators(limit: Int): List<RecentlyActiveCreatorRecord>
fun findRecentlyActiveCreators(limit: Int, includeAdultActivities: Boolean = false): List<RecentlyActiveCreatorRecord>
fun findRecentDebutCreators(now: LocalDateTime, limit: Int): List<RecentDebutCreatorRecord>
fun findRecentDebutCreators(
now: LocalDateTime,
offset: Int = 0,
limit: Int,
includeAdultContents: Boolean = false
): List<RecentDebutCreatorRecord>
fun findFirstAudioContents(now: LocalDateTime, limit: Int): List<HomeFirstAudioContentRecord>
fun findFirstAudioContents(
now: LocalDateTime,
offset: Int = 0,
limit: Int,
includeAdultContents: Boolean = false
): List<HomeFirstAudioContentRecord>
fun findAiCharacterSnapshots(
windowStart: LocalDateTime,

View File

@@ -4,7 +4,11 @@ import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
import java.time.LocalDateTime
interface RecommendationSnapshotPort {
fun findLatestSnapshots(sectionType: RecommendedSectionType): List<RecommendationSnapshotRecord>
fun findLatestSnapshots(
sectionType: RecommendedSectionType,
offset: Int = 0,
limit: Int = Int.MAX_VALUE
): List<RecommendationSnapshotRecord>
fun replaceSnapshots(
sectionType: RecommendedSectionType,

View File

@@ -88,6 +88,17 @@ class HomeRecommendationQueryServiceTest {
val creators = service.findRecentlyActiveCreators()
assertEquals(10, port.activeCreatorLimit)
assertEquals(false, port.activeCreatorIncludeAdultActivities)
assertEquals(port.activeCreators, creators)
}
@Test
@DisplayName("최근 활동 크리에이터는 성인 활동 노출 여부를 조회 포트에 위임한다")
fun shouldFindRecentlyActiveCreatorsWithAdultVisibilityPolicy() {
val creators = service.findRecentlyActiveCreators(limit = 8, includeAdultActivities = true)
assertEquals(8, port.activeCreatorLimit)
assertEquals(true, port.activeCreatorIncludeAdultActivities)
assertEquals(port.activeCreators, creators)
}
@@ -389,12 +400,19 @@ class HomeRecommendationQueryServiceTest {
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
var liveLimit: Int? = null
var liveOffset: Int? = null
var liveIncludeAdultLives: Boolean? = null
var bannerLimit: Int? = null
var activeCreatorLimit: Int? = null
var activeCreatorIncludeAdultActivities: Boolean? = null
var recentDebutNow: LocalDateTime? = null
var recentDebutLimit: Int? = null
var recentDebutOffset: Int? = null
var recentDebutIncludeAdultContents: Boolean? = null
var firstAudioNow: LocalDateTime? = null
var firstAudioLimit: Int? = null
var firstAudioOffset: Int? = null
var firstAudioIncludeAdultContents: Boolean? = null
var aiCharacterDetailIds: List<Long> = emptyList()
var cheerCreatorDetailIds: List<Long> = emptyList()
var popularCommunityDetailIds: List<Long> = emptyList()
@@ -466,8 +484,14 @@ class HomeRecommendationQueryServiceTest {
var popularCommunityDetails: List<HomePopularCommunityRecommendationRecord> = emptyList()
var genreCreatorRecommendations: List<HomeGenreCreatorRecommendationGroup> = emptyList()
override fun findLiveRecommendations(limit: Int): List<HomeLiveRecommendationRecord> {
override fun findLiveRecommendations(
offset: Int,
limit: Int,
includeAdultLives: Boolean
): List<HomeLiveRecommendationRecord> {
liveOffset = offset
liveLimit = limit
liveIncludeAdultLives = includeAdultLives
return liveRecommendations
}
@@ -476,20 +500,38 @@ class HomeRecommendationQueryServiceTest {
return banners
}
override fun findRecentlyActiveCreators(limit: Int): List<RecentlyActiveCreatorRecord> {
override fun findRecentlyActiveCreators(
limit: Int,
includeAdultActivities: Boolean
): List<RecentlyActiveCreatorRecord> {
activeCreatorLimit = limit
activeCreatorIncludeAdultActivities = includeAdultActivities
return activeCreators
}
override fun findRecentDebutCreators(now: LocalDateTime, limit: Int): List<RecentDebutCreatorRecord> {
override fun findRecentDebutCreators(
now: LocalDateTime,
offset: Int,
limit: Int,
includeAdultContents: Boolean
): List<RecentDebutCreatorRecord> {
recentDebutNow = now
recentDebutOffset = offset
recentDebutLimit = limit
recentDebutIncludeAdultContents = includeAdultContents
return recentDebutCreators
}
override fun findFirstAudioContents(now: LocalDateTime, limit: Int): List<HomeFirstAudioContentRecord> {
override fun findFirstAudioContents(
now: LocalDateTime,
offset: Int,
limit: Int,
includeAdultContents: Boolean
): List<HomeFirstAudioContentRecord> {
firstAudioNow = now
firstAudioOffset = offset
firstAudioLimit = limit
firstAudioIncludeAdultContents = includeAdultContents
return firstAudioContents
}
@@ -548,14 +590,21 @@ class HomeRecommendationQueryServiceTest {
private class FakeHomeRecommendationSnapshotPort : RecommendationSnapshotPort {
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
override fun findLatestSnapshots(sectionType: RecommendedSectionType): List<RecommendationSnapshotRecord> {
override fun findLatestSnapshots(
sectionType: RecommendedSectionType,
offset: Int,
limit: Int
): List<RecommendationSnapshotRecord> {
val latestSnapshotAt = snapshots
.filter { it.sectionType == sectionType }
.maxOfOrNull { it.snapshotAt }
return snapshots
val all = snapshots
.filter { it.sectionType == sectionType && it.snapshotAt == latestSnapshotAt }
.sortedWith(compareByDescending<RecommendationSnapshotRecord> { it.score }.thenBy { it.randomTieBreaker })
if (offset == 0 && limit == Int.MAX_VALUE) return all
return all.drop(offset).take(limit)
}
override fun replaceSnapshots(

View File

@@ -176,14 +176,21 @@ class RecommendationSnapshotRefreshServiceTest {
private class FakeRecommendationSnapshotPort : RecommendationSnapshotPort {
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
override fun findLatestSnapshots(sectionType: RecommendedSectionType): List<RecommendationSnapshotRecord> {
override fun findLatestSnapshots(
sectionType: RecommendedSectionType,
offset: Int,
limit: Int
): List<RecommendationSnapshotRecord> {
val latestSnapshotAt = snapshots
.filter { it.sectionType == sectionType }
.maxOfOrNull { it.snapshotAt }
return snapshots
val all = snapshots
.filter { it.sectionType == sectionType && it.snapshotAt == latestSnapshotAt }
.sortedWith(compareByDescending<RecommendationSnapshotRecord> { it.score }.thenBy { it.randomTieBreaker })
if (offset == 0 && limit == Int.MAX_VALUE) return all
return all.drop(offset).take(limit)
}
override fun replaceSnapshots(