test #426

Merged
klaus merged 415 commits from test into main 2026-06-27 00:35:30 +00:00
2 changed files with 220 additions and 0 deletions
Showing only changes of commit 1c7bac3a73 - Show all commits

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")
}
}

View File

@@ -0,0 +1,84 @@
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.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
class AudioRecommendationSnapshotRefreshServiceTest {
private val snapshotPort = Mockito.mock(RecommendationSnapshotPort::class.java)
private val queryPort = Mockito.mock(AudioRecommendationQueryPort::class.java)
private val service = AudioRecommendationSnapshotRefreshService(snapshotPort, queryPort)
@Test
@DisplayName("일 배치는 KST 전날 23:59:59 기준으로 여섯 오디오 스냅샷을 교체한다")
fun shouldRefreshAllAudioSnapshotsWithKstPreviousDaySnapshotAt() {
val now = LocalDateTime.of(2026, 6, 24, 0, 0)
val snapshotAt = LocalDateTime.of(2026, 6, 23, 23, 59, 59)
val newAndHotWindowStart = LocalDateTime.of(2026, 6, 21, 0, 0)
val mostCommentedWindowStart = LocalDateTime.of(2026, 6, 17, 0, 0)
service.refreshDailySnapshots(now)
Mockito.verify(queryPort).findNewAndHotSnapshots(
newAndHotWindowStart,
snapshotAt,
AudioRecommendationVisibility.SAFE,
AudioRecommendationSnapshotRefreshService.NEW_AND_HOT_LIMIT
)
Mockito.verify(queryPort).findMostCommentedSnapshots(
mostCommentedWindowStart,
snapshotAt,
AudioRecommendationVisibility.ALL,
AudioRecommendationSnapshotRefreshService.MOST_COMMENTED_LIMIT
)
Mockito.verify(queryPort).findRecommendedAudioSnapshots(
mostCommentedWindowStart,
snapshotAt,
AudioRecommendationVisibility.SAFE,
AudioRecommendationSnapshotRefreshService.RECOMMENDED_AUDIO_LIMIT
)
Mockito.verify(snapshotPort).replaceSnapshots(RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE, snapshotAt, emptyList())
Mockito.verify(snapshotPort).replaceSnapshots(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, snapshotAt, emptyList())
Mockito.verify(snapshotPort).replaceSnapshots(RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE, snapshotAt, emptyList())
Mockito.verify(snapshotPort).replaceSnapshots(RecommendedSectionType.MOST_COMMENTED_AUDIO_ALL, snapshotAt, emptyList())
Mockito.verify(snapshotPort).replaceSnapshots(RecommendedSectionType.RECOMMENDED_AUDIO_SAFE, snapshotAt, emptyList())
Mockito.verify(snapshotPort).replaceSnapshots(RecommendedSectionType.RECOMMENDED_AUDIO_ALL, snapshotAt, emptyList())
}
@Test
@DisplayName("일 배치는 ZonedDateTime 입력의 zone과 무관하게 KST 날짜 경계 기준으로 스냅샷 시각을 계산한다")
fun shouldRefreshSnapshotsByKstBoundaryFromZonedDateTime() {
val now = ZonedDateTime.of(2026, 6, 24, 0, 0, 0, 0, ZoneId.of("Asia/Seoul"))
val snapshotAt = LocalDateTime.of(2026, 6, 23, 23, 59, 59)
val newAndHotWindowStart = LocalDateTime.of(2026, 6, 21, 0, 0)
val mostCommentedWindowStart = LocalDateTime.of(2026, 6, 17, 0, 0)
service.refreshDailySnapshots(now)
Mockito.verify(queryPort).findNewAndHotSnapshots(
newAndHotWindowStart,
snapshotAt,
AudioRecommendationVisibility.SAFE,
AudioRecommendationSnapshotRefreshService.NEW_AND_HOT_LIMIT
)
Mockito.verify(queryPort).findMostCommentedSnapshots(
mostCommentedWindowStart,
snapshotAt,
AudioRecommendationVisibility.ALL,
AudioRecommendationSnapshotRefreshService.MOST_COMMENTED_LIMIT
)
Mockito.verify(queryPort).findRecommendedAudioSnapshots(
mostCommentedWindowStart,
snapshotAt,
AudioRecommendationVisibility.SAFE,
AudioRecommendationSnapshotRefreshService.RECOMMENDED_AUDIO_LIMIT
)
}
}