feat(ranking): 주간 스냅샷 갱신을 추가한다

This commit is contained in:
2026-06-08 18:21:50 +09:00
parent 6891573dcc
commit 1b74e43706
3 changed files with 311 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.v2.ranking.adapter.out.scheduler
import kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshService
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
@Component
class CreatorRankingSnapshotScheduler(
private val refreshService: CreatorRankingSnapshotRefreshService
) {
@Scheduled(cron = "0 0 6 * * MON", zone = "Asia/Seoul")
fun refreshLastCompletedWeek() {
refreshService.refreshLastCompletedWeek()
}
}

View File

@@ -0,0 +1,102 @@
package kr.co.vividnext.sodalive.v2.ranking.application
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.CreatorRankingSnapshotPort
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.ZonedDateTime
@Service
class CreatorRankingSnapshotRefreshService(
private val aggregationPort: CreatorRankingAggregationPort,
private val snapshotPort: CreatorRankingSnapshotPort
) {
private val periodPolicy = CreatorRankingPeriodPolicy()
private val scorePolicy = CreatorRankingScorePolicy()
@Transactional
fun refreshLastCompletedWeek() {
refreshLastCompletedWeek(ZonedDateTime.now())
}
@Transactional
fun refreshLastCompletedWeek(now: ZonedDateTime) {
val period = periodPolicy.resolveLastCompletedWeek(now)
val utcRange = periodPolicy.toUtcRange(period)
val snapshots = aggregationPort.aggregateCandidates(
startInclusiveUtc = utcRange.startInclusiveUtc,
endExclusiveUtc = utcRange.endExclusiveUtc
).map { it.toSnapshotRecord(utcRange) }
.sortedByDescending { it.finalScore }
.takeRankedBoundary(limit = SNAPSHOT_LIMIT)
snapshotPort.replaceSnapshots(
aggregationStartAtUtc = utcRange.startInclusiveUtc,
aggregationEndAtUtc = utcRange.endExclusiveUtc,
newSnapshots = snapshots
)
}
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 List<CreatorRankingSnapshotRecord>.takeRankedBoundary(limit: Int): List<CreatorRankingSnapshotRecord> {
if (size <= limit) return this
val boundaryScore = this[limit - 1].finalScore
return filter { it.finalScore >= boundaryScore }
}
companion object {
private const val SNAPSHOT_LIMIT = 20
}
}