feat(recommend): 추천 스냅샷 갱신 서비스를 추가한다

This commit is contained in:
2026-05-31 00:58:17 +09:00
parent 58e59c5cb4
commit 82d935e63f
3 changed files with 276 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.v2.recommend.adapter.out.scheduler
import kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshService
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
@Component
class RecommendationSnapshotScheduler(
private val refreshService: RecommendationSnapshotRefreshService
) {
@Scheduled(cron = "0 0 6 * * *", zone = "Asia/Seoul")
fun refreshDailySnapshots() {
refreshService.refreshDailySnapshots()
}
}

View File

@@ -0,0 +1,64 @@
package kr.co.vividnext.sodalive.v2.recommend.application
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeRecommendationQueryPort
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotPort
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotRecord
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneId
@Service
class RecommendationSnapshotRefreshService(
private val snapshotPort: RecommendationSnapshotPort,
private val queryPort: HomeRecommendationQueryPort
) {
@Transactional(readOnly = true)
fun getLatestSnapshots(sectionType: RecommendedSectionType): List<RecommendationSnapshotRecord> {
return snapshotPort.findLatestSnapshots(sectionType)
}
@Transactional
fun refreshDailySnapshots() {
refreshDailySnapshots(LocalDateTime.now())
}
@Transactional
fun refreshDailySnapshots(now: LocalDateTime) {
val snapshotAt = now
.atZone(UTC_ZONE)
.withZoneSameInstant(KST_ZONE)
.toLocalDate()
.minusDays(1)
.atTime(23, 59, 59)
val windowStart = snapshotAt.toLocalDate().minusDays(6).atStartOfDay()
replaceAiCharacterSnapshots(windowStart, snapshotAt)
replaceCheerCreatorSnapshots(windowStart, snapshotAt)
replacePopularCommunitySnapshots(windowStart, snapshotAt)
}
private fun replaceAiCharacterSnapshots(windowStart: LocalDateTime, snapshotAt: LocalDateTime) {
val snapshots = queryPort.findAiCharacterSnapshots(windowStart, snapshotAt, AI_CHARACTER_SNAPSHOT_LIMIT)
snapshotPort.replaceSnapshots(RecommendedSectionType.AI_CHARACTER, snapshotAt, snapshots)
}
private fun replaceCheerCreatorSnapshots(windowStart: LocalDateTime, snapshotAt: LocalDateTime) {
val snapshots = queryPort.findCheerCreatorSnapshots(windowStart, snapshotAt, CHEER_CREATOR_SNAPSHOT_LIMIT)
snapshotPort.replaceSnapshots(RecommendedSectionType.CHEER_CREATOR, snapshotAt, snapshots)
}
private fun replacePopularCommunitySnapshots(windowStart: LocalDateTime, snapshotAt: LocalDateTime) {
val snapshots = queryPort.findPopularCommunitySnapshots(windowStart, snapshotAt, POPULAR_COMMUNITY_SNAPSHOT_LIMIT)
snapshotPort.replaceSnapshots(RecommendedSectionType.POPULAR_COMMUNITY, snapshotAt, snapshots)
}
companion object {
private const val AI_CHARACTER_SNAPSHOT_LIMIT = 20
private const val CHEER_CREATOR_SNAPSHOT_LIMIT = 16
private const val POPULAR_COMMUNITY_SNAPSHOT_LIMIT = 20
private val UTC_ZONE: ZoneId = ZoneId.of("UTC")
private val KST_ZONE: ZoneId = ZoneId.of("Asia/Seoul")
}
}