From 5019c32145e46cb6aa8a4e6b14da6cf27c84103e Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 8 Jun 2026 15:23:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(ranking):=20=EC=A3=BC=EA=B0=84=20=EB=9E=AD?= =?UTF-8?q?=ED=82=B9=20=EA=B8=B0=EA=B0=84=20=EC=A0=95=EC=B1=85=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/CreatorRankingPeriodPolicy.kt | 42 +++++++++++++ .../domain/CreatorRankingPeriodPolicyTest.kt | 59 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicy.kt create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicyTest.kt 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) + } +}