feat(home): 크리에이터 랭킹 조회 fallback을 재조회로 변경한다

This commit is contained in:
2026-07-10 14:45:42 +09:00
parent d5ffa56f17
commit 263202f0f5
2 changed files with 75 additions and 649 deletions

View File

@@ -2,19 +2,13 @@ package kr.co.vividnext.sodalive.v2.ranking.application
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
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.CreatorRankingType
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.CreatorRankingSnapshotPort
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
@@ -23,42 +17,21 @@ import java.time.ZonedDateTime
class CreatorRankingQueryService(
private val snapshotPort: CreatorRankingSnapshotPort,
private val blockPort: CreatorRankingBlockPort,
private val aggregationPort: CreatorRankingAggregationPort,
private val snapshotJobService: CreatorRankingSnapshotJobService,
private val nowProvider: () -> ZonedDateTime = { ZonedDateTime.now() },
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
private val log = LoggerFactory.getLogger(javaClass)
private val periodPolicy = CreatorRankingPeriodPolicy()
private val scorePolicy = CreatorRankingScorePolicy()
@Transactional(readOnly = true)
fun getCreatorRankings(viewerMemberId: Long?): CreatorRankingResult {
val startedAt = System.currentTimeMillis()
return runCatching {
val nowUtc = nowUtc()
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc)
val latestSnapshots = findLatestVisibleSnapshots(nowUtc)
val latestItems = latestSnapshots.toRankedItems()
if (latestItems.isEmpty()) {
if (snapshotPort.isSnapshotTableEmpty()) {
val fallbackItems = aggregateColdStartFallback(nowUtc).toRankedItems()
if (fallbackItems.isNotEmpty()) {
delegateColdStartSnapshotRefresh()
}
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(
result = CreatorRankingResult(showRankChange = false, items = emptyList()),
blockedCreatorCount = 0
)
return@runCatching QueryLogResult(CreatorRankingResult(showRankChange = false, items = emptyList()), 0)
}
val previousItems = snapshotPort.findPreviousVisibleSnapshots(
@@ -77,10 +50,7 @@ class CreatorRankingQueryService(
).maskIfBlocked(blockedCreatorIds)
}
QueryLogResult(
result = CreatorRankingResult(showRankChange = showRankChange, items = items),
blockedCreatorCount = blockedCreatorIds.size
)
QueryLogResult(CreatorRankingResult(showRankChange = showRankChange, items = items), blockedCreatorIds.size)
}.onSuccess { logResult ->
log.info(
"event=creator_ranking_query_success showRankChange={} itemCount={} blockedCreatorCount={} elapsedMs={}",
@@ -99,75 +69,30 @@ class CreatorRankingQueryService(
}.getOrThrow().result
}
private fun findLatestVisibleSnapshots(nowUtc: LocalDateTime): List<CreatorRankingSnapshotRecord> {
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc)
if (latestSnapshots.isNotEmpty()) return latestSnapshots
runCatching { snapshotJobService.refreshLastCompletedWeekByFallback() }
.onFailure { ex ->
log.warn("event=creator_ranking_query_fallback_failure error={}", ex.message, ex)
}
return snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc)
}
private data class QueryLogResult(
val result: CreatorRankingResult,
val blockedCreatorCount: Int
)
private fun aggregateColdStartFallback(nowUtc: LocalDateTime): List<CreatorRankingSnapshotRecord> {
val startedAt = System.currentTimeMillis()
val period = periodPolicy.resolveLastCompletedWeek(nowProvider())
val utcRange = periodPolicy.toUtcRange(period)
val visibleFromAtUtc = periodPolicy.resolveVisibleFromAtUtc(period.endExclusiveKst)
if (visibleFromAtUtc > nowUtc) {
return emptyList()
}
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 delegateColdStartSnapshotRefresh() {
runCatching {
snapshotJobService.ensureLastCompletedWeekSnapshotForColdStart()
}.onFailure { ex ->
log.warn(
"event=creator_ranking_query_cold_start_snapshot_refresh_failure error={}",
ex.message,
ex
)
}
}
private fun nowUtc(): LocalDateTime {
return nowProvider().withZoneSameInstant(UTC_ZONE).toLocalDateTime()
}
private fun List<CreatorRankingSnapshotRecord>.toRankedItems(): List<CreatorRankingItem> {
return groupBy { it.finalScore }
.toSortedMap(compareByDescending { it })
.values
.flatMap { it.shuffled() }
return sortedBy { it.rankNo }
.take(RANKING_LIMIT)
.mapIndexed { index, snapshot -> snapshot.toItem(rank = index + 1) }
.map { snapshot -> snapshot.toItem(rank = snapshot.rankNo) }
}
private fun CreatorRankingSnapshotRecord.toItem(rank: Int): CreatorRankingItem {
@@ -181,70 +106,13 @@ 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(
rankingType = CreatorRankingType.WEEKLY,
aggregationStartAtUtc = utcRange.startInclusiveUtc,
aggregationEndAtUtc = utcRange.endExclusiveUtc,
visibleFromAtUtc = utcRange.endExclusiveUtc.plusHours(9),
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> {
if (viewerMemberId == null) {
return emptySet()
}
return blockPort.findBlockedCreatorIds(
memberId = viewerMemberId,
creatorIds = items.map { it.creatorId }
)
if (viewerMemberId == null) return emptySet()
return blockPort.findBlockedCreatorIds(memberId = viewerMemberId, creatorIds = items.map { it.creatorId })
}
private fun CreatorRankingItem.maskIfBlocked(blockedCreatorIds: Set<Long>): CreatorRankingItem {
if (!blockedCreatorIds.contains(creatorId)) {
return this
}
if (!blockedCreatorIds.contains(creatorId)) return this
return copy(
creatorId = MASKED_CREATOR_ID,
nickname = MASKED_NICKNAME,