타입 레퍼런스
CalendarSpec, CalendarEvent, Booking, EventType, JsonPatchOp 등 핵심 타입 정의
reopt design업데이트
1. CalendarSpec & 5-맵
CalendarSpec은 다섯 개의 id-keyed 맵과 기본 timeZone, version으로 이루어진 평면 문서입니다. RFC 6902 JSON Patch로만 편집되며, additive-only 불변식을 지킵니다(필드 제거·용도 변경 금지, top-level 맵 이름 변경 금지).
스칼라 별칭 · enum
| 타입 | 정의 | 설명 |
|---|---|---|
TimeZoneId | string | IANA 타임존 id (예: Asia/Seoul) |
ISODateTime | string | 오프셋 포함 ISO 8601 절대 시각(timed 이벤트) |
ISODate | string | 타임존 없는 floating 날짜 YYYY-MM-DD(all-day) |
RRuleString | string | RFC 5545 RRULE 본문(RRULE: 접두어 제외) |
WallClockTime | string | 로컬 벽시계 HH:mm(가용성 규칙, 스케줄 tz 기준) |
Weekday | 0 | 1 | … | 6 | 요일(0=일요일, Date.getDay() 일치) |
CalendarViewMode | "month" | "week" | "day" | "agenda" | "timeline" | 뷰 모드 |
CalendarMode | "interactive" | "stream" | "readonly" | 상호작용 모드(stream=AI 중 포인터 잠금) |
CalendarVariant | "events" | "booking" | 제품 지향 변형 |
TimeGridOptions | { startHour?, endHour?, hourHeight?, snapMinutes?, maxBodyHeight?, scrollToNow? } | 주/일 타임그리드 표시 옵션(Calendar.timeGrid) |
EventStatus | "confirmed" | "tentative" | "cancelled" | 이벤트 확정 상태 |
AttendeeStatus | "needs-action" | "accepted" | … | 참석자 RSVP 상태 |
BookingStatus | "pending" | "confirmed" | "cancelled" | "rejected" | 예약 라이프사이클 상태 |
CalendarSpec
interface CalendarSpec {
/** spec 기본 IANA 타임존 (필수). */
timeZone: TimeZoneId;
/** 시간 축 — 모든 이벤트 (id → CalendarEvent). */
events: Record<string, CalendarEvent>;
/** 사이드바 — 캘린더/리소스 (id → Calendar). */
calendars: Record<string, Calendar>;
/** 명명된 가용성 스케줄 (id → AvailabilitySchedule). */
availability: Record<string, AvailabilitySchedule>;
/** 예약 카탈로그 — 예약 가능 타입 (id → EventType). */
eventTypes: Record<string, EventType>;
/** 예약 큐 — 예약 레코드 (id → Booking). */
bookings: Record<string, Booking>;
/** 낙관적 동시성 버전 (mutation마다 증가). */
version: number;
}
/** 5개 top-level 맵의 키. */
type CalendarContainer =
| "events"
| "calendars"
| "availability"
| "eventTypes"
| "bookings";
/** 컨테이너 + id로 엔티티를 가리키는 참조. */
interface EntityRef {
container: CalendarContainer;
id: string;
}5개의 맵은 각각 독립 축을 모델링합니다: events=시간 축, calendars=사이드바, availability=명명된 스케줄, eventTypes=예약 카탈로그, bookings=예약 큐. 단일 kind 맵이 아니라 분리했기에 패치 경로가 자기 설명적입니다(/events/{id}/start).
Calendar (export명 CalendarResource)
calendars 맵의 엔티티입니다. 이름 충돌을 피하려 Calendar 컴포넌트와 구분해 CalendarResource로 export됩니다.
import type { CalendarResource } from "@reopt-ai/opt-calendar";
interface Calendar {
id: string;
name: string;
color?: string; // CSS 색상 또는 opt-ui 토큰
hidden?: boolean; // 렌더에서 제외하되 spec엔 유지
primary?: boolean; // 기본 캘린더 여부
timeZone?: TimeZoneId;
description?: string;
meta?: Record<string, unknown>;
}2. CalendarEvent
이벤트의 필수 핵심은 id·title·start·end뿐이며 나머지는 모두 선택입니다. timed 이벤트는 오프셋 포함 ISODateTime, all-day는 floating ISODate를 저장하고 end는 배타적입니다(RFC 5545).
interface CalendarEvent {
// === 필수 핵심 ===
id: string;
title: string;
start: ISODateTime | ISODate; // inclusive
end: ISODateTime | ISODate; // exclusive
// === 시간/소속 ===
allDay?: boolean; // true면 start/end는 ISODate
timeZone?: TimeZoneId; // 이벤트별 tz 오버라이드
calendarId?: string; // 소속 캘린더 (→ calendars)
location?: string;
description?: string;
status?: EventStatus; // 기본 "confirmed"
attendees?: EventAttendee[];
// === 반복 (series master) ===
rrule?: RRuleString; // 시리즈 마스터의 반복 규칙
rdates?: ISODateTime[]; // 추가 1회성 occurrence (RDATE)
exdates?: ISODateTime[]; // 제외 occurrence (EXDATE)
// === 분리 override (detached) ===
recurringEventId?: string; // 마스터 이벤트 id (→ events)
originalStart?: ISODateTime | ISODate; // 대체되는 occurrence 시작
// === 예약 연결 ===
bookingId?: string; // 이 이벤트를 materialize한 예약 (→ bookings)
// === 확장 ===
meta?: Record<string, unknown>; // 네임스페이스 통합 데이터 (meta.kind 등)
}반복 시리즈의 한 occurrence만 바꾸려면 그 시작을 exdates에 넣고, recurringEventId+originalStart를 가진 분리 override 이벤트를 추가합니다.
EventAttendee
type EventStatus = "confirmed" | "tentative" | "cancelled";
type AttendeeStatus =
| "needs-action"
| "accepted"
| "declined"
| "tentative";
interface EventAttendee {
email: string; // 신원
name?: string;
status?: AttendeeStatus; // 기본 "needs-action"
optional?: boolean; // 참석 선택 여부
organizer?: boolean; // 주최자 여부
}3. 예약 타입
예약은 "레코드 + materialized 이벤트" 모델입니다. EventType+AvailabilitySchedule은 공급 정의로 요청 시 BookingSlot을 생성하고(그리드에 그리지 않음), Booking은 확정 시 CalendarEvent로 materialize됩니다.
EventType
interface EventType {
id: string;
slug: string; // URL 슬러그 (예: "30min")
title: string;
durationMinutes: number;
description?: string;
availabilityId?: string; // 가용성 스케줄 (→ availability)
calendarId?: string; // 확정 예약이 materialize될 캘린더
timeZone?: TimeZoneId;
bufferBeforeMinutes?: number;
bufferAfterMinutes?: number;
minimumNoticeMinutes?: number; // 최소 예약 여유
bookingWindowDays?: number; // 며칠 앞까지 예약 허용
seatsPerSlot?: number; // >1이면 그룹 이벤트. 기본 1
slotIntervalMinutes?: number; // 슬롯 시작 간격. 생략 시 duration
maxBookingsPerDay?: number; // 하루 최대 접수 건수
locationKind?: string; // 예: "google-meet", "in-person"
color?: string;
hidden?: boolean;
meta?: Record<string, unknown>;
}AvailabilitySchedule
/** 주간 반복 가용성 창(벽시계, 스케줄 tz 기준). */
interface WeeklyAvailabilityRule {
day: Weekday;
start: WallClockTime; // inclusive
end: WallClockTime; // exclusive
}
/** 특정 날짜 override(휴일/특별 영업시간). */
interface AvailabilityDateOverride {
date: ISODate;
windows: Array<{ start: WallClockTime; end: WallClockTime }>; // [] = 종일 휴무
}
/** 명명된 예약 가능 스케줄 (Cal.com "availability"). */
interface AvailabilitySchedule {
id: string;
name: string;
timeZone?: TimeZoneId; // 벽시계 규칙 해석 tz. 생략 시 spec tz
rules: WeeklyAvailabilityRule[];
overrides?: AvailabilityDateOverride[];
meta?: Record<string, unknown>;
}BookingSlot (파생)
이벤트 타입 + 가용성에서 이벤트/예약을 뺀 파생 슬롯입니다. spec에 저장되지 않습니다.
interface BookingSlot {
eventTypeId: string;
start: ISODateTime;
end: ISODateTime;
seatsRemaining?: number; // 그룹 타입의 잔여 좌석
}Booking
type BookingStatus = "pending" | "confirmed" | "cancelled" | "rejected";
interface BookingAttendee {
name: string;
email: string;
timeZone?: TimeZoneId;
notes?: string;
}
interface Booking {
id: string;
eventTypeId: string; // 예약 가능 타입 (→ eventTypes)
start: ISODateTime; // 확정 시작 (절대)
end: ISODateTime;
status: BookingStatus;
attendee: BookingAttendee; // 주 예약자
guests?: BookingAttendee[];
timeZone?: TimeZoneId;
eventId?: string; // materialize된 이벤트 (↔ CalendarEvent.bookingId)
createdAt?: ISODateTime;
meta?: Record<string, unknown>;
}라이프사이클(create/confirm/cancel/reschedule)과 confirm→materialize 흐름은 예약 · 가용성 문서를 참고하세요.
4. 스토어 · 패치
드래그·리사이즈·수동 편집·AI 스트림이 모두 store.applyPatch(ops) 한 경로로 흐르며 하나의 undo 히스토리를 공유합니다. 편집 단위는 RFC 6902 JSON Patch op입니다.
JsonPatchOp
interface JsonPatchOp {
/** 연산 종류. */
op: "add" | "remove" | "replace" | "move" | "copy" | "test";
/** spec 안으로의 RFC 6901 JSON Pointer (예: /events/e1/start). */
path: string;
/** add/replace/test의 값. */
value?: unknown;
/** move/copy의 소스 포인터. */
from?: string;
}CalendarStore
interface CalendarStore {
/** 현재 spec (다음 mutation 전까지 안정 참조). */
getSpec: () => CalendarSpec;
/** RFC 6902 패치 적용. */
applyPatch: (ops: JsonPatchOp[], options?: ApplyCalendarPatchOptions) => void;
/** 변경 구독. 해제 함수를 반환 (useSyncExternalStore 호환). */
subscribe: (listener: () => void) => () => void;
undo: () => void;
redo: () => void;
canUndo: () => boolean;
canRedo: () => boolean;
/** 마지막 mutation이 바꾼 "container/id" 키. */
getLastChangedIds: () => ReadonlySet<string>;
/** 외부 스트림 시작 — 개별 applyPatch가 히스토리를 건너뜀. */
beginExternalStream: () => void;
/** 외부 스트림 종료 — 전체를 하나의 undo 엔트리로 기록. */
endExternalStream: () => void;
isExternalStreaming: () => boolean;
/** forceRender로 증가하는 엔티티별 버전 카운터 (리마운트 키용). */
getEntityVersions: () => ReadonlyMap<string, number>;
}ApplyCalendarPatchOptions
interface ApplyCalendarPatchOptions {
/** 변경 엔티티의 버전 카운터를 올려 뷰 리마운트를 강제(예: 드래그 중 AI 패치). */
forceRender?: boolean;
/** 텔레메트리/디버깅용 origin 태그(예: "drag", "ai", "remote"). */
source?: string;
}CalendarValidationResult
validateCalendarSpec(spec)가 구조 무결성(id/키 일치, end<start, 매달린 상호참조)을 검사해 반환합니다.
interface CalendarValidationError {
container: string; // 문제 엔티티가 사는 컨테이너
id: string; // 엔티티 id
message: string; // 사람이 읽는 메시지
}
interface CalendarValidationResult {
valid: boolean; // 오류가 없으면 true
errors: CalendarValidationError[];
}5. 엔진 · 카탈로그 타입
반복 전개와 타임존 해석은 주입 가능한 seam입니다. 기본 minimalRecurrenceEngine·intlTimeZoneAdapter는 zero-dep이며, 같은 인터페이스로 npm rrule이나 Temporal 폴리필을 주입할 수 있습니다.
RecurrenceEngine
/** 반달 열림 전개 윈도우 [start, end) (일 정렬). */
interface RecurrenceRange {
start: Date;
end: Date;
}
/** 주입 가능한 반복 전개기. */
interface RecurrenceEngine {
/** 범위 안, 마스터의 occurrence 시작 문자열들. */
expand: (master: CalendarEvent, range: RecurrenceRange) => string[];
}TimeZoneAdapter
/** 타임존에서 어떤 순간의 벽시계 파트. */
interface ZonedParts {
year: number;
month: number;
day: number;
hour: number;
minute: number;
weekday: Weekday;
}
/** 주입 가능한 타임존 어댑터. */
interface TimeZoneAdapter {
/** tz에서 ISO 값의 지역화 표시 문자열. */
format: (
iso: ISODateTime | ISODate,
tz?: TimeZoneId,
opts?: Intl.DateTimeFormatOptions,
locale?: string,
) => string;
/** 절대 순간의 tz별 벽시계 파트. */
getParts: (instant: Date, tz: TimeZoneId) => ZonedParts;
/** tz에서 특정 날짜의 벽시계 시간 → 절대 순간. */
wallClockToInstant: (
date: ISODate,
time: WallClockTime,
tz: TimeZoneId,
) => Date;
}CalendarEventInstance
expandEvents()가 반환하는, 인스턴스 시각에 놓인 구체 occurrence입니다. CalendarEvent를 확장합니다.
interface CalendarEventInstance extends CalendarEvent {
/** 이 occurrence의 고유 키 (id@canonicalStart). */
instanceKey: string;
/** 엔진의 정규 occurrence 시작 (EXDATE/override 매칭용). */
occurrenceStart?: string;
/** 반복 규칙으로 생성됨(독립 이벤트와 구분). */
isRecurring: boolean;
/** 분리(override)된 occurrence 여부. */
isOverride: boolean;
/** 시리즈 마스터 id (반복 인스턴스/override). */
masterId?: string;
}CalendarCatalog
엔티티 종류 레지스트리로, AI 시스템 프롬프트·structured-output JSON Schema·attr 검증을 구동합니다. 각 kind는 인스턴스가 착지할 top-level 맵(container)을 선언합니다.
/** 스키마 필드 타입 힌트(iso-*, rrule은 프롬프트에서 의미 전달). */
type SchemaFieldType =
| "string"
| "number"
| "boolean"
| "string[]"
| "iso-datetime"
| "iso-date"
| "rrule"
| "object"
| "object[]";
/** 필드 정의 — 순수 타입, 또는 default를 가진 객체. */
type SchemaFieldDef =
| SchemaFieldType
| { type: SchemaFieldType; default?: unknown };
/** 카탈로그의 엔티티 kind 하나. */
interface EntityDefinition {
kind: string; // 예: "event", "all-day-event", "booking"
container: CalendarContainer;
attrsSchema: Record<string, SchemaFieldDef>;
zodSchema?: ZodLikeSchema; // 런타임 검증용(선택)
prompt?: string;
examples?: string[]; // 프롬프트에 주입할 NDJSON 예제 라인
}
/** 함수 없는 직렬화 엔티티 스키마. */
interface EntitySchema {
kind: string;
container: CalendarContainer;
attrsSchema: Record<string, SchemaFieldDef>;
prompt?: string;
}
/** 직렬화된 카탈로그(JSON/서버 안전). */
interface CatalogSchema {
entities: Record<string, EntitySchema>;
}
/** 카탈로그 레지스트리 + 생성기. */
interface CalendarCatalog {
entities: Record<string, EntityDefinition>;
prompt: (options?: { protocol?: "json-patch" }) => string;
jsonSchema: () => Record<string, unknown>;
toJSON: () => CatalogSchema;
validateAttrs: (spec: CalendarSpec) => AttrsValidationResult;
}