diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRankingSchedulePolicy.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRankingSchedulePolicy.kt new file mode 100644 index 00000000..ca74d0d3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRankingSchedulePolicy.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.v2.content.ranking.domain + +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.ZoneId + +class AudioRankingSchedulePolicy { + fun resolveVisibleFromAt(aggregationEndAtKst: LocalDateTime): LocalDateTime { + return aggregationEndAtKst.toLocalDate() + .atTime(VISIBLE_FROM_TIME) + .atZone(KST_ZONE) + .withZoneSameInstant(UTC_ZONE) + .toLocalDateTime() + } + + fun isVisible(visibleFromAtUtc: LocalDateTime, nowUtc: LocalDateTime): Boolean { + return !nowUtc.isBefore(visibleFromAtUtc) + } + + companion object { + private val KST_ZONE: ZoneId = ZoneId.of("Asia/Seoul") + private val UTC_ZONE: ZoneId = ZoneId.of("UTC") + private val VISIBLE_FROM_TIME: LocalTime = LocalTime.of(9, 0) + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRankingSchedulePolicyTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRankingSchedulePolicyTest.kt new file mode 100644 index 00000000..8652182b --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/domain/AudioRankingSchedulePolicyTest.kt @@ -0,0 +1,42 @@ +package kr.co.vividnext.sodalive.v2.content.ranking.domain + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.time.LocalDateTime + +class AudioRankingSchedulePolicyTest { + private val policy = AudioRankingSchedulePolicy() + + @Test + @DisplayName("집계 종료일과 같은 KST 날짜 09시를 UTC LocalDateTime으로 변환해 공개 시각을 산출한다") + fun shouldResolveVisibleFromAtAsSameKstDateNineAmConvertedToUtc() { + val aggregationEndAtKst = LocalDateTime.of(2026, 6, 8, 0, 0) + + val visibleFromAtUtc = policy.resolveVisibleFromAt(aggregationEndAtKst) + + assertEquals(LocalDateTime.of(2026, 6, 8, 0, 0), visibleFromAtUtc) + } + + @Test + @DisplayName("09시 KST 이전에는 새 스냅샷을 공개하지 않는다") + fun shouldNotBeVisibleBeforeNineAmKst() { + val visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0) + val nowUtc = LocalDateTime.of(2026, 6, 7, 23, 59) + + val visible = policy.isVisible(visibleFromAtUtc, nowUtc) + + assertFalse(visible) + } + + @Test + @DisplayName("09시 KST 경계와 이후에는 새 스냅샷을 공개한다") + fun shouldBeVisibleAtAndAfterNineAmKst() { + val visibleFromAtUtc = LocalDateTime.of(2026, 6, 8, 0, 0) + + assertTrue(policy.isVisible(visibleFromAtUtc, LocalDateTime.of(2026, 6, 8, 0, 0))) + assertTrue(policy.isVisible(visibleFromAtUtc, LocalDateTime.of(2026, 6, 8, 0, 1))) + } +}