Recurring events

Recurring events

@mantine/schedule에서 반복 이벤트를 정의하고 뷰가 시리즈를 확장하는 방식에 대한 문서예요. RFC 5545 rrule 패턴과 오버라이드 이벤트, 확장 동작을 설명해요.

출처: 문서

본문

일반적인 rrule 패턴 (Common rrule patterns)

  • FREQ=DAILY;COUNT=10 – 매일, 10회 발생
  • FREQ=WEEKLY;BYDAY=MO,WE,FR – 매주 월, 수, 금요일
  • FREQ=MONTHLY;BYMONTHDAY=15 – 매월 15일
  • FREQ=YEARLY;COUNT=5 – 매년 1회, 5회

오버라이드 이벤트 (Override event)

오버라이드 이벤트는 시리즈에서 생성된 단일 발생을 대체해요. recurringEventId로 시리즈를 가리키고 recurrenceId로 대체할 발생을 식별해요(YYYY-MM-DD HH:mm:ss 형식, 원래 시작 시간과 일치):

const override = {
  id: 'weekly-series-override',
  title: 'Weekly planning (moved)',
  start: '2024-01-17 16:00:00',
  end: '2024-01-17 17:00:00',
  color: 'grape',
  recurringEventId: 'weekly-series',
  recurrenceId: '2024-01-17 10:00:00',
};

렌더링이 동작하는 방식 (How rendering works)

각 뷰는 표시된 날짜 범위에 대해서만 반복 이벤트를 확장해요:

  • rrule 문자열을 파싱하고 범위 내의 발생 시작 시간을 생성해요
  • exdate 항목과 일치하는 발생을 제거해요
  • 일치하는 발생을 오버라이드 이벤트로 대체해요
  • 일회성 이벤트와 병합해 렌더링해요

생성된 인스턴스는 recurringInstance 메타데이터 객체를 포함해요:

  • isRecurringInstance – 생성된 이벤트에 대해 항상 true
  • recurringEventId – 부모 시리즈 id
  • recurrenceId – 발생 식별자
  • originalStart / originalEnd – 오버라이드 전의 발생 날짜

expandRecurringEvents 유틸리티

커스텀 로직에 expandRecurringEvents 유틸리티를 직접 사용할 수 있어요:

import { expandRecurringEvents } from '@mantine/schedule';

const expanded = expandRecurringEvents({
  events,
  rangeStart: '2024-01-15 00:00:00',
  rangeEnd: '2024-01-21 23:59:59',
  expansionLimit: 2000, // optional, default 2000
});

recurrenceExpansionLimit prop

모든 뷰는 recurrenceExpansionLimit prop(기본값 2000)을 받아 반복 시리즈당 생성되는 인스턴스 수를 제한해요. 이는 무제한 반복 규칙(예: COUNT나 UNTIL이 없는 FREQ=DAILY)의 성능 문제를 방지해요. 제한은 각 시리즈에 독립적으로 적용되므로, 무제한 시리즈가 여러 개인 일정은 최대 limit × 시리즈 수의 총 인스턴스를 생성할 수 있다는 점에 주의하세요.

Schedule 데모 (Schedule demo)

import { useState } from 'react';
import { Schedule } from '@mantine/schedule';

const events = [
  {
    id: 'schedule-series',
    title: 'Weekly planning (series)',
    start: '2024-01-15 10:00:00',
    end: '2024-01-15 11:00:00',
    color: 'blue',
    recurrence: {
      rrule: 'FREQ=WEEKLY;BYDAY=MO,WE;COUNT=16',
      exdate: ['2024-01-17 10:00:00'],
    },
  },
  {
    id: 'schedule-series-override',
    title: 'Weekly planning (moved one occurrence)',
    start: '2024-01-17 16:00:00',
    end: '2024-01-17 17:00:00',
    color: 'grape',
    recurringEventId: 'schedule-series',
    recurrenceId: '2024-01-17 10:00:00',
  },
  {
    id: 'schedule-one-off',
    title: 'One-off roadmap review',
    start: '2024-01-18 12:00:00',
    end: '2024-01-18 13:00:00',
    color: 'green',
  },
];

function Demo() {
  const [date, setDate] = useState('2024-01-15');
  return <Schedule date={date} onDateChange={setDate} events={events} />;
}

DayView 데모 (DayView demo)

import dayjs from 'dayjs';
import { useState } from 'react';
import { DayView } from '@mantine/schedule';

const today = dayjs().format('YYYY-MM-DD');

const events = [
  {
    id: 'daily-sync-series',
    title: 'Daily sync (series)',
    start: `${dayjs(today).subtract(2, 'day').format('YYYY-MM-DD')} 09:00:00`,
    end: `${dayjs(today).subtract(2, 'day').format('YYYY-MM-DD')} 09:30:00`,
    color: 'blue',
    recurrence: {
      rrule: 'FREQ=DAILY;COUNT=10',
      exdate: [`${today} 09:00:00`],
    },
  },
  {
    id: 'daily-sync-override',
    title: 'Daily sync (moved today)',
    start: `${today} 14:00:00`,
    end: `${today} 14:30:00`,
    color: 'grape',
    recurringEventId: 'daily-sync-series',
    recurrenceId: `${today} 09:00:00`,
  },
  {
    id: 'one-off-day',
    title: 'One-off planning',
    start: `${today} 11:00:00`,
    end: `${today} 12:00:00`,
    color: 'green',
  },
];

function Demo() {
  const [date, setDate] = useState(today);

  return (
    <DayView date={date} onDateChange={setDate} events={events} />
  );
}

WeekView 데모 (WeekView demo)

import dayjs from 'dayjs';
import { useState } from 'react';
import { WeekView, getStartOfWeek } from '@mantine/schedule';

const weekStart = dayjs(getStartOfWeek({ date: new Date(), firstDayOfWeek: 1 })).format('YYYY-MM-DD');

const events = [
  {
    id: 'weekly-sync-series',
    title: 'Weekly sync (series)',
    start: `${weekStart} 10:00:00`,
    end: `${weekStart} 11:00:00`,
    color: 'blue',
    recurrence: {
      rrule: 'FREQ=WEEKLY;BYDAY=MO,WE;COUNT=12',
      exdate: [`${dayjs(weekStart).add(2, 'day').format('YYYY-MM-DD')} 10:00:00`],
    },
  },
  {
    id: 'weekly-sync-override',
    title: 'Weekly sync (moved this week)',
    start: `${dayjs(weekStart).add(2, 'day').format('YYYY-MM-DD')} 15:00:00`,
    end: `${dayjs(weekStart).add(2, 'day').format('YYYY-MM-DD')} 16:00:00`,
    color: 'grape',
    recurringEventId: 'weekly-sync-series',
    recurrenceId: `${dayjs(weekStart).add(2, 'day').format('YYYY-MM-DD')} 10:00:00`,
  },
];

function Demo() {
  const [date, setDate] = useState(weekStart);

  return (
    <WeekView date={date} onDateChange={setDate} events={events} />
  );
}

MonthView 데모 (MonthView demo)

import { useState } from 'react';
import { MonthView } from '@mantine/schedule';

const month = '2025-11-01';

const events = [
  {
    id: 'monthly-planning-series',
    title: 'Weekly planning (series)',
    start: '2025-11-03 10:00:00',
    end: '2025-11-03 11:00:00',
    color: 'blue',
    recurrence: {
      rrule: 'FREQ=WEEKLY;BYDAY=MO;COUNT=10',
      exdate: ['2025-11-17 10:00:00'],
    },
  },
  {
    id: 'monthly-planning-override',
    title: 'Weekly planning (moved occurrence)',
    start: '2025-11-17 15:00:00',
    end: '2025-11-17 16:00:00',
    color: 'grape',
    recurringEventId: 'monthly-planning-series',
    recurrenceId: '2025-11-17 10:00:00',
  },
];

function Demo() {
  const [date, setDate] = useState(month);

  return <MonthView date={date} onDateChange={setDate} events={events} />;
}

YearView 데모 (YearView demo)

import dayjs from 'dayjs';
import { useState } from 'react';
import { YearView } from '@mantine/schedule';

const year = dayjs().year();

const events = [
  {
    id: 'yearly-series',
    title: 'Monthly architecture review (series)',
    start: `${year}-01-10 09:00:00`,
    end: `${year}-01-10 10:00:00`,
    color: 'blue',
    recurrence: {
      rrule: 'FREQ=MONTHLY;COUNT=12',
      exdate: [`${year}-11-10 09:00:00`],
    },
  },
  {
    id: 'yearly-series-override',
    title: 'Monthly architecture review (moved in Nov)',
    start: `${year}-11-10 13:00:00`,
    end: `${year}-11-10 14:00:00`,
    color: 'grape',
    recurringEventId: 'yearly-series',
    recurrenceId: `${year}-11-10 09:00:00`,
  },
];

function Demo() {
  const [date, setDate] = useState(`${year}-01-01`);
  return <YearView date={date} onDateChange={setDate} events={events} />;
}

더 알아보기 (Learn more)