타임존
TimeZoneAdapter, wall-clock↔instant 변환, DST 경계에서의 렌더/반복 정확성
reopt designUpdated
1. 시간 인코딩 3종
opt-calendar은 시간을 세 가지로 인코딩합니다. 이 분리 덕분에 native Intl만으로 모든 표시 작업을 처리하며, 레포에 별도 날짜 라이브러리가 없습니다.
- Timed 이벤트 — 오프셋 포함 절대 ISO datetime(
ISODateTime), 예"2026-07-01T09:00:00-04:00". 특정 순간을 가리킵니다. - All-day 이벤트 — 타임존 없는 floating ISO date(
ISODate), 예"2026-07-13". 뷰어/spec 타임존에서 해석하며end는 배타적입니다. - 가용성(availability) — wall-clock
HH:mm(WallClockTime) + 스케줄의 타임존.WeeklyAvailabilityRule이 요일별 창을 정의하고 스케줄 tz에서 해석됩니다.
// 1) Timed — 오프셋 포함 절대 instant
spec.events.sync = {
id: "sync",
title: "주간 싱크",
start: "2026-07-01T09:00:00-04:00",
end: "2026-07-01T10:00:00-04:00",
};
// 2) All-day — 타임존 없는 floating date, end 배타적
spec.events.offsite = {
id: "offsite",
title: "오프사이트",
start: "2026-07-13",
end: "2026-07-15", // 7/14까지 (end 배타적)
allDay: true,
};
// 3) Availability — wall-clock HH:mm + 스케줄 tz
spec.availability.default = {
id: "default",
name: "업무 시간",
timeZone: "America/New_York",
rules: [
{ day: 1, start: "09:00", end: "17:00" }, // 월 09:00–17:00 (스케줄 tz)
],
};2. TimeZoneAdapter 계약
타임존 해석은 TimeZoneAdapter seam 뒤에 있습니다. 기본값 intlTimeZoneAdapter는 native Intl.DateTimeFormat 기반이라 무거운 의존성이 없습니다. 계약은 그리드가 필요로 하는 세 가지입니다.
format(iso, tz?, opts?, locale?)— 타임존에서 ISO 값의 지역화된 표시 문자열getParts(instant, tz)→ZonedParts— 절대 instant의 wall-clock 파트(year / month / day / hour / minute / weekday)wallClockToInstant(date, time, tz)→Date— 특정 타임존의 wall-clock date+time을 절대 instant로
DST-grade 정확성이 필요하면 같은 seam으로 Temporal polyfill 기반 어댑터를 주입할 수 있습니다(hard dependency 아님). expandEvents(spec, range, engine?, adapter?)의 4번째 인자, 그리고 event-time 헬퍼들의 마지막 인자로 넘깁니다.
import { intlTimeZoneAdapter } from "@reopt-ai/opt-calendar";
// wall-clock → 절대 instant (해당 날짜의 tz 오프셋을 반영)
const instant = intlTimeZoneAdapter.wallClockToInstant(
"2026-07-01",
"09:00",
"America/New_York",
);
instant.toISOString(); // "2026-07-01T13:00:00.000Z" (EDT, UTC-4)
// 절대 instant → 특정 tz의 wall-clock 파트
const parts = intlTimeZoneAdapter.getParts(instant, "America/New_York");
parts.hour; // 9
parts.minute; // 0
parts.weekday; // 3 (수요일)3. Calendar timeZone prop 정렬
<Calendar timeZone="..." />가 넘기는 표시 타임존은 세 가지를 한 기준으로 정렬합니다.
- 뷰 위치 계산 — timed 이벤트가 day/week 그리드의 어느 줄에 그려질지(
getEventDayMinutes가 tz에서 wall-clock 해석) - 반복 전개 — occurrence를 master tz의 wall-clock으로 materialize
- 드래그 수식 — 이동·리사이즈를 wall-clock 분으로 계산해 다시 절대 instant로(
computeEventMove/computeEventResize)
같은 절대 instant라도 표시 타임존이 다르면 다른 줄에 그려집니다. event-time 헬퍼는 tz를 주면 어댑터로 wall-clock을 해석하고, 안 주면 문자열에서 textual하게 읽는 same-tz fast path로 폴백합니다.
import {
getEventDayMinutes,
computeEventMove,
} from "@reopt-ai/opt-calendar";
const event = {
id: "sync",
title: "싱크",
start: "2026-07-01T20:00:00Z",
end: "2026-07-01T21:00:00Z",
};
// 표시 tz에서 그리드 위치(분) 해석 — 같은 instant, 다른 tz → 다른 날/줄
getEventDayMinutes(event, "America/New_York");
// → { dayKey: "2026-07-01", startMin: 960, endMin: 1020 } (16:00)
getEventDayMinutes(event, "Asia/Seoul");
// → { dayKey: "2026-07-02", startMin: 300, endMin: 360 } (05:00 다음날)
// 드래그: +30분 이동을 wall-clock으로 계산 후 절대 instant로 환산
computeEventMove(event, 30, "America/New_York");
// → { start: "2026-07-01T20:30:00.000Z", end: "2026-07-01T21:30:00.000Z" }4. DST 정확성
반복 occurrence는 master 타임존의 절대 instant로 materialize됩니다. expandEvents는 master의 wall-clock HH:mm를 뽑아, 각 occurrence 날짜마다 wallClockToInstant로 그 날짜의 오프셋을 다시 계산합니다. 그래서 "매일 09:00" 시리즈는 DST 경계를 지나도 9시를 유지합니다 — 바뀌는 것은 wall-clock이 아니라 저장되는 오프셋입니다.
import { expandEvents } from "@reopt-ai/opt-calendar";
// master: America/New_York, 매일 09:00
spec.timeZone = "America/New_York";
spec.events.standup = {
id: "standup",
title: "데일리 스탠드업",
start: "2026-03-07T09:00:00-05:00", // EST (UTC-5)
end: "2026-03-07T09:15:00-05:00",
rrule: "FREQ=DAILY",
};
// DST 봄 전환(미국 2026-03-08)을 가로질러 전개
const instances = expandEvents(spec, {
start: new Date("2026-03-07"),
end: new Date("2026-03-10"),
});
// wall-clock 09:00은 유지되고, 절대 오프셋만 -05:00 → -04:00으로 이동
instances[0].start; // "2026-03-07T14:00:00.000Z" (09:00 EST)
instances[1].start; // "2026-03-08T13:00:00.000Z" (09:00 EDT — 오프셋만 이동)
instances[2].start; // "2026-03-09T13:00:00.000Z" (09:00 EDT)canonical occurrenceStart 키는 wall-clock을 textual하게 보존하므로 EXDATE·override 매칭에 안정적으로 쓰이고, materialize된 start/end만 날짜별 오프셋을 반영합니다. 기본 intlTimeZoneAdapter로도 위 결과가 정확하며, 더 엄격한 DST 처리가 필요하면 Temporal 어댑터를 주입하세요.