feat(ranking): 크리에이터 랭킹 조회 서비스를 추가한다

This commit is contained in:
2026-06-08 22:17:54 +09:00
parent 39806a999e
commit be726f0aac
3 changed files with 294 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
package kr.co.vividnext.sodalive.v2.ranking.application
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem
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.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class CreatorRankingQueryService(
private val snapshotPort: CreatorRankingSnapshotPort,
private val blockPort: CreatorRankingBlockPort,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
@Transactional(readOnly = true)
fun getCreatorRankings(viewerMemberId: Long?): CreatorRankingResult {
val latestItems = snapshotPort.findLatestSnapshots().toRankedItems()
if (latestItems.isEmpty()) {
return CreatorRankingResult(showRankChange = false, items = emptyList())
}
val previousItems = snapshotPort.findPreviousCompletedSnapshots().toRankedItems()
val previousRankByCreatorId = previousItems.associate { it.creatorId to it.rank }
val showRankChange = previousRankByCreatorId.isNotEmpty()
val blockedCreatorIds = findBlockedCreatorIds(viewerMemberId = viewerMemberId, items = latestItems)
val items = latestItems.map { item ->
val previousRank = previousRankByCreatorId[item.creatorId]
item.copy(
rankChange = if (showRankChange && previousRank != null) previousRank - item.rank else null,
isNew = showRankChange && previousRank == null
).maskIfBlocked(blockedCreatorIds)
}
return CreatorRankingResult(showRankChange = showRankChange, items = items)
}
private fun List<CreatorRankingSnapshotRecord>.toRankedItems(): List<CreatorRankingItem> {
return groupBy { it.finalScore }
.toSortedMap(compareByDescending { it })
.values
.flatMap { it.shuffled() }
.take(RANKING_LIMIT)
.mapIndexed { index, snapshot -> snapshot.toItem(rank = index + 1) }
}
private fun CreatorRankingSnapshotRecord.toItem(rank: Int): CreatorRankingItem {
return CreatorRankingItem(
rank = rank,
rankChange = null,
isNew = false,
creatorId = creatorId,
nickname = nickname,
profileImageUrl = profileImageUrl
)
}
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 }
)
}
private fun CreatorRankingItem.maskIfBlocked(blockedCreatorIds: Set<Long>): CreatorRankingItem {
if (!blockedCreatorIds.contains(creatorId)) {
return this
}
return copy(
creatorId = MASKED_CREATOR_ID,
nickname = MASKED_NICKNAME,
profileImageUrl = "$cloudFrontHost/$DEFAULT_PROFILE_IMAGE_PATH"
)
}
companion object {
private const val RANKING_LIMIT = 20
private const val MASKED_CREATOR_ID = 0L
private const val MASKED_NICKNAME = ""
private const val DEFAULT_PROFILE_IMAGE_PATH = "profile/default-profile.png"
}
}
data class CreatorRankingResult(
val showRankChange: Boolean,
val items: List<CreatorRankingItem>
)

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.v2.ranking.port.out
interface CreatorRankingBlockPort {
fun findBlockedCreatorIds(memberId: Long, creatorIds: Collection<Long>): Set<Long>
}