fix(content): 오디오 추천 조회 fallback을 섹션별로 적용한다

This commit is contained in:
2026-07-12 22:44:14 +09:00
parent d8fbf449c4
commit 91f0d20a79
2 changed files with 122 additions and 139 deletions

View File

@@ -7,12 +7,8 @@ import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommenda
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendations
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
import org.redisson.api.RedissonClient
import org.springframework.stereotype.Service
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
@@ -20,30 +16,31 @@ import java.time.ZoneId
class AudioRecommendationQueryService(
private val queryPort: AudioRecommendationQueryPort,
private val memberContentPreferenceService: MemberContentPreferenceService,
private val snapshotPort: RecommendationSnapshotPort,
private val snapshotRefreshService: AudioRecommendationSnapshotRefreshService,
private val redissonClient: RedissonClient
private val snapshotFallbackService: AudioRecommendationSnapshotFallbackService
) {
fun getRecommendations(member: Member?): AudioRecommendations {
val now = LocalDateTime.now()
val fallbackNow = LocalDateTime.now(KST_ZONE)
val canViewAdultContent = canViewAdultContent(member)
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
val memberId = member?.id
val newAndHotSectionType = newAndHotSectionType(visibility)
val newAndHotSnapshots = snapshotPort.findLatestSnapshots(newAndHotSectionType, limit = NEW_AND_HOT_HOME_LIMIT)
val mostCommentedSnapshots = snapshotPort.findLatestSnapshots(
mostCommentedSectionType(visibility),
limit = MOST_COMMENTED_AUDIO_LIMIT
)
val recommendedSnapshots = snapshotPort.findLatestSnapshots(
recommendedAudioSectionType(visibility),
limit = RECOMMENDED_AUDIO_LIMIT
)
val refreshedNewAndHotSnapshots = refreshMissingNewAndHotSnapshots(
val mostCommentedSectionType = mostCommentedSectionType(visibility)
val recommendedAudioSectionType = recommendedAudioSectionType(visibility)
val newAndHotSnapshots = findSnapshotsWithFallback(
newAndHotSectionType,
newAndHotSnapshots,
offset = 0,
limit = NEW_AND_HOT_HOME_LIMIT
limit = NEW_AND_HOT_HOME_LIMIT,
now = fallbackNow
)
val mostCommentedSnapshots = findSnapshotsWithFallback(
mostCommentedSectionType,
limit = MOST_COMMENTED_AUDIO_LIMIT,
now = fallbackNow
)
val recommendedSnapshots = findSnapshotsWithFallback(
recommendedAudioSectionType,
limit = RECOMMENDED_AUDIO_LIMIT,
now = fallbackNow
)
return AudioRecommendations(
@@ -51,7 +48,7 @@ class AudioRecommendationQueryService(
originalSeries = queryPort.findOriginalSeries(ORIGINAL_SERIES_LIMIT, memberId, canViewAdultContent, now),
latestAudios = queryPort.findLatestAudios(LATEST_AUDIO_LIMIT, memberId, canViewAdultContent, now),
newAndHotAudios = queryPort.findAudioCardsByIds(
refreshedNewAndHotSnapshots.map { it.targetId },
newAndHotSnapshots.map { it.targetId },
memberId,
canViewAdultContent,
now
@@ -74,14 +71,14 @@ class AudioRecommendationQueryService(
fun findNewAndHotAudios(member: Member, offset: Long, limit: Int): List<AudioCard> {
val now = LocalDateTime.now()
val fallbackNow = LocalDateTime.now(KST_ZONE)
val canViewAdultContent = canViewAdultContent(member)
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
val sectionType = newAndHotSectionType(visibility)
val snapshots = snapshotPort.findLatestSnapshots(sectionType, offset, limit)
val refreshedSnapshots = refreshMissingNewAndHotSnapshots(sectionType, snapshots, offset, limit)
val snapshots = findSnapshotsWithFallback(sectionType, offset, limit, fallbackNow)
return queryPort.findAudioCardsByIds(
refreshedSnapshots.map { it.targetId },
snapshots.map { it.targetId },
member.id,
canViewAdultContent,
now
@@ -113,29 +110,13 @@ class AudioRecommendationQueryService(
}
}
private fun refreshMissingNewAndHotSnapshots(
private fun findSnapshotsWithFallback(
sectionType: RecommendedSectionType,
snapshots: List<RecommendationSnapshotRecord>,
offset: Long,
limit: Int
offset: Long = 0,
limit: Int,
now: LocalDateTime
): List<RecommendationSnapshotRecord> {
if (snapshots.isNotEmpty()) return snapshots
val today = LocalDate.now(KST_ZONE)
val marker = redissonClient.getBucket<String>(newAndHotLazyRefreshMarkerKey(today))
if (!marker.setIfAbsent(LAZY_REFRESH_ATTEMPTED_VALUE, LAZY_REFRESH_MARKER_TTL)) {
return snapshots
}
runCatching {
snapshotRefreshService.refreshDailySnapshots()
}.onFailure { ex ->
marker.delete()
throw ex
}
return snapshotPort.findLatestSnapshots(sectionType, offset, limit)
}
private fun newAndHotLazyRefreshMarkerKey(date: LocalDate): String {
return "$LAZY_REFRESH_MARKER_KEY_PREFIX:$date"
return snapshotFallbackService.refreshIfMissing(sectionType, offset, limit, now)
}
private fun canViewAdultContent(member: Member?): Boolean {
@@ -152,9 +133,6 @@ class AudioRecommendationQueryService(
const val NEW_AND_HOT_HOME_LIMIT = 12
const val MOST_COMMENTED_AUDIO_LIMIT = 5
const val RECOMMENDED_AUDIO_LIMIT = 20
private const val LAZY_REFRESH_MARKER_KEY_PREFIX = "audio-recommendation:new-and-hot:lazy-refresh-attempted"
private const val LAZY_REFRESH_ATTEMPTED_VALUE = "1"
private val LAZY_REFRESH_MARKER_TTL: Duration = Duration.ofDays(2)
private val KST_ZONE: ZoneId = ZoneId.of("Asia/Seoul")
}
}