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

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

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

© 2026 reopt-ai. All rights reserved.

Calendar
  1. 문서
  2. /
  3. Calendar
  4. /
  5. 이벤트 편집

이벤트 편집

이벤트 CRUD, 팝오버 에디터, 드래그·리사이즈 상호작용과 단일 undo 경로

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

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

1. 이벤트 모델

CalendarEvent는 events 맵의 한 항목입니다. 필수는 id·title·start·end 넷뿐이고, 나머지는 모두 하위 호환 기본값을 갖는 optional입니다.

필드필수설명
idOevents 맵 내 유니크 id
titleO이벤트 제목
startO포함(inclusive) 시작. timed=오프셋 ISO datetime, all-day=floating ISO date
endO배타(exclusive) 종료 (RFC 5545). all-day는 마지막 날 다음 날짜
allDayall-day 이벤트 여부 (datetime 대신 date 사용)
timeZone이벤트별 tz override. 없으면 CalendarSpec.timeZone
calendarId소속 calendar id (→ calendars); 생략 시 기본 캘린더
location자유 형식 장소
statusconfirmed(기본) | tentative | cancelled
attendeesEventAttendee[] — email/name/status

시간 인코딩. timed 이벤트의 start/end는 오프셋을 포함한 절대 ISO datetime(2026-07-06T14:00:00+09:00)이고, all-day는 timezone 없는 floating ISO date(2026-07-20)입니다. all-day의 end는 배타적이라, 하루짜리 이벤트는 다음 날 날짜를 씁니다. 이 분리 덕분에 네이티브 Intl만으로 모든 표시가 해결됩니다(날짜 라이브러리 불필요).

tsx
import type { CalendarEvent } from "@reopt-ai/opt-calendar";

// timed 이벤트 — start/end는 오프셋 포함 ISO datetime
const meeting: CalendarEvent = {
  id: "sync",
  title: "주간 싱크",
  start: "2026-07-06T14:00:00+09:00",
  end: "2026-07-06T15:00:00+09:00",
  calendarId: "work",
  location: "회의실 A",
  status: "confirmed",
};

// all-day 이벤트 — start/end는 floating ISO date, end는 배타적
const holiday: CalendarEvent = {
  id: "holiday",
  title: "창립기념일",
  start: "2026-07-20",
  end: "2026-07-21", // 하루짜리: 마지막 날 다음 날짜
  allDay: true,
};

rrule·exdates·recurringEventId·originalStart 같은 반복 관련 필드는 반복 일정 문서에서 다룹니다.

2. CRUD 패치 빌더

이벤트 생성·수정·삭제는 순수(pure) 패치 빌더로 조립한 뒤 store.applyPatch(ops, options)로 커밋합니다. 빌더가 순수하기 때문에 직접 편집도 AI가 내보내는 것과 정확히 같은 패치 모양을 만듭니다. options.source는 텔레메트리·디버깅용 자유 형식 태그입니다.

빌더결과 패치
generateEventId()런타임 유니크 id 문자열 (SSR key용 아님)
buildAllDayEvent(id, title, dateKey)하루짜리 all-day CalendarEvent 조립 (end 배타)
buildAddEventPatch(event)add /events/{id} (전체 객체 1회)
buildReplaceFieldPatch(id, field, value)replace /events/{id}/{field}
buildRemoveEventPatch(id)remove /events/{id} (이벤트/시리즈 마스터)
tsx
import {
  generateEventId,
  buildAllDayEvent,
  buildAddEventPatch,
  buildReplaceFieldPatch,
  buildRemoveEventPatch,
} from "@reopt-ai/opt-calendar";

// CREATE — id 생성 → all-day 이벤트 조립 → add 패치로 커밋
const id = generateEventId();
const event = buildAllDayEvent(id, "출시일", "2026-07-20");
store.applyPatch(buildAddEventPatch(event), { source: "create" });

// UPDATE — 필드 하나를 replace
store.applyPatch(buildReplaceFieldPatch(id, "title", "정식 출시"), {
  source: "edit",
});

// DELETE — 이벤트(또는 시리즈 마스터) 제거
store.applyPatch(buildRemoveEventPatch(id), { source: "delete" });

3. 팝오버 편집

EventEditorPopover는 자기 트리거 버튼을 직접 렌더하는 팝오버 폼입니다. 제목·종일 토글·시작/종료 날짜와 시간·반복 규칙·설명·장소·참석자(이메일 추가/제거)·소속 캘린더를 한 폼에서 편집하고 Duplicate로 복제하며, 저장·삭제는 모두 store를 거쳐 커밋되어 드래그·AI 편집과 같은 undo 히스토리를 공유합니다. 편집 모드에는 렌더된 인스턴스를 event로 넘기면 모든 필드가 프리필됩니다. 반복 select의 "사용자 지정"은 INTERVAL·요일 (BYDAY)·종료 조건(UNTIL/COUNT)을 저작하는 빌더를 열고, 기존 커스텀 룰은 자동으로 파싱해 프리필합니다(parseRRuleToForm/buildRRuleFromForm).

tsx
import { EventEditorPopover } from "@reopt-ai/opt-calendar";

// 이벤트 칩을 트리거로 감싸 클릭 시 편집 팝오버를 연다.
// event(렌더 인스턴스)를 넘기면 시간/설명/캘린더까지 프리필된다.
<EventEditorPopover
  mode="edit"
  eventId={event.id}
  event={event}
  triggerContent={<EventChip event={event} />}
  triggerAriaLabel={event.title}
/>;

// create 모드 — 기본은 all-day, 종일 체크를 풀면 timed 이벤트로 생성.
// initialStartTime을 주면 처음부터 timed 폼으로 연다.
<EventEditorPopover
  mode="create"
  date={anchorDate}
  initialStartTime="09:00"
  initialEndTime="10:00"
  triggerContent={<>새 이벤트</>}
/>;

반복 마스터의 한 occurrence에서 시간을 바꿔 저장하면 그 인스턴스만 detach된 override로 커밋되고(드래그와 동일 규칙), 제목· 설명처럼 시간이 아닌 필드만 바꾸면 시리즈 전체(마스터)에 적용됩니다. 이벤트가 calendarId를 가지면 칩과 블록 색상이 해당 캘린더의 color로 렌더됩니다.

팝오버가 edit 모드로 열릴 때 Calendar의 onEventActivate(eventId) 콜백이 호출됩니다. 사이드 패널을 함께 여는 등 활성 이벤트에 반응할 때 사용하세요. 반복 마스터의 한 occurrence를 편집할 때는 occurrenceStart를 넘겨 this/following/all 삭제 범위 선택지를 노출할 수 있습니다(자세히는 반복 일정 문서).

4. 드래그·리사이즈

별도 설정 없이 뷰가 기본으로 timed 이벤트에 드래그·리사이즈를 붙여 줍니다. useEventDrag/useEventResize는 포인터 이동 중에는 스냅된 미리보기 delta만 추적하고, 한 번의 pointerup에 단일 패치를 커밋합니다 — 즉 드래그 한 번이 하나의 undo 엔트리입니다. 이 커밋은 AI 스트림이 쓰는 것과 같은 applyPatch 경로로 흐릅니다.

tsx
import { useEventDrag } from "@reopt-ai/opt-calendar";

// 뷰가 내부적으로 각 timed 이벤트에 연결하는 훅.
// onCommit이 곧 단일 변경 경로 — store.applyPatch로 흐른다.
const drag = useEventDrag({
  event,
  geometry, // TimeGrid가 제공하는 px↔분 매퍼
  timeZone: "Asia/Seoul",
  // 한 번의 pointerup에 단일 이동 패치를 커밋 → 하나의 undo 엔트리
  onCommit: (ops) => store.applyPatch(ops, { source: "drag" }),
});

return <div onPointerDown={drag.onPointerDown}>{event.title}</div>;

기본 buildOps는 replace /events/{id}/start + /end를 내보냅니다. 반복 시리즈의 한 인스턴스를 드래그할 때는 buildOps로 occurrence-override 시퀀스를 주입해 그 인스턴스만 detach합니다(마스터와 나머지는 그대로). 상세 규칙은 반복 일정 문서를 참고하세요.

주간 뷰에서는 가로 드래그가 요일 이동입니다 — useEventDrag에 dayIndex/dayCount를 넘기면 세로(시간) + 가로(날짜) 이동을 한 번의 패치로 커밋합니다(computeEventMoveDelta). 빈 그리드에서는 드래그로 새 이벤트를 생성합니다:

tsx
import { useSlotDragCreate } from "@reopt-ai/opt-calendar";

// 뷰가 각 day 컬럼에 기본으로 붙이는 훅. 빈 슬롯을 누르고 끌면
// 스냅된 시간 범위가 선택되고, 놓는 순간 timed 이벤트가 생성된다.
const create = useSlotDragCreate({
  dayKey: "2026-07-06",
  geometry,
  timeZone: "Asia/Seoul",
  title: "(제목 없음)",
  onCommit: (ops) => store.applyPatch(ops, { source: "create" }),
});

return (
  <div onPointerDown={create.onPointerDown}>
    {create.selection ? <SelectionGhost range={create.selection} /> : null}
  </div>
);

5. undo / redo

모든 편집이 한 히스토리를 공유하므로 undo/redo는 store 하나로 제어됩니다. store.undo()/redo()로 되돌리고, canUndo()/canRedo()로 가용 여부를 읽습니다. AI 스트림은 전체가 한 엔트리이므로 한 번의 undo로 통째로 되돌아갑니다.

tsx
<div role="toolbar" aria-label="편집 히스토리">
  <button onClick={() => store.undo()} disabled={!store.canUndo()}>
    실행 취소
  </button>
  <button onClick={() => store.redo()} disabled={!store.canRedo()}>
    다시 실행
  </button>
</div>

canUndo()/canRedo()는 호출 시점의 스냅샷 값이므로, 버튼 활성 상태를 최신으로 유지하려면 store.subscribe(...)로 변경을 구독해 리렌더링하세요(또는 Hook 레퍼런스의 store 훅 사용).

6. 뷰

Calendar는 month·week·day·agenda·timeline(하루를 가로 시간축으로, 캘린더별 레인) 다섯 가지 뷰를 전환합니다. 아젠다는 "더 보기"로 14일씩 확장됩니다. view/onViewChange로 뷰를 제어하고 (생략 시 내부 상태), weekStartsOn으로 주 시작 요일을 정합니다(0=일요일, 기본 0).

tsx
"use client";

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

export function EventsCalendar({ store }: { store: CalendarStore }) {
  const [view, setView] = useState<CalendarViewMode>("week");

  return (
    <Calendar
      store={store}
      view={view}
      onViewChange={setView}
      weekStartsOn={1} // 월요일 시작
      timeZone="Asia/Seoul"
      onEventActivate={(id) => console.log("활성 이벤트:", id)}
    />
  );
}

주/일 타임그리드는 timeGrid prop(TimeGridOptions)으로 조정합니다 — 시간 범위(startHour/endHour), 행 높이(hourHeight), 스냅(snapMinutes), 그리고 maxBodyHeight를 주면 그리드 내부 스크롤 + 마운트 시 현재 시각으로 자동 스크롤(scrollToNow)이 켜집니다. 자정을 넘는 timed 이벤트는 각 날짜 컬럼에 클리핑된 세그먼트로 렌더됩니다.

tsx
<Calendar
  store={store}
  view="week"
  timeGrid={{ startHour: 7, endHour: 20, maxBodyHeight: 560 }}
  hideWeekends // 월–금만 렌더 (월 뷰도 5열)
  showWeekNumbers // 주간/일간 코너에 ISO 주 번호 (W28)
/>

제어 없이 두면 Calendar가 month 뷰로 시작해 내부에서 뷰·anchor 날짜를 관리합니다. Hook과 remote 소스 연동은 Hook 레퍼런스 문서를 참고하세요.

PreviousCalendar 개요이벤트, 예약, 반복(RRULE), 타임존, 드래그·리사이즈, AI 스트리밍 draft를 한 엔진으로 다루는 opt-calendar 실전 가이드Calendar
Calendar 개요 페이지로 이동
Next반복 일정RRULE 전개, occurrence 모델, this/following/all 인스턴스 단위 편집 시퀀스Calendar