diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicy.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicy.kt new file mode 100644 index 00000000..23dd8ae6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicy.kt @@ -0,0 +1,42 @@ +package kr.co.vividnext.sodalive.v2.ranking.domain + +import java.time.DayOfWeek +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.temporal.TemporalAdjusters + +class CreatorRankingPeriodPolicy { + fun resolveLastCompletedWeek(now: ZonedDateTime): CreatorRankingPeriod { + val nowKst = now.withZoneSameInstant(KST_ZONE) + val thisWeekMonday = nowKst.toLocalDate() + .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) + .atStartOfDay() + return CreatorRankingPeriod( + startInclusiveKst = thisWeekMonday.minusWeeks(1), + endExclusiveKst = thisWeekMonday + ) + } + + fun toUtcRange(period: CreatorRankingPeriod): CreatorRankingUtcRange { + return CreatorRankingUtcRange( + startInclusiveUtc = period.startInclusiveKst.atZone(KST_ZONE).withZoneSameInstant(UTC_ZONE).toLocalDateTime(), + endExclusiveUtc = period.endExclusiveKst.atZone(KST_ZONE).withZoneSameInstant(UTC_ZONE).toLocalDateTime() + ) + } + + companion object { + private val KST_ZONE: ZoneId = ZoneId.of("Asia/Seoul") + private val UTC_ZONE: ZoneId = ZoneId.of("UTC") + } +} + +data class CreatorRankingPeriod( + val startInclusiveKst: LocalDateTime, + val endExclusiveKst: LocalDateTime +) + +data class CreatorRankingUtcRange( + val startInclusiveUtc: LocalDateTime, + val endExclusiveUtc: LocalDateTime +) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicyTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicyTest.kt new file mode 100644 index 00000000..331a110a --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicyTest.kt @@ -0,0 +1,59 @@ +package kr.co.vividnext.sodalive.v2.ranking.domain + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZonedDateTime + +class CreatorRankingPeriodPolicyTest { + private val policy = CreatorRankingPeriodPolicy() + + @Test + @DisplayName("월요일 KST 기준 지난 주 월요일 00시 이상 이번 주 월요일 00시 미만 기간을 산출한다") + fun shouldResolveLastCompletedWeekByKstMonday() { + val now = ZonedDateTime.of(2026, 6, 8, 6, 0, 0, 0, ZoneId.of("Asia/Seoul")) + + val period = policy.resolveLastCompletedWeek(now) + + assertEquals(LocalDateTime.of(2026, 6, 1, 0, 0), period.startInclusiveKst) + assertEquals(LocalDateTime.of(2026, 6, 8, 0, 0), period.endExclusiveKst) + } + + @Test + @DisplayName("기간 산출은 서버 timezone UTC와 무관하게 KST 기준으로 계산한다") + fun shouldResolveLastCompletedWeekIndependentOfServerTimezone() { + val now = ZonedDateTime.of(2026, 6, 7, 21, 0, 0, 0, ZoneId.of("UTC")) + + val period = policy.resolveLastCompletedWeek(now) + + assertEquals(LocalDateTime.of(2026, 6, 1, 0, 0), period.startInclusiveKst) + assertEquals(LocalDateTime.of(2026, 6, 8, 0, 0), period.endExclusiveKst) + } + + @Test + @DisplayName("연도 경계를 넘어도 KST 기준 지난 완료 주차를 산출한다") + fun shouldResolveLastCompletedWeekAcrossYearBoundary() { + val now = ZonedDateTime.of(2026, 1, 1, 12, 0, 0, 0, ZoneId.of("Asia/Seoul")) + + val period = policy.resolveLastCompletedWeek(now) + + assertEquals(LocalDateTime.of(2025, 12, 22, 0, 0), period.startInclusiveKst) + assertEquals(LocalDateTime.of(2025, 12, 29, 0, 0), period.endExclusiveKst) + } + + @Test + @DisplayName("KST 기간은 DB 조회용 UTC LocalDateTime 이상/미만 조건으로 변환한다") + fun shouldConvertKstPeriodToUtcRange() { + val period = CreatorRankingPeriod( + startInclusiveKst = LocalDateTime.of(2026, 6, 1, 0, 0), + endExclusiveKst = LocalDateTime.of(2026, 6, 8, 0, 0) + ) + + val utcRange = policy.toUtcRange(period) + + assertEquals(LocalDateTime.of(2026, 5, 31, 15, 0), utcRange.startInclusiveUtc) + assertEquals(LocalDateTime.of(2026, 6, 7, 15, 0), utcRange.endExclusiveUtc) + } +}