feat(recommend): 홈 추천 전체보기 페이징 조회를 추가한다
This commit is contained in:
@@ -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.RecentDebutCreatorRecord
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentlyActiveCreatorRecord
|
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.RecommendationSnapshotPort
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotRecord
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -24,36 +23,48 @@ class HomeRecommendationQueryService(
|
|||||||
private val queryPort: HomeRecommendationQueryPort,
|
private val queryPort: HomeRecommendationQueryPort,
|
||||||
private val snapshotPort: RecommendationSnapshotPort
|
private val snapshotPort: RecommendationSnapshotPort
|
||||||
) {
|
) {
|
||||||
fun findLiveRecommendations(limit: Int = DEFAULT_LIVE_LIMIT): List<HomeLiveRecommendationRecord> {
|
fun findLiveRecommendations(
|
||||||
return queryPort.findLiveRecommendations(limit)
|
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> {
|
fun findHomeBanners(limit: Int = DEFAULT_BANNER_LIMIT): List<HomeBannerRecommendationRecord> {
|
||||||
return queryPort.findHomeBanners(limit)
|
return queryPort.findHomeBanners(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findRecentlyActiveCreators(limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT): List<RecentlyActiveCreatorRecord> {
|
fun findRecentlyActiveCreators(
|
||||||
return queryPort.findRecentlyActiveCreators(limit)
|
limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT,
|
||||||
|
includeAdultActivities: Boolean = false
|
||||||
|
): List<RecentlyActiveCreatorRecord> {
|
||||||
|
return queryPort.findRecentlyActiveCreators(limit, includeAdultActivities)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findRecentDebutCreators(
|
fun findRecentDebutCreators(
|
||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT
|
offset: Int = 0,
|
||||||
|
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT,
|
||||||
|
includeAdultContents: Boolean = false
|
||||||
): List<RecentDebutCreatorRecord> {
|
): List<RecentDebutCreatorRecord> {
|
||||||
return queryPort.findRecentDebutCreators(now, limit)
|
return queryPort.findRecentDebutCreators(now, offset, limit, includeAdultContents)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findFirstAudioContents(
|
fun findFirstAudioContents(
|
||||||
now: LocalDateTime,
|
now: LocalDateTime,
|
||||||
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT
|
offset: Int = 0,
|
||||||
|
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT,
|
||||||
|
includeAdultContents: Boolean = false
|
||||||
): List<HomeFirstAudioContentRecord> {
|
): List<HomeFirstAudioContentRecord> {
|
||||||
return queryPort.findFirstAudioContents(now, limit)
|
return queryPort.findFirstAudioContents(now, offset, limit, includeAdultContents)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findAiCharacterRecommendations(
|
fun findAiCharacterRecommendations(
|
||||||
|
offset: Int = 0,
|
||||||
limit: Int = DEFAULT_AI_CHARACTER_LIMIT
|
limit: Int = DEFAULT_AI_CHARACTER_LIMIT
|
||||||
): List<HomeAiCharacterRecommendationRecord> {
|
): 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 })
|
val detailsById = queryPort.findAiCharacterRecommendationDetails(snapshots.map { it.targetId })
|
||||||
.associateBy { it.characterId }
|
.associateBy { it.characterId }
|
||||||
|
|
||||||
@@ -63,7 +74,7 @@ class HomeRecommendationQueryService(
|
|||||||
fun findCheerCreatorRecommendations(
|
fun findCheerCreatorRecommendations(
|
||||||
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT
|
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT
|
||||||
): List<HomeCheerCreatorRecommendationRecord> {
|
): 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 })
|
val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId })
|
||||||
.associateBy { it.creatorId }
|
.associateBy { it.creatorId }
|
||||||
|
|
||||||
@@ -74,7 +85,8 @@ class HomeRecommendationQueryService(
|
|||||||
limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT,
|
limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT,
|
||||||
includeAdultCommunities: Boolean = false
|
includeAdultCommunities: Boolean = false
|
||||||
): List<HomePopularCommunityRecommendationRecord> {
|
): 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(
|
val detailsById = queryPort.findPopularCommunityRecommendationDetails(
|
||||||
snapshots.map { it.targetId },
|
snapshots.map { it.targetId },
|
||||||
includeAdultCommunities
|
includeAdultCommunities
|
||||||
@@ -112,10 +124,6 @@ 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_LIVE_LIMIT = 20
|
||||||
private const val DEFAULT_BANNER_LIMIT = 20
|
private const val DEFAULT_BANNER_LIMIT = 20
|
||||||
|
|||||||
@@ -4,15 +4,29 @@ 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 findLiveRecommendations(
|
||||||
|
offset: Int = 0,
|
||||||
|
limit: Int,
|
||||||
|
includeAdultLives: Boolean = false
|
||||||
|
): List<HomeLiveRecommendationRecord>
|
||||||
|
|
||||||
fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord>
|
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(
|
fun findAiCharacterSnapshots(
|
||||||
windowStart: LocalDateTime,
|
windowStart: LocalDateTime,
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
|
|||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
interface RecommendationSnapshotPort {
|
interface RecommendationSnapshotPort {
|
||||||
fun findLatestSnapshots(sectionType: RecommendedSectionType): List<RecommendationSnapshotRecord>
|
fun findLatestSnapshots(
|
||||||
|
sectionType: RecommendedSectionType,
|
||||||
|
offset: Int = 0,
|
||||||
|
limit: Int = Int.MAX_VALUE
|
||||||
|
): List<RecommendationSnapshotRecord>
|
||||||
|
|
||||||
fun replaceSnapshots(
|
fun replaceSnapshots(
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
|
|||||||
@@ -88,6 +88,17 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
val creators = service.findRecentlyActiveCreators()
|
val creators = service.findRecentlyActiveCreators()
|
||||||
|
|
||||||
assertEquals(10, port.activeCreatorLimit)
|
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)
|
assertEquals(port.activeCreators, creators)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,12 +400,19 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
|
|
||||||
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
|
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
|
||||||
var liveLimit: Int? = null
|
var liveLimit: Int? = null
|
||||||
|
var liveOffset: Int? = null
|
||||||
|
var liveIncludeAdultLives: Boolean? = null
|
||||||
var bannerLimit: Int? = null
|
var bannerLimit: Int? = null
|
||||||
var activeCreatorLimit: Int? = null
|
var activeCreatorLimit: Int? = null
|
||||||
|
var activeCreatorIncludeAdultActivities: Boolean? = null
|
||||||
var recentDebutNow: LocalDateTime? = null
|
var recentDebutNow: LocalDateTime? = null
|
||||||
var recentDebutLimit: Int? = null
|
var recentDebutLimit: Int? = null
|
||||||
|
var recentDebutOffset: Int? = null
|
||||||
|
var recentDebutIncludeAdultContents: Boolean? = null
|
||||||
var firstAudioNow: LocalDateTime? = null
|
var firstAudioNow: LocalDateTime? = null
|
||||||
var firstAudioLimit: Int? = null
|
var firstAudioLimit: Int? = null
|
||||||
|
var firstAudioOffset: Int? = null
|
||||||
|
var firstAudioIncludeAdultContents: Boolean? = null
|
||||||
var aiCharacterDetailIds: List<Long> = emptyList()
|
var aiCharacterDetailIds: List<Long> = emptyList()
|
||||||
var cheerCreatorDetailIds: List<Long> = emptyList()
|
var cheerCreatorDetailIds: List<Long> = emptyList()
|
||||||
var popularCommunityDetailIds: List<Long> = emptyList()
|
var popularCommunityDetailIds: List<Long> = emptyList()
|
||||||
@@ -466,8 +484,14 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
var popularCommunityDetails: List<HomePopularCommunityRecommendationRecord> = emptyList()
|
var popularCommunityDetails: List<HomePopularCommunityRecommendationRecord> = emptyList()
|
||||||
var genreCreatorRecommendations: List<HomeGenreCreatorRecommendationGroup> = 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
|
liveLimit = limit
|
||||||
|
liveIncludeAdultLives = includeAdultLives
|
||||||
return liveRecommendations
|
return liveRecommendations
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,20 +500,38 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
return banners
|
return banners
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun findRecentlyActiveCreators(limit: Int): List<RecentlyActiveCreatorRecord> {
|
override fun findRecentlyActiveCreators(
|
||||||
|
limit: Int,
|
||||||
|
includeAdultActivities: Boolean
|
||||||
|
): List<RecentlyActiveCreatorRecord> {
|
||||||
activeCreatorLimit = limit
|
activeCreatorLimit = limit
|
||||||
|
activeCreatorIncludeAdultActivities = includeAdultActivities
|
||||||
return activeCreators
|
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
|
recentDebutNow = now
|
||||||
|
recentDebutOffset = offset
|
||||||
recentDebutLimit = limit
|
recentDebutLimit = limit
|
||||||
|
recentDebutIncludeAdultContents = includeAdultContents
|
||||||
return recentDebutCreators
|
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
|
firstAudioNow = now
|
||||||
|
firstAudioOffset = offset
|
||||||
firstAudioLimit = limit
|
firstAudioLimit = limit
|
||||||
|
firstAudioIncludeAdultContents = includeAdultContents
|
||||||
return firstAudioContents
|
return firstAudioContents
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,14 +590,21 @@ class HomeRecommendationQueryServiceTest {
|
|||||||
private class FakeHomeRecommendationSnapshotPort : RecommendationSnapshotPort {
|
private class FakeHomeRecommendationSnapshotPort : RecommendationSnapshotPort {
|
||||||
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
|
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
|
val latestSnapshotAt = snapshots
|
||||||
.filter { it.sectionType == sectionType }
|
.filter { it.sectionType == sectionType }
|
||||||
.maxOfOrNull { it.snapshotAt }
|
.maxOfOrNull { it.snapshotAt }
|
||||||
|
|
||||||
return snapshots
|
val all = snapshots
|
||||||
.filter { it.sectionType == sectionType && it.snapshotAt == latestSnapshotAt }
|
.filter { it.sectionType == sectionType && it.snapshotAt == latestSnapshotAt }
|
||||||
.sortedWith(compareByDescending<RecommendationSnapshotRecord> { it.score }.thenBy { it.randomTieBreaker })
|
.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(
|
override fun replaceSnapshots(
|
||||||
|
|||||||
@@ -176,14 +176,21 @@ class RecommendationSnapshotRefreshServiceTest {
|
|||||||
private class FakeRecommendationSnapshotPort : RecommendationSnapshotPort {
|
private class FakeRecommendationSnapshotPort : RecommendationSnapshotPort {
|
||||||
private val snapshots = mutableListOf<RecommendationSnapshotRecord>()
|
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
|
val latestSnapshotAt = snapshots
|
||||||
.filter { it.sectionType == sectionType }
|
.filter { it.sectionType == sectionType }
|
||||||
.maxOfOrNull { it.snapshotAt }
|
.maxOfOrNull { it.snapshotAt }
|
||||||
|
|
||||||
return snapshots
|
val all = snapshots
|
||||||
.filter { it.sectionType == sectionType && it.snapshotAt == latestSnapshotAt }
|
.filter { it.sectionType == sectionType && it.snapshotAt == latestSnapshotAt }
|
||||||
.sortedWith(compareByDescending<RecommendationSnapshotRecord> { it.score }.thenBy { it.randomTieBreaker })
|
.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(
|
override fun replaceSnapshots(
|
||||||
|
|||||||
Reference in New Issue
Block a user