feat(audio-recommendation): 추천 snapshot 갱신 서비스를 추가한다

This commit is contained in:
2026-06-23 21:05:26 +09:00
parent 70346b911f
commit 1c7bac3a73
2 changed files with 220 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
package kr.co.vividnext.sodalive.v2.audio.recommendation.application
import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioRecommendationVisibility
import kr.co.vividnext.sodalive.v2.audio.recommendation.port.out.AudioRecommendationQueryPort
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
@Service
class AudioRecommendationSnapshotRefreshService(
private val snapshotPort: RecommendationSnapshotPort,
private val queryPort: AudioRecommendationQueryPort
) {
private val log = LoggerFactory.getLogger(javaClass)
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun refreshDailySnapshots() {
refreshDailySnapshots(ZonedDateTime.now(KST_ZONE))
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun refreshDailySnapshots(now: LocalDateTime) {
refreshDailySnapshots(now.atZone(KST_ZONE))
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
fun refreshDailySnapshots(now: ZonedDateTime) {
val startedAt = System.currentTimeMillis()
val snapshotAt = snapshotAt(now)
val newAndHotWindowStart = windowStart(snapshotAt, days = 3)
val mostCommentedWindowStart = windowStart(snapshotAt, days = 7)
val recommendedWindowStart = mostCommentedWindowStart
runCatching {
replaceNewAndHotSnapshots(newAndHotWindowStart, snapshotAt, AudioRecommendationVisibility.SAFE)
replaceNewAndHotSnapshots(newAndHotWindowStart, snapshotAt, AudioRecommendationVisibility.ALL)
replaceMostCommentedSnapshots(mostCommentedWindowStart, snapshotAt, AudioRecommendationVisibility.SAFE)
replaceMostCommentedSnapshots(mostCommentedWindowStart, snapshotAt, AudioRecommendationVisibility.ALL)
replaceRecommendedAudioSnapshots(recommendedWindowStart, snapshotAt, AudioRecommendationVisibility.SAFE)
replaceRecommendedAudioSnapshots(recommendedWindowStart, snapshotAt, AudioRecommendationVisibility.ALL)
}.onSuccess {
log.info(
"event=audio_recommendation_snapshot_refresh_success snapshotAt={} elapsedMs={}",
snapshotAt,
System.currentTimeMillis() - startedAt
)
}.onFailure { ex ->
log.warn(
"event=audio_recommendation_snapshot_refresh_failure snapshotAt={} elapsedMs={} error={}",
snapshotAt,
System.currentTimeMillis() - startedAt,
ex.message,
ex
)
throw ex
}
}
private fun replaceNewAndHotSnapshots(
windowStart: LocalDateTime,
snapshotAt: LocalDateTime,
visibility: AudioRecommendationVisibility
) {
val sectionType = visibility.newAndHotSectionType()
val snapshots = queryPort.findNewAndHotSnapshots(windowStart, snapshotAt, visibility, NEW_AND_HOT_LIMIT)
snapshotPort.replaceSnapshots(sectionType, snapshotAt, snapshots)
}
private fun replaceMostCommentedSnapshots(
windowStart: LocalDateTime,
snapshotAt: LocalDateTime,
visibility: AudioRecommendationVisibility
) {
val sectionType = visibility.mostCommentedSectionType()
val snapshots = queryPort.findMostCommentedSnapshots(windowStart, snapshotAt, visibility, MOST_COMMENTED_LIMIT)
snapshotPort.replaceSnapshots(sectionType, snapshotAt, snapshots)
}
private fun replaceRecommendedAudioSnapshots(
windowStart: LocalDateTime,
snapshotAt: LocalDateTime,
visibility: AudioRecommendationVisibility
) {
val sectionType = visibility.recommendedAudioSectionType()
val snapshots = queryPort.findRecommendedAudioSnapshots(windowStart, snapshotAt, visibility, RECOMMENDED_AUDIO_LIMIT)
snapshotPort.replaceSnapshots(sectionType, snapshotAt, snapshots)
}
private fun snapshotAt(now: ZonedDateTime): LocalDateTime {
val nowKst = now
.withZoneSameInstant(KST_ZONE)
return nowKst.toLocalDate()
.minusDays(1)
.atTime(23, 59, 59)
}
private fun windowStart(snapshotAt: LocalDateTime, days: Long): LocalDateTime {
return snapshotAt.toLocalDate()
.minusDays(days - 1)
.atStartOfDay()
}
private fun AudioRecommendationVisibility.newAndHotSectionType(): RecommendedSectionType {
return when (this) {
AudioRecommendationVisibility.SAFE -> RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE
AudioRecommendationVisibility.ALL -> RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL
}
}
private fun AudioRecommendationVisibility.mostCommentedSectionType(): RecommendedSectionType {
return when (this) {
AudioRecommendationVisibility.SAFE -> RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE
AudioRecommendationVisibility.ALL -> RecommendedSectionType.MOST_COMMENTED_AUDIO_ALL
}
}
private fun AudioRecommendationVisibility.recommendedAudioSectionType(): RecommendedSectionType {
return when (this) {
AudioRecommendationVisibility.SAFE -> RecommendedSectionType.RECOMMENDED_AUDIO_SAFE
AudioRecommendationVisibility.ALL -> RecommendedSectionType.RECOMMENDED_AUDIO_ALL
}
}
companion object {
const val NEW_AND_HOT_LIMIT = 12
const val MOST_COMMENTED_LIMIT = 5
const val RECOMMENDED_AUDIO_LIMIT = 10
private val KST_ZONE: ZoneId = ZoneId.of("Asia/Seoul")
}
}