feat(content-ranking): 스냅샷 기반 랭킹 조회를 추가한다

This commit is contained in:
2026-06-24 16:23:18 +09:00
parent 4e97364a14
commit f34962b285
2 changed files with 234 additions and 3 deletions

View File

@@ -1,17 +1,80 @@
package kr.co.vividnext.sodalive.v2.content.ranking.application
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRanking
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingItem
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType
import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshotPort
import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshotRecord
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.ZoneOffset
import java.time.ZonedDateTime
@Service
class AudioRankingQueryService {
class AudioRankingQueryService(
private val snapshotPort: AudioRankingSnapshotPort,
private val memberContentPreferenceService: MemberContentPreferenceService,
private val nowProvider: () -> ZonedDateTime = { ZonedDateTime.now() }
) {
@Transactional(readOnly = true)
fun getRankings(type: AudioRankingType, member: Member?): AudioRanking {
val nowUtc = nowProvider().withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(type, nowUtc)
if (latestSnapshots.isEmpty()) {
return AudioRanking(showRankChange = false, type = type, items = emptyList())
}
val canViewAdultContent = canViewAdultContent(member)
val latestVisibleSnapshots = latestSnapshots.visibleTo(canViewAdultContent).take(ITEM_LIMIT)
val previousSnapshots = snapshotPort.findPreviousVisibleSnapshots(
rankingType = type,
currentAggregationStartAtUtc = latestSnapshots.first().aggregationStartAtUtc,
nowUtc = nowUtc
)
val previousRankByContentId = previousSnapshots.visibleTo(canViewAdultContent)
.take(ITEM_LIMIT)
.mapIndexed { index, snapshot -> snapshot.contentId to index + 1 }
.toMap()
val showRankChange = previousRankByContentId.isNotEmpty()
return AudioRanking(
showRankChange = false,
showRankChange = showRankChange,
type = type,
items = emptyList()
items = latestVisibleSnapshots.mapIndexed { index, snapshot ->
snapshot.toItem(index + 1, showRankChange, previousRankByContentId)
}
)
}
private fun canViewAdultContent(member: Member?): Boolean {
if (member == null) return false
return memberContentPreferenceService.canViewAdultContent(member)
}
private fun List<AudioRankingSnapshotRecord>.visibleTo(canViewAdultContent: Boolean): List<AudioRankingSnapshotRecord> {
return if (canViewAdultContent) this else filter { !it.isAdult }
}
private fun AudioRankingSnapshotRecord.toItem(
rank: Int,
showRankChange: Boolean,
previousRankByContentId: Map<Long, Int>
): AudioRankingItem {
val previousRank = previousRankByContentId[contentId]
return AudioRankingItem(
contentId = contentId,
title = title,
creatorNickname = creatorNickname,
rank = rank,
rankChange = if (showRankChange && previousRank != null) previousRank - rank else null,
isNew = showRankChange && previousRank == null,
coverImageUrl = coverImageUrl
)
}
companion object {
private const val ITEM_LIMIT = 20
}
}