feat(content-ranking): 랭킹 조회 fallback과 차단 필터를 적용한다

This commit is contained in:
2026-06-24 19:03:12 +09:00
parent 7ec19e3c8c
commit cf29600ad3
2 changed files with 195 additions and 11 deletions

View File

@@ -5,10 +5,11 @@ import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreference
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.AudioRankingBlockPort
import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshotPort
import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshotRecord
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.ZoneOffset
import java.time.ZonedDateTime
@@ -16,24 +17,27 @@ import java.time.ZonedDateTime
class AudioRankingQueryService(
private val snapshotPort: AudioRankingSnapshotPort,
private val memberContentPreferenceService: MemberContentPreferenceService,
private val blockPort: AudioRankingBlockPort,
private val jobService: AudioRankingSnapshotJobService,
private val nowProvider: () -> ZonedDateTime = { ZonedDateTime.now() }
) {
@Transactional(readOnly = true)
private val log = LoggerFactory.getLogger(javaClass)
fun getRankings(type: AudioRankingType, member: Member?): AudioRanking {
val nowUtc = nowProvider().withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(type, nowUtc)
val latestSnapshots = 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)
val blockedCreatorMemberIds = blockedCreatorMemberIds(member, latestSnapshots + previousSnapshots)
val latestVisibleSnapshots = latestSnapshots.visibleTo(canViewAdultContent, blockedCreatorMemberIds).take(ITEM_LIMIT)
val previousRankByContentId = previousSnapshots.visibleTo(canViewAdultContent, blockedCreatorMemberIds)
.take(ITEM_LIMIT)
.mapIndexed { index, snapshot -> snapshot.contentId to index + 1 }
.toMap()
@@ -48,13 +52,44 @@ class AudioRankingQueryService(
)
}
private fun findLatestVisibleSnapshots(
type: AudioRankingType,
nowUtc: java.time.LocalDateTime
): List<AudioRankingSnapshotRecord> {
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(type, nowUtc)
if (latestSnapshots.isNotEmpty()) return latestSnapshots
runCatching { jobService.refreshLastCompletedWeekByFallback(type) }
.onFailure { ex ->
log.warn(
"event=audio_ranking_query_fallback_failure rankingType={} error={}",
type,
ex.message,
ex
)
}
return snapshotPort.findLatestVisibleSnapshots(type, nowUtc)
}
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 blockedCreatorMemberIds(member: Member?, snapshots: List<AudioRankingSnapshotRecord>): Set<Long> {
val memberId = member?.id ?: return emptySet()
val creatorMemberIds = snapshots.map { it.creatorMemberId }.toSet()
if (creatorMemberIds.isEmpty()) return emptySet()
return blockPort.findBlockedCreatorMemberIds(memberId, creatorMemberIds)
}
private fun List<AudioRankingSnapshotRecord>.visibleTo(
canViewAdultContent: Boolean,
blockedCreatorMemberIds: Set<Long>
): List<AudioRankingSnapshotRecord> {
return filter { snapshot ->
(canViewAdultContent || !snapshot.isAdult) && snapshot.creatorMemberId !in blockedCreatorMemberIds
}
}
private fun AudioRankingSnapshotRecord.toItem(