feat(ranking): 조회 cold-start fallback을 추가한다
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.ranking.application
|
package kr.co.vividnext.sodalive.v2.ranking.application
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem
|
||||||
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicy
|
||||||
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicy
|
||||||
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||||
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingUtcRange
|
||||||
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationPort
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
|
||||||
@@ -8,15 +13,20 @@ import org.slf4j.LoggerFactory
|
|||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
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.ZonedDateTime
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class CreatorRankingQueryService(
|
class CreatorRankingQueryService(
|
||||||
private val snapshotPort: CreatorRankingSnapshotPort,
|
private val snapshotPort: CreatorRankingSnapshotPort,
|
||||||
private val blockPort: CreatorRankingBlockPort,
|
private val blockPort: CreatorRankingBlockPort,
|
||||||
|
private val aggregationPort: CreatorRankingAggregationPort,
|
||||||
|
private val nowProvider: () -> ZonedDateTime = { ZonedDateTime.now() },
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val cloudFrontHost: String
|
private val cloudFrontHost: String
|
||||||
) {
|
) {
|
||||||
private val log = LoggerFactory.getLogger(javaClass)
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
|
private val periodPolicy = CreatorRankingPeriodPolicy()
|
||||||
|
private val scorePolicy = CreatorRankingScorePolicy()
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getCreatorRankings(viewerMemberId: Long?): CreatorRankingResult {
|
fun getCreatorRankings(viewerMemberId: Long?): CreatorRankingResult {
|
||||||
@@ -24,6 +34,17 @@ class CreatorRankingQueryService(
|
|||||||
return runCatching {
|
return runCatching {
|
||||||
val latestItems = snapshotPort.findLatestSnapshots().toRankedItems()
|
val latestItems = snapshotPort.findLatestSnapshots().toRankedItems()
|
||||||
if (latestItems.isEmpty()) {
|
if (latestItems.isEmpty()) {
|
||||||
|
if (snapshotPort.isSnapshotTableEmpty()) {
|
||||||
|
val fallbackItems = aggregateColdStartFallback().toRankedItems()
|
||||||
|
val blockedCreatorIds = findBlockedCreatorIds(viewerMemberId = viewerMemberId, items = fallbackItems)
|
||||||
|
return@runCatching QueryLogResult(
|
||||||
|
result = CreatorRankingResult(
|
||||||
|
showRankChange = false,
|
||||||
|
items = fallbackItems.map { it.maskIfBlocked(blockedCreatorIds) }
|
||||||
|
),
|
||||||
|
blockedCreatorCount = blockedCreatorIds.size
|
||||||
|
)
|
||||||
|
}
|
||||||
return@runCatching QueryLogResult(
|
return@runCatching QueryLogResult(
|
||||||
result = CreatorRankingResult(showRankChange = false, items = emptyList()),
|
result = CreatorRankingResult(showRankChange = false, items = emptyList()),
|
||||||
blockedCreatorCount = 0
|
blockedCreatorCount = 0
|
||||||
@@ -69,6 +90,43 @@ class CreatorRankingQueryService(
|
|||||||
val blockedCreatorCount: Int
|
val blockedCreatorCount: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun aggregateColdStartFallback(): List<CreatorRankingSnapshotRecord> {
|
||||||
|
val startedAt = System.currentTimeMillis()
|
||||||
|
val period = periodPolicy.resolveLastCompletedWeek(nowProvider())
|
||||||
|
val utcRange = periodPolicy.toUtcRange(period)
|
||||||
|
log.info(
|
||||||
|
"event=creator_ranking_query_cold_start_fallback_attempt " +
|
||||||
|
"aggregationStartAtUtc={} aggregationEndAtUtc={}",
|
||||||
|
utcRange.startInclusiveUtc,
|
||||||
|
utcRange.endExclusiveUtc
|
||||||
|
)
|
||||||
|
return runCatching {
|
||||||
|
aggregationPort.aggregateCandidates(
|
||||||
|
startInclusiveUtc = utcRange.startInclusiveUtc,
|
||||||
|
endExclusiveUtc = utcRange.endExclusiveUtc
|
||||||
|
).map { it.toSnapshotRecord(utcRange) }
|
||||||
|
}.onSuccess { snapshots ->
|
||||||
|
log.info(
|
||||||
|
"event=creator_ranking_query_cold_start_fallback_success " +
|
||||||
|
"aggregationStartAtUtc={} aggregationEndAtUtc={} itemCount={} elapsedMs={}",
|
||||||
|
utcRange.startInclusiveUtc,
|
||||||
|
utcRange.endExclusiveUtc,
|
||||||
|
snapshots.size.coerceAtMost(RANKING_LIMIT),
|
||||||
|
System.currentTimeMillis() - startedAt
|
||||||
|
)
|
||||||
|
}.onFailure { ex ->
|
||||||
|
log.warn(
|
||||||
|
"event=creator_ranking_query_cold_start_fallback_failure " +
|
||||||
|
"aggregationStartAtUtc={} aggregationEndAtUtc={} elapsedMs={} error={}",
|
||||||
|
utcRange.startInclusiveUtc,
|
||||||
|
utcRange.endExclusiveUtc,
|
||||||
|
System.currentTimeMillis() - startedAt,
|
||||||
|
ex.message,
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
private fun List<CreatorRankingSnapshotRecord>.toRankedItems(): List<CreatorRankingItem> {
|
private fun List<CreatorRankingSnapshotRecord>.toRankedItems(): List<CreatorRankingItem> {
|
||||||
return groupBy { it.finalScore }
|
return groupBy { it.finalScore }
|
||||||
.toSortedMap(compareByDescending { it })
|
.toSortedMap(compareByDescending { it })
|
||||||
@@ -89,6 +147,54 @@ class CreatorRankingQueryService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CreatorRankingSnapshotCandidate.toSnapshotRecord(utcRange: CreatorRankingUtcRange): CreatorRankingSnapshotRecord {
|
||||||
|
val calculatedContentLiveScore = scorePolicy.calculateContentLiveScore(
|
||||||
|
liveCanAmount = liveCanAmount,
|
||||||
|
contentPurchaseCanAmount = contentPurchaseCanAmount
|
||||||
|
)
|
||||||
|
val calculatedEngagementScore = scorePolicy.calculateEngagementScore(
|
||||||
|
contentLikeCount = contentLikeCount,
|
||||||
|
contentCommentCount = contentCommentCount
|
||||||
|
)
|
||||||
|
val calculatedSupportScore = scorePolicy.calculateSupportScore(
|
||||||
|
channelDonationCanAmount = channelDonationCanAmount,
|
||||||
|
channelDonationCount = channelDonationCount,
|
||||||
|
fanTalkCount = fanTalkCount
|
||||||
|
)
|
||||||
|
val calculatedFanLoyaltyScore = scorePolicy.calculateFanLoyaltyScore(
|
||||||
|
finalFollowerCount = finalFollowerCount,
|
||||||
|
followIncrease = followIncrease
|
||||||
|
)
|
||||||
|
val calculatedFinalScore = scorePolicy.calculateFinalScore(
|
||||||
|
contentLiveScore = calculatedContentLiveScore,
|
||||||
|
engagementScore = calculatedEngagementScore,
|
||||||
|
supportScore = calculatedSupportScore,
|
||||||
|
fanLoyaltyScore = calculatedFanLoyaltyScore
|
||||||
|
)
|
||||||
|
|
||||||
|
return CreatorRankingSnapshotRecord(
|
||||||
|
aggregationStartAtUtc = utcRange.startInclusiveUtc,
|
||||||
|
aggregationEndAtUtc = utcRange.endExclusiveUtc,
|
||||||
|
creatorId = creatorId,
|
||||||
|
nickname = nickname,
|
||||||
|
profileImageUrl = profileImageUrl,
|
||||||
|
finalScore = calculatedFinalScore,
|
||||||
|
contentLiveScore = calculatedContentLiveScore,
|
||||||
|
engagementScore = calculatedEngagementScore,
|
||||||
|
supportScore = calculatedSupportScore,
|
||||||
|
fanLoyaltyScore = calculatedFanLoyaltyScore,
|
||||||
|
liveCanAmount = liveCanAmount,
|
||||||
|
contentPurchaseCanAmount = contentPurchaseCanAmount,
|
||||||
|
contentLikeCount = contentLikeCount,
|
||||||
|
contentCommentCount = contentCommentCount,
|
||||||
|
channelDonationCanAmount = channelDonationCanAmount,
|
||||||
|
channelDonationCount = channelDonationCount,
|
||||||
|
fanTalkCount = fanTalkCount,
|
||||||
|
finalFollowerCount = finalFollowerCount,
|
||||||
|
followIncrease = followIncrease
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun findBlockedCreatorIds(viewerMemberId: Long?, items: List<CreatorRankingItem>): Set<Long> {
|
private fun findBlockedCreatorIds(viewerMemberId: Long?, items: List<CreatorRankingItem>): Set<Long> {
|
||||||
if (viewerMemberId == null) {
|
if (viewerMemberId == null) {
|
||||||
return emptySet()
|
return emptySet()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.ranking.application
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||||
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationPort
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
|
||||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
|
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
|
||||||
@@ -16,6 +17,8 @@ import org.junit.jupiter.api.extension.ExtendWith
|
|||||||
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 java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
|
||||||
@ExtendWith(OutputCaptureExtension::class)
|
@ExtendWith(OutputCaptureExtension::class)
|
||||||
class CreatorRankingQueryServiceTest {
|
class CreatorRankingQueryServiceTest {
|
||||||
@@ -71,6 +74,90 @@ class CreatorRankingQueryServiceTest {
|
|||||||
assertTrue(result.items.isEmpty())
|
assertTrue(result.items.isEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("최신 스냅샷이 있으면 cold-start fallback 집계를 호출하지 않는다")
|
||||||
|
fun shouldNotUseColdStartFallbackWhenLatestSnapshotsExist() {
|
||||||
|
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||||
|
val aggregationPort = FakeCreatorRankingQueryAggregationPort()
|
||||||
|
snapshotPort.latestSnapshots = listOf(snapshot(creatorId = 1L, finalScore = 100.0))
|
||||||
|
snapshotPort.snapshotTableEmpty = true
|
||||||
|
aggregationPort.candidates = listOf(candidate(creatorId = 2L))
|
||||||
|
val service = service(snapshotPort = snapshotPort, aggregationPort = aggregationPort)
|
||||||
|
|
||||||
|
val result = service.getCreatorRankings(viewerMemberId = null)
|
||||||
|
|
||||||
|
assertEquals(listOf(1L), result.items.map { it.creatorId })
|
||||||
|
assertEquals(0, aggregationPort.aggregateCallCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("최신 스냅샷이 없고 스냅샷 테이블이 완전히 비어 있으면 cold-start fallback을 반환한다")
|
||||||
|
fun shouldUseColdStartFallbackOnlyWhenSnapshotTableIsEmpty() {
|
||||||
|
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||||
|
val aggregationPort = FakeCreatorRankingQueryAggregationPort()
|
||||||
|
snapshotPort.snapshotTableEmpty = true
|
||||||
|
aggregationPort.candidates = listOf(
|
||||||
|
candidate(creatorId = 1L, liveCanAmount = 100),
|
||||||
|
candidate(creatorId = 2L, liveCanAmount = 200)
|
||||||
|
)
|
||||||
|
val service = service(snapshotPort = snapshotPort, aggregationPort = aggregationPort)
|
||||||
|
|
||||||
|
val result = service.getCreatorRankings(viewerMemberId = null)
|
||||||
|
|
||||||
|
assertFalse(result.showRankChange)
|
||||||
|
assertEquals(listOf(2L, 1L), result.items.map { it.creatorId })
|
||||||
|
assertEquals(listOf(1, 2), result.items.map { it.rank })
|
||||||
|
assertTrue(result.items.all { it.rankChange == null })
|
||||||
|
assertTrue(result.items.none { it.isNew })
|
||||||
|
assertEquals(1, aggregationPort.aggregateCallCount)
|
||||||
|
assertEquals(LocalDateTime.of(2026, 5, 31, 15, 0), aggregationPort.startInclusiveUtc)
|
||||||
|
assertEquals(LocalDateTime.of(2026, 6, 7, 15, 0), aggregationPort.endExclusiveUtc)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("최신 스냅샷이 없어도 과거 스냅샷 row가 있으면 cold-start fallback을 호출하지 않는다")
|
||||||
|
fun shouldNotUseColdStartFallbackWhenAnyHistoricalSnapshotExists() {
|
||||||
|
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||||
|
val aggregationPort = FakeCreatorRankingQueryAggregationPort()
|
||||||
|
snapshotPort.snapshotTableEmpty = false
|
||||||
|
aggregationPort.candidates = listOf(candidate(creatorId = 1L))
|
||||||
|
val service = service(snapshotPort = snapshotPort, aggregationPort = aggregationPort)
|
||||||
|
|
||||||
|
val result = service.getCreatorRankings(viewerMemberId = null)
|
||||||
|
|
||||||
|
assertFalse(result.showRankChange)
|
||||||
|
assertTrue(result.items.isEmpty())
|
||||||
|
assertEquals(0, aggregationPort.aggregateCallCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("cold-start fallback도 차단 관계가 있으면 크리에이터 식별 정보만 마스킹한다")
|
||||||
|
fun shouldMaskBlockedCreatorIdentityInColdStartFallback() {
|
||||||
|
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||||
|
val aggregationPort = FakeCreatorRankingQueryAggregationPort()
|
||||||
|
val blockPort = FakeCreatorRankingBlockPort()
|
||||||
|
snapshotPort.snapshotTableEmpty = true
|
||||||
|
aggregationPort.candidates = listOf(
|
||||||
|
candidate(creatorId = 1L, liveCanAmount = 200),
|
||||||
|
candidate(creatorId = 2L, liveCanAmount = 100)
|
||||||
|
)
|
||||||
|
blockPort.blockedCreatorIds = setOf(1L)
|
||||||
|
val service = service(
|
||||||
|
snapshotPort = snapshotPort,
|
||||||
|
blockPort = blockPort,
|
||||||
|
aggregationPort = aggregationPort
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = service.getCreatorRankings(viewerMemberId = 99L)
|
||||||
|
|
||||||
|
assertEquals(99L, blockPort.memberId)
|
||||||
|
assertEquals(setOf(1L, 2L), blockPort.creatorIds)
|
||||||
|
assertEquals(0L, result.items.first().creatorId)
|
||||||
|
assertEquals("", result.items.first().nickname)
|
||||||
|
assertEquals("https://cdn.test/profile/default-profile.png", result.items.first().profileImageUrl)
|
||||||
|
assertEquals(2L, result.items[1].creatorId)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("직전 완료 주차 스냅샷이 없으면 순위 변화 없이 최신 스냅샷 상위 20명을 반환한다")
|
@DisplayName("직전 완료 주차 스냅샷이 없으면 순위 변화 없이 최신 스냅샷 상위 20명을 반환한다")
|
||||||
fun shouldReturnLatestTopTwentyWithoutRankChangeWhenPreviousSnapshotsDoNotExist() {
|
fun shouldReturnLatestTopTwentyWithoutRankChangeWhenPreviousSnapshotsDoNotExist() {
|
||||||
@@ -213,17 +300,86 @@ class CreatorRankingQueryServiceTest {
|
|||||||
assertTrue(output.out.contains("error=latest snapshots failed"))
|
assertTrue(output.out.contains("error=latest snapshots failed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("cold-start fallback 성공은 기간과 반환 수를 로그로 남긴다")
|
||||||
|
fun shouldLogColdStartFallbackSuccessWithPeriodAndCount(output: CapturedOutput) {
|
||||||
|
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||||
|
val aggregationPort = FakeCreatorRankingQueryAggregationPort()
|
||||||
|
snapshotPort.snapshotTableEmpty = true
|
||||||
|
aggregationPort.candidates = listOf(candidate(creatorId = 1L))
|
||||||
|
val service = service(snapshotPort = snapshotPort, aggregationPort = aggregationPort)
|
||||||
|
|
||||||
|
service.getCreatorRankings(viewerMemberId = null)
|
||||||
|
|
||||||
|
assertTrue(output.out.contains("event=creator_ranking_query_cold_start_fallback_attempt"))
|
||||||
|
assertTrue(output.out.contains("event=creator_ranking_query_cold_start_fallback_success"))
|
||||||
|
assertTrue(output.out.contains("aggregationStartAtUtc=2026-05-31T15:00"))
|
||||||
|
assertTrue(output.out.contains("aggregationEndAtUtc=2026-06-07T15:00"))
|
||||||
|
assertTrue(output.out.contains("itemCount=1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("cold-start fallback 실패는 기간과 에러를 로그로 남기고 예외를 전파한다")
|
||||||
|
fun shouldLogColdStartFallbackFailureWithError(output: CapturedOutput) {
|
||||||
|
val snapshotPort = FakeCreatorRankingQuerySnapshotPort()
|
||||||
|
val aggregationPort = FakeCreatorRankingQueryAggregationPort()
|
||||||
|
snapshotPort.snapshotTableEmpty = true
|
||||||
|
aggregationPort.failure = IllegalStateException("fallback failed")
|
||||||
|
val service = service(snapshotPort = snapshotPort, aggregationPort = aggregationPort)
|
||||||
|
|
||||||
|
val exception = assertThrows(IllegalStateException::class.java) {
|
||||||
|
service.getCreatorRankings(viewerMemberId = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("fallback failed", exception.message)
|
||||||
|
assertTrue(output.out.contains("event=creator_ranking_query_cold_start_fallback_attempt"))
|
||||||
|
assertTrue(output.out.contains("event=creator_ranking_query_cold_start_fallback_failure"))
|
||||||
|
assertTrue(output.out.contains("aggregationStartAtUtc=2026-05-31T15:00"))
|
||||||
|
assertTrue(output.out.contains("aggregationEndAtUtc=2026-06-07T15:00"))
|
||||||
|
assertTrue(output.out.contains("error=fallback failed"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun service(
|
private fun service(
|
||||||
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingQuerySnapshotPort(),
|
snapshotPort: CreatorRankingSnapshotPort = FakeCreatorRankingQuerySnapshotPort(),
|
||||||
blockPort: CreatorRankingBlockPort = FakeCreatorRankingBlockPort()
|
blockPort: CreatorRankingBlockPort = FakeCreatorRankingBlockPort(),
|
||||||
|
aggregationPort: CreatorRankingAggregationPort = FakeCreatorRankingQueryAggregationPort()
|
||||||
): CreatorRankingQueryService {
|
): CreatorRankingQueryService {
|
||||||
return CreatorRankingQueryService(
|
return CreatorRankingQueryService(
|
||||||
snapshotPort = snapshotPort,
|
snapshotPort = snapshotPort,
|
||||||
blockPort = blockPort,
|
blockPort = blockPort,
|
||||||
|
aggregationPort = aggregationPort,
|
||||||
|
nowProvider = {
|
||||||
|
ZonedDateTime.of(2026, 6, 8, 7, 30, 0, 0, ZoneId.of("Asia/Seoul"))
|
||||||
|
},
|
||||||
cloudFrontHost = "https://cdn.test"
|
cloudFrontHost = "https://cdn.test"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun candidate(
|
||||||
|
creatorId: Long,
|
||||||
|
liveCanAmount: Long = 100
|
||||||
|
): CreatorRankingSnapshotCandidate {
|
||||||
|
return CreatorRankingSnapshotCandidate(
|
||||||
|
creatorId = creatorId,
|
||||||
|
nickname = "creator-$creatorId",
|
||||||
|
profileImageUrl = "profile-$creatorId.png",
|
||||||
|
finalScore = 0.0,
|
||||||
|
contentLiveScore = 0.0,
|
||||||
|
engagementScore = 0.0,
|
||||||
|
supportScore = 0.0,
|
||||||
|
fanLoyaltyScore = 0.0,
|
||||||
|
liveCanAmount = liveCanAmount,
|
||||||
|
contentPurchaseCanAmount = 0,
|
||||||
|
contentLikeCount = 0,
|
||||||
|
contentCommentCount = 0,
|
||||||
|
channelDonationCanAmount = 0,
|
||||||
|
channelDonationCount = 0,
|
||||||
|
fanTalkCount = 0,
|
||||||
|
finalFollowerCount = 0,
|
||||||
|
followIncrease = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun snapshot(
|
private fun snapshot(
|
||||||
creatorId: Long,
|
creatorId: Long,
|
||||||
finalScore: Double
|
finalScore: Double
|
||||||
@@ -256,6 +412,7 @@ private class FakeCreatorRankingQuerySnapshotPort : CreatorRankingSnapshotPort {
|
|||||||
var latestSnapshots: List<CreatorRankingSnapshotRecord> = emptyList()
|
var latestSnapshots: List<CreatorRankingSnapshotRecord> = emptyList()
|
||||||
var previousSnapshots: List<CreatorRankingSnapshotRecord> = emptyList()
|
var previousSnapshots: List<CreatorRankingSnapshotRecord> = emptyList()
|
||||||
var latestFailure: RuntimeException? = null
|
var latestFailure: RuntimeException? = null
|
||||||
|
var snapshotTableEmpty: Boolean = true
|
||||||
|
|
||||||
override fun findSnapshotsByAggregationPeriod(
|
override fun findSnapshotsByAggregationPeriod(
|
||||||
aggregationStartAtUtc: LocalDateTime,
|
aggregationStartAtUtc: LocalDateTime,
|
||||||
@@ -269,6 +426,8 @@ private class FakeCreatorRankingQuerySnapshotPort : CreatorRankingSnapshotPort {
|
|||||||
|
|
||||||
override fun findPreviousCompletedSnapshots(): List<CreatorRankingSnapshotRecord> = previousSnapshots
|
override fun findPreviousCompletedSnapshots(): List<CreatorRankingSnapshotRecord> = previousSnapshots
|
||||||
|
|
||||||
|
override fun isSnapshotTableEmpty(): Boolean = snapshotTableEmpty
|
||||||
|
|
||||||
override fun replaceSnapshots(
|
override fun replaceSnapshots(
|
||||||
aggregationStartAtUtc: LocalDateTime,
|
aggregationStartAtUtc: LocalDateTime,
|
||||||
aggregationEndAtUtc: LocalDateTime,
|
aggregationEndAtUtc: LocalDateTime,
|
||||||
@@ -276,6 +435,25 @@ private class FakeCreatorRankingQuerySnapshotPort : CreatorRankingSnapshotPort {
|
|||||||
) = Unit
|
) = Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class FakeCreatorRankingQueryAggregationPort : CreatorRankingAggregationPort {
|
||||||
|
var candidates: List<CreatorRankingSnapshotCandidate> = emptyList()
|
||||||
|
var failure: RuntimeException? = null
|
||||||
|
var aggregateCallCount = 0
|
||||||
|
var startInclusiveUtc: LocalDateTime? = null
|
||||||
|
var endExclusiveUtc: LocalDateTime? = null
|
||||||
|
|
||||||
|
override fun aggregateCandidates(
|
||||||
|
startInclusiveUtc: LocalDateTime,
|
||||||
|
endExclusiveUtc: LocalDateTime
|
||||||
|
): List<CreatorRankingSnapshotCandidate> {
|
||||||
|
aggregateCallCount++
|
||||||
|
this.startInclusiveUtc = startInclusiveUtc
|
||||||
|
this.endExclusiveUtc = endExclusiveUtc
|
||||||
|
failure?.let { throw it }
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class FakeCreatorRankingBlockPort : CreatorRankingBlockPort {
|
private class FakeCreatorRankingBlockPort : CreatorRankingBlockPort {
|
||||||
var blockedCreatorIds: Set<Long> = emptySet()
|
var blockedCreatorIds: Set<Long> = emptySet()
|
||||||
var memberId: Long? = null
|
var memberId: Long? = null
|
||||||
|
|||||||
Reference in New Issue
Block a user