reopt designreopt design
DocsExploreToolsPricingBuilder
시작하기
시작하기
Playground
핵심 개념
핵심 개념
Calendar
Calendar 개요
이벤트 편집
반복 일정
타임존
예약 · 가용성
AI 스트리밍
원격 소스
앱 조합
구축·운영
스킬
릴리즈 노트
레퍼런스
Hook 레퍼런스
타입 레퍼런스
Oopt-calendar
reopt designreopt design

AI 시대를 위한 디자인 시스템

  • 문서
  • 가격
  • 릴리즈 노트
  • GitHub
  • 서비스 약관
  • 개인정보처리방침

© 2026 reopt-ai. All rights reserved.

레퍼런스
  1. 문서
  2. /
  3. 레퍼런스
  4. /
  5. Hook 레퍼런스

Hook 레퍼런스

useCalendar, useCalendarRender, useCalendarNavigation, useCalendarRemoteSource, useCalendarSuggestion 시그니처와 예제

reopt design · 업데이트 2026년 7월 2일

시작하기핵심 개념개요이벤트 편집반복 일정타임존예약 · 가용성AI 스트리밍원격 소스앱 조합스킬릴리즈 노트Hook 레퍼런스타입 레퍼런스

1. Provider 컨텍스트 훅

아래 다섯 훅은 CalendarProvider 컨텍스트를 읽습니다. <Calendar>가 내부에서 provider를 렌더하므로, 그 아래 뷰·컴포넌트에서 바로 호출할 수 있습니다. 커스텀 뷰를 직접 조합할 땐 <CalendarProvider>로 감싸세요. Provider 밖에서 호출하면 예외를 던집니다.

Hook반환설명
useCalendarContext()CalendarContextValue컨텍스트 원본(store · catalog · timeZone · weekStartsOn · messages · onEventActivate · suggestion)
useCalendarSpec()CalendarSpec라이브 spec 구독. 모든 mutation마다 리렌더 (useSyncExternalStore)
useCalendarRender()CalendarRender뷰가 렌더할 spec. AI 초안이 활성이면 draft 오버레이를 반환
useCalendarStore()CalendarStore하부 store(안정 참조). applyPatch/undo/redo 접근
useCalendarMessages()CalendarMessages해석된 UI 메시지(로케일 오버라이드 반영)

CalendarRender

useCalendarRender()는 라이브 spec과 AI 초안 오버레이를 하나로 합쳐 뷰가 렌더할 대상을 돌려줍니다.

tsx
interface CalendarRender {
  /** 렌더할 spec — 초안이 활성일 땐 draft, 아니면 라이브 spec. */
  spec: CalendarSpec;
  /** 활성 초안이 바꾼 "container/id" 키 집합 (없으면 빈 Set). */
  changedIds: ReadonlySet<string>;
  /** AI 초안 오버레이가 렌더되는 중인지. */
  isDraft: boolean;
}
tsx
"use client";

import {
  CalendarProvider,
  MonthView,
  useCalendarRender,
  useCalendarStore,
} from "@reopt-ai/opt-calendar";
import type { CalendarStore } from "@reopt-ai/opt-calendar";

// CalendarProvider 아래에서는 컨텍스트 훅을 바로 읽을 수 있습니다.
function DraftBadge() {
  const { spec, changedIds, isDraft } = useCalendarRender();
  const store = useCalendarStore();
  const eventCount = Object.keys(spec.events).length;

  return (
    <div className="flex items-center gap-2">
      <span>이벤트 {eventCount}개</span>
      {isDraft && <span>AI 초안 {changedIds.size}건 대기</span>}
      <button onClick={() => store.undo()} disabled={!store.canUndo()}>
        되돌리기
      </button>
    </div>
  );
}

export function CustomCalendar({ store }: { store: CalendarStore }) {
  return (
    <CalendarProvider store={store} timeZone="Asia/Seoul">
      <DraftBadge />
      <MonthView date={new Date()} />
    </CalendarProvider>
  );
}

2. useCalendarNavigation

활성 뷰(day/week/month)의 단위로 앵커 날짜를 이전/다음/오늘로 옮기고, 기간 라벨을 계산합니다. 툴바를 직접 만들 때 사용합니다.

tsx
function useCalendarNavigation(
  options: UseCalendarNavigationOptions,
): UseCalendarNavigationResult;

Options

속성타입설명
dateDate현재 앵커 날짜
viewCalendarViewMode스텝 단위를 결정 (day=±1일, week=±7일, 그 외=±1개월)
onDateChange(date: Date) => void새 앵커 날짜 콜백
timeZoneTimeZoneId?라벨 포매팅 타임존
weekStartsOnWeekday?주 시작 요일. 기본 0(일요일)
localestring?라벨용 BCP-47 로케일

Returns

속성타입설명
labelstring사람이 읽는 기간 라벨 (예: "2026년 7월")
goToday() => void오늘로 이동
goPrev() => void이전 기간으로
goNext() => void다음 기간으로
tsx
"use client";

import { useCalendarNavigation } from "@reopt-ai/opt-calendar";
import { useState } from "react";

function CustomToolbar() {
  const [date, setDate] = useState(() => new Date());
  const { label, goToday, goPrev, goNext } = useCalendarNavigation({
    date,
    view: "month",
    onDateChange: setDate,
    timeZone: "Asia/Seoul",
    weekStartsOn: 1,
    locale: "ko",
  });

  return (
    <div className="flex items-center gap-2">
      <button onClick={goPrev} aria-label="이전">‹</button>
      <strong>{label}</strong>
      <button onClick={goNext} aria-label="다음">›</button>
      <button onClick={goToday}>오늘</button>
    </div>
  );
}

3. useCalendarRemoteSource

보이는 날짜 윈도우만 원격에서 fetch하고, 로컬/AI 편집을 낙관적으로 write-back하는 원격 이벤트 소스입니다. opt-datagrid의 원격 소스를 "행 페이지" 대신 "날짜 범위"로 바꾼 모델이며, 첫 인자로 store를 받습니다.

tsx
function useCalendarRemoteSource(
  store: CalendarStore,
  options: CalendarRemoteSourceOptions,
): CalendarRemoteSourceResult;

Options

속성타입설명
loadEvents(p) => Promise<CalendarRemoteLoadResult>윈도우의 이벤트를 fetch (필수). window/reason/snapshotVersion/signal 전달
savePatches(p) => Promise<… | void>로컬/AI 패치를 저장 (선택). 지정 시 낙관적 write-back 활성화
subscribeToInvalidations(handler) => unsubscribe서버 push 무효화 구독. 알림 시 현재 윈도우 재로드
onTelemetry(t: CalendarRemoteTelemetry) => void로드/저장/무효화 카운터 sink

Returns

속성타입설명
onWindowChanged(w: CalendarRemoteWindow) => void보이는 윈도우 변경 시 호출(내비/뷰 전환). <Calendar>는 자동 연결
applyLocalOps(ops, changedEventIds?) => void로컬 ops를 낙관적으로 적용하고 저장을 큐잉
isLoadingboolean로드 진행 중 여부
lastErrorError | null마지막 로드 오류
snapshotVersionCalendarSnapshotVersion현재 스냅샷 버전(낙관적 동시성 토큰)
refresh() => void현재 윈도우 재로드
telemetryCalendarRemoteTelemetryloadCount/saveCount/staleDropCount 등 스냅샷
tsx
"use client";

import {
  Calendar,
  useCalendarRemoteSource,
} from "@reopt-ai/opt-calendar";
import type { CalendarStore } from "@reopt-ai/opt-calendar";

function RemoteCalendar({ store }: { store: CalendarStore }) {
  const remote = useCalendarRemoteSource(store, {
    loadEvents: async ({ window, signal }) => {
      const res = await fetch(
        `/api/events?from=${window.start}&to=${window.end}`,
        { signal },
      );
      return { events: await res.json() };
    },
    savePatches: async ({ ops, snapshotVersion }) => {
      const res = await fetch("/api/events/patch", {
        method: "POST",
        body: JSON.stringify({ ops, snapshotVersion }),
      });
      return res.json(); // { events?, snapshotVersion?, rejected? }
    },
    subscribeToInvalidations: (handler) => {
      const ws = new WebSocket("wss://api/events/live");
      ws.onmessage = () => handler();
      return () => ws.close();
    },
  });

  // <Calendar>가 뷰/날짜 변경 시 remote.onWindowChanged를 자동 호출합니다.
  return (
    <Calendar store={store} timeZone="Asia/Seoul" remoteSource={remote} />
  );
}

4. useCalendarSuggestion

AI 패치를 라이브 spec이 아닌 shadow draft로 스트리밍하는 리뷰 플로우입니다. 소비자는 초안을 미리 본 뒤 전체를 승인(하나의 undo 엔트리로 적용)·폐기·추가 스트리밍하거나, 엔티티 단위로 승인/거부할 수 있습니다. core 훅이라 ai 의존성이 없습니다(이미 생성된 패치 스트림을 소비).

tsx
function useCalendarSuggestion(
  store: CalendarStore,
): UseCalendarSuggestionResult;

Returns

속성타입설명
suggestionCalendarSuggestion | null현재 초안(없으면 null). <Calendar>의 suggestion prop에 전달
start(source: PatchSource) => Promise<void>격리된 초안으로 패치 스트리밍 시작(라이브 spec 불변)
approve() => void초안을 하나의 undo 엔트리로 라이브 store에 적용
reject() => void초안 폐기
refine(source: PatchSource) => Promise<void>기존 초안에 패치를 이어서 스트리밍
abort() => void진행 중인 스트림 중단(초안은 ready로 유지)
approveEntity(ref: EntityRef) => void초안의 단일 엔티티 변경만 적용
rejectEntity(ref: EntityRef) => void초안의 단일 엔티티를 base로 되돌림

CalendarSuggestion

tsx
interface CalendarSuggestion {
  /** 스트리밍 상태. */
  status: "streaming" | "ready" | "error";
  /** AI 변경이 적용된 초안 spec. */
  draftSpec: CalendarSpec;
  /** base와 다른 "container/id" 키 집합. */
  changedIds: ReadonlySet<string>;
  /** AI가 누적한 패치. */
  patches: JsonPatchOp[];
  /** 스트리밍 실패 시 오류. */
  error: Error | null;
  /** 지금까지 적용된 op 수. */
  appliedCount: number;
}
tsx
"use client";

import {
  Calendar,
  useCalendarSuggestion,
} from "@reopt-ai/opt-calendar";
import type { CalendarStore } from "@reopt-ai/opt-calendar";

function AiCalendar({ store }: { store: CalendarStore }) {
  const ai = useCalendarSuggestion(store);

  async function ask(prompt: string) {
    const res = await fetch("/api/calendar/ai", {
      method: "POST",
      body: JSON.stringify({ prompt }),
    });
    // NDJSON 패치 스트림을 shadow draft로 흘려보냄
    await ai.start(res.body!);
  }

  return (
    <>
      {ai.suggestion && (
        <div className="flex gap-2">
          <span>{ai.suggestion.changedIds.size}건 제안</span>
          <button onClick={ai.approve}>승인</button>
          <button onClick={ai.reject}>폐기</button>
        </div>
      )}
      <Calendar
        store={store}
        timeZone="Asia/Seoul"
        suggestion={ai.suggestion}
      />
    </>
  );
}

NDJSON 스트림 생성과 서버 핸들러 연동은 AI 스트리밍 문서를 참고하세요.

5. 상호작용 훅

useEventDrag/useEventResize는 시간 그리드 칩의 포인터 드래그·엣지 리사이즈를 처리하는 저수준 훅입니다. <Calendar>가 내부에서 이미 연결하므로, 커스텀 타임 그리드를 직접 만들 때만 필요합니다. 둘 다 드롭/릴리스 시점에 단 하나의 replace 패치를 onCommit으로 커밋하여 드래그·AI와 같은 단일 undo 경로를 공유합니다.

useEventDrag

tsx
function useEventDrag(options: UseEventDragOptions): UseEventDragResult;
Option타입설명
eventCalendarEvent드래그 대상 이벤트
geometryTimeGridGeometrypx↔분 매퍼(컬럼 기하)
onCommit(ops: JsonPatchOp[]) => void드롭 시 move 패치 커밋(보통 applyPatch)
disabledboolean?드래그 비활성화
timeZonestring?tz 정확한 move 수식용 표시 타임존
buildOps(start, end) => JsonPatchOp[]커밋 패치 커스터마이즈(반복 인스턴스는 override 시퀀스 주입)
dayIndex / dayCountnumber?지정 시 가로 드래그가 요일 이동이 됩니다(주간 뷰 크로스-데이 드래그, deltaDays·dayOffsetPx 반환)

반환: onPointerDown(draggable에 연결), isDragging, deltaMinutes(드래그 중 스냅된 분 델타 — ghost 렌더용, 그 외 0), deltaDays/dayOffsetPx(가로 요일 이동분).

useEventResize

useEventDrag와 동일하되 edge: "start" | "end" 옵션이 추가되고, 반환은 onPointerDown, isResizing, deltaMinutes입니다.

tsx
function useEventResize(
  options: UseEventResizeOptions,
): UseEventResizeResult;

useSlotDragCreate

빈 타임 그리드에서 드래그로 이벤트를 생성하는 훅입니다. day 컬럼 요소에 onPointerDown을 연결하면, 빈 슬롯을 누르고 끄는 동안 스냅된 선택 범위(selection)를 추적하고 놓는 순간 timed 이벤트 add 패치를 onCommit으로 커밋합니다. 기존 이벤트 블록 위에서 시작한 프레스와 최소 길이(기본 스냅 단위) 미만의 클릭은 무시합니다. <Calendar> 주/일 뷰가 기본으로 연결합니다.

tsx
function useSlotDragCreate(
  options: UseSlotDragCreateOptions,
): UseSlotDragCreateResult;
// options: { dayKey, geometry, timeZone?, onCommit, title,
//            disabled?, minDurationMinutes?, onCreated? }
// result:  { onPointerDown, selection }
tsx
"use client";

import {
  createTimeGridGeometry,
  useEventDrag,
} from "@reopt-ai/opt-calendar";
import type { CalendarEvent, CalendarStore } from "@reopt-ai/opt-calendar";

function DraggableChip({
  event,
  store,
}: {
  event: CalendarEvent;
  store: CalendarStore;
}) {
  // pxPerMinute=0.8 ⇒ 48px/시간, 15분 스냅 (모두 기본값)
  const geometry = createTimeGridGeometry({
    pxPerMinute: 0.8,
    snapMinutes: 15,
  });

  const { onPointerDown, isDragging, deltaMinutes } = useEventDrag({
    event,
    geometry,
    timeZone: "Asia/Seoul",
    // 드래그·리사이즈·AI가 공유하는 단일 커밋 경로
    onCommit: (ops) => store.applyPatch(ops, { source: "drag" }),
  });

  return (
    <div
      onPointerDown={onPointerDown}
      data-dragging={isDragging}
      style={{ transform: `translateY(${geometry.minuteToY(deltaMinutes)}px)` }}
    >
      {event.title}
    </div>
  );
}
Previous릴리즈 노트opt-calendar 버전별 변경 사항과 하이라이트를 패키지 기준으로 정리합니다.구축·운영
릴리즈 노트 페이지로 이동
Next타입 레퍼런스CalendarSpec, CalendarEvent, Booking, EventType, JsonPatchOp 등 핵심 타입 정의레퍼런스