Schedule
Schedule
일별·주별·월별·연별 뷰를 가진 일정 컴포넌트예요. @mantine/schedule 패키지의 컴포넌트예요. 자동 뷰 전환과 함께 모든 일정 뷰(Day, Week, Month, Year)를 결합한 통합 컴포넌트예요.
출처: 문서
본문
사용법 (Usage)
Schedule은 자동 뷰 전환과 함께 모든 일정 뷰(Day, Week, Month, Year)를 결합한 통합 컴포넌트예요. 헤더의 뷰 선택기를 사용해 뷰 간에 전환할 수 있어요.
import { Schedule } from '@mantine/schedule';
import { events } from './data';
function Demo() {
return (
<Schedule
events={events}
onEventClick={() => {}}
/>
);
}
제어된 상태 (Controlled state)
view, onViewChange, date, onDateChange props로 현재 뷰와 날짜를 외부에서 제어할 수 있어요. 커스텀 내비게이션을 만들거나 다른 컴포넌트와 통합할 수 있게 해줘요.
import { useState } from 'react';
import { Text } from '@mantine/core';
import { Schedule, ScheduleViewLevel } from '@mantine/schedule';
import { events } from './data';
function Demo() {
const [view, setView] = useState('week');
const [date, setDate] = useState(new Date());
return (
<>
<Text>Current view: {view}</Text>
<Schedule
view={view as ScheduleViewLevel}
onViewChange={setView as (v: ScheduleViewLevel) => void}
date={date}
onDateChange={(newDate) => setDate(new Date(newDate))}
events={events}
/>
</>
);
}
뷰별 props (View-specific props)
dayViewProps, weekViewProps, monthViewProps, yearViewProps를 통해 뷰별 props를 전달해 각 뷰를 커스터마이즈할 수 있어요. 이 props는 해당 뷰 컴포넌트로 전달돼요.
예를 들어 Day와 Week 뷰에 다른 시간 범위를 설정하거나, Month와 Year 뷰의 주 시작 요일을 커스터마이즈할 수 있어요.
import { Schedule } from '@mantine/schedule';
import { events } from './data';
function Demo() {
return (
<Schedule
events={events}
dayViewProps={{ startTime: '08:00:00', endTime: '17:00:00' }}
weekViewProps={{ startTime: '08:00:00', endTime: '17:00:00' }}
monthViewProps={{ firstDayOfWeek: 1 }}
yearViewProps={{ firstDayOfWeek: 1 }}
/>
);
}
기본 뷰 (Default view)
defaultView로 비제어 모드에서 처음 표시할 뷰를 제어할 수 있어요.
import { Schedule } from '@mantine/schedule';
import { events } from './data';
function Demo() {
return (
<Schedule
events={events}
defaultView="month"
/>
);
}
커스텀 헤더 (Custom header)
ScheduleHeader 복합 컴포넌트를 자체 컨트롤과 함께 사용해 커스텀 헤더를 만들 수 있어요. 뷰별 props로 withHeader={false}를 전달해 모든 뷰에서 기본 헤더를 비활성화할 수 있어요.
import dayjs from 'dayjs';
import { useState } from 'react';
import {
DateStringValue,
getStartOfWeek,
Schedule,
ScheduleHeader,
ScheduleViewLevel,
} from '@mantine/schedule';
import { events } from './data';
function getNavigationHandlers(
date: DateStringValue,
view: ScheduleViewLevel
) {
const d = dayjs(date);
switch (view) {
case 'day':
return {
previous: d.subtract(1, 'day'),
next: d.add(1, 'day'),
};
case 'week':
return {
previous: d.subtract(1, 'week'),
next: d.add(1, 'week'),
};
case 'month':
return {
previous: d.subtract(1, 'month').startOf('month'),
next: d.add(1, 'month').startOf('month'),
};
case 'year':
return {
previous: d.subtract(1, 'year').startOf('year'),
next: d.add(1, 'year').startOf('year'),
};
}
}
function getHeaderLabel(date: DateStringValue, view: ScheduleViewLevel) {
const d = dayjs(date);
switch (view) {
case 'day':
return d.format('dddd, MMMM D, YYYY');
case 'week': {
const start = dayjs(
getStartOfWeek({ date, firstDayOfWeek: 1 })
);
const end = start.add(6, 'day');
if (start.month() === end.month()) {
return `${start.format('MMM D')} – ${end.format('D, YYYY')}`;
}
return `${start.format('MMM D')} – ${end.format('MMM D, YYYY')}`;
}
case 'month':
return d.format('MMMM YYYY');
case 'year':
return d.format('YYYY');
}
}
function Demo() {
const [date, setDate] = useState(
dayjs().format('YYYY-MM-DD')
);
const [view, setView] = useState('week');
const nav = getNavigationHandlers(date, view as ScheduleViewLevel);
return (
<>
<ScheduleHeader>
<ScheduleHeader.Control
onClick={() => setDate(nav.previous.format('YYYY-MM-DD') as DateStringValue)}
>
‹
</ScheduleHeader.Control>
<ScheduleHeader.Title>
{getHeaderLabel(date, view as ScheduleViewLevel)}
</ScheduleHeader.Title>
<ScheduleHeader.Control
onClick={() => setDate(nav.next.format('YYYY-MM-DD') as DateStringValue)}
>
›
</ScheduleHeader.Control>
<ScheduleHeader.Today
onClick={() => setDate(dayjs().format('YYYY-MM-DD') as DateStringValue)}
/>
<ScheduleHeader.Control onClick={() => window.print()}>
Print
</ScheduleHeader.Control>
</ScheduleHeader>
<Schedule
date={date}
onDateChange={setDate}
view={view as ScheduleViewLevel}
onViewChange={setView as (v: ScheduleViewLevel) => void}
events={events}
dayViewProps={{ withHeader: false }}
weekViewProps={{ withHeader: false }}
monthViewProps={{ withHeader: false }}
yearViewProps={{ withHeader: false }}
/>
</>
);
}
공통 props (Common props)
다음 props는 모든 뷰에서 공유돼요:
events– 표시할 이벤트locale– 날짜 형식 로케일radius– 테두리 반경labels– i18n 라벨 재정의renderEventBody– 커스텀 이벤트 렌더러withDragDrop– 드래그 앤 드롭 활성화onEventDrop– 드래그 앤 드롭 핸들러canDragEvent– 드래그 권한 콜백withEventResize– 이벤트 리사이즈 활성화onEventResize– 리사이즈 핸들러canResizeEvent– 리사이즈 권한 콜백mode– 상호작용 모드 (default또는static)
이 props는 자동으로 모든 뷰에 전달돼요. 뷰별 커스터마이즈에는 dayViewProps, weekViewProps, monthViewProps, yearViewProps props를 사용해요.
어젠다 뷰 (Agenda view)
withAgenda prop으로 DayView, WeekView, MonthView의 헤더에 "Agenda" 버튼을 표시할 수 있어요. 클릭하면 현재 표시된 날짜 범위의 이벤트를 보여주는 AgendaView가 열려요.
import dayjs from 'dayjs';
import { Schedule } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const tomorrow = dayjs().add(1, 'day').format('YYYY-MM-DD');
const events = [
{
id: 'standup',
title: 'Morning Standup',
start: `${today} 09:00:00`,
end: `${today} 09:30:00`,
color: 'blue',
},
{
id: 'team-meeting',
title: 'Team Meeting',
start: `${today} 10:00:00`,
end: `${today} 11:30:00`,
color: 'green',
},
{
id: 'weekly-review-series',
title: 'Weekly review',
start: `${dayjs(today).startOf('week').format('YYYY-MM-DD')} 16:00:00`,
end: `${dayjs(today).startOf('week').format('YYYY-MM-DD')} 17:00:00`,
color: 'orange',
recurrence: {
rrule: 'FREQ=WEEKLY;COUNT=8',
},
},
];
function Demo() {
return (
<Schedule events={events} withAgenda />
);
}
드래그 앤 드롭 (Drag and drop)
withEventsDragAndDrop prop으로 이벤트 드래그 앤 드롭을 활성화할 수 있어요. 이벤트는 모든 뷰에서 서로 다른 타임 슬롯과 날짜로 드래그할 수 있어요.
import { useState } from 'react';
import dayjs from 'dayjs';
import { Schedule, ScheduleEventData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const tomorrow = dayjs().add(1, 'day').format('YYYY-MM-DD');
const initialEvents: ScheduleEventData[] = [
{
id: 1,
title: 'Morning Standup',
start: `${today} 09:00:00`,
end: `${today} 09:30:00`,
color: 'blue',
},
{
id: 2,
title: 'Team Meeting',
start: `${today} 10:00:00`,
end: `${today} 11:30:00`,
color: 'green',
},
{
id: 6,
title: 'All Day Conference',
start: `${today} 00:00:00`,
end: dayjs(today).add(1, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'),
color: 'red',
},
];
function Demo() {
const [events, setEvents] = useState(initialEvents);
const handleEventDrop = ({ eventId, newStart, newEnd }: { eventId: string | number; newStart: string; newEnd: string }) => {
setEvents((prev) =>
prev.map((event) =>
event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
)
);
};
return (
<Schedule
events={events}
onEventDrop={handleEventDrop}
withEventsDragAndDrop
/>
);
}
이벤트 생성 및 업데이트 (Create and update events)
withDragSlotSelect prop으로 사용자가 타임 슬롯이나 날짜 셀을 가로질러 드래그해 범위를 선택할 수 있게 해요. 드래그가 끝나면 onSlotDragEnd 콜백이 범위의 시작·종료 날짜로 호출돼요. onTimeSlotClick, onAllDaySlotClick, onDayClick, onEventClick 콜백과 결합하면 모든 뷰에서 완전한 이벤트 생성·편집 경험을 구현할 수 있어요.
import dayjs from 'dayjs';
import { useRef, useState } from 'react';
import { PlusIcon } from '@phosphor-icons/react';
import { ActionIcon, Box, Group } from '@mantine/core';
import { Schedule, ScheduleEventData } from '@mantine/schedule';
import { EventData, EventForm } from './EventForm';
import { events } from './events';
function Demo() {
const [allEvents, setAllEvents] = useState(events);
const [formOpened, setFormOpened] = useState(false);
const [selectedEventData, setSelectedEventData] = useState(null);
const mobileSelectedDate = useRef(dayjs().format('YYYY-MM-DD'));
const handleTimeSlotClick = ({ slotStart, slotEnd }: { slotStart: string; slotEnd: string }) => {
setSelectedEventData({
title: '',
start: new Date(slotStart),
end: new Date(slotEnd),
color: 'blue',
});
setFormOpened(true);
};
const handleAllDaySlotClick = (slotDate: string) => {
setSelectedEventData({
title: '',
start: dayjs(slotDate).startOf('day').toDate(),
end: dayjs(slotDate).endOf('day').toDate(),
color: 'blue',
});
setFormOpened(true);
};
const handleDayClick = (date: string) => {
setSelectedEventData({
title: '',
start: dayjs(date).startOf('day').toDate(),
end: dayjs(date).endOf('day').toDate(),
color: 'blue',
});
setFormOpened(true);
};
const handleEventClick = (event: ScheduleEventData) => {
setSelectedEventData({
id: event.id,
title: event.title,
start: new Date(event.start),
end: new Date(event.end),
color: event.color || 'blue',
});
setFormOpened(true);
};
const handleSubmit = (values: EventData) => {
if (values.id) {
setAllEvents((prev) =>
prev.map((event) =>
event.id === values.id
? {
...event,
title: values.title,
start: dayjs(values.start).toISOString(),
end: dayjs(values.end).toISOString(),
color: values.color || 'blue',
}
: event
)
);
} else {
setAllEvents((prev) => [
...prev,
{
id: Math.random().toString(36).substring(2, 11),
title: values.title,
start: dayjs(values.start).toISOString(),
end: dayjs(values.end).toISOString(),
color: values.color || 'blue',
},
]);
}
};
const handleSlotDragEnd = (rangeStart: string, rangeEnd: string) => {
setSelectedEventData({
title: '',
start: new Date(rangeStart),
end: new Date(rangeEnd),
color: 'blue',
});
setFormOpened(true);
};
const handleDeleteEvent = () => {
if (selectedEventData?.id) {
setAllEvents((prev) => prev.filter((e) => e.id !== selectedEventData.id));
}
};
const handleCreateEvent = () => {
const selectedDate = mobileSelectedDate.current;
setSelectedEventData({
title: '',
start: dayjs(selectedDate).startOf('day').toDate(),
end: dayjs(selectedDate).endOf('day').toDate(),
color: 'blue',
});
setFormOpened(true);
};
return (
<>
<Schedule
events={allEvents}
onTimeSlotClick={handleTimeSlotClick}
onAllDaySlotClick={handleAllDaySlotClick}
onDayClick={handleDayClick}
onEventClick={handleEventClick}
onSlotDragEnd={handleSlotDragEnd}
withDragSlotSelect
/>
<EventForm
setFormOpened={setFormOpened}
onExitTransitionEnd={() => setSelectedEventData(null)}
values={selectedEventData}
onSubmit={handleSubmit}
onDelete={selectedEventData?.id ? handleDeleteEvent : undefined}
/>
</>
);
}
외부 드래그 앤 드롭 (External drag and drop)
onExternalEventDrop을 사용하면 컴포넌트 바깥에서 일정으로 항목을 끌어올 수 있어요. 외부 항목은 onDragStart 중에 dataTransfer에 데이터를 설정해야 해요. 콜백은 DataTransfer 객체와 드롭 대상 날짜시간을 받아요.
import { useRef, useState } from 'react';
import dayjs from 'dayjs';
import { Box, Grid, Text } from '@mantine/core';
import { Schedule, ScheduleEventData } from '@mantine/schedule';
const sidebarItems = [
{ title: 'Quick Sync', duration: 30, color: 'teal' },
{ title: 'Workshop', duration: 120, color: 'orange' },
{ title: 'One-on-One', duration: 60, color: 'violet' },
];
function Demo() {
const [events, setEvents] = useState([]);
const nextId = useRef(1);
const handleExternalDrop = (dataTransfer: DataTransfer, dropDateTime: string) => {
const raw = dataTransfer.getData('text/plain');
if (!raw) {
return;
}
const data = JSON.parse(raw);
const start = dayjs(dropDateTime);
const end = start.add(data.duration, 'minutes');
setEvents((prev) => [
...prev,
{
id: nextId.current++,
title: data.title,
start: start.format('YYYY-MM-DD HH:mm:ss'),
end: end.format('YYYY-MM-DD HH:mm:ss'),
color: data.color,
},
]);
};
const handleEventDrop = ({ eventId, newStart, newEnd }: { eventId: string | number; newStart: string; newEnd: string }) => {
setEvents((prev) =>
prev.map((event) =>
event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
)
);
};
return (
<Grid>
<Grid.Col span={4}>
<Text>Drag to schedule</Text>
{sidebarItems.map((item) => (
<div
draggable
onDragStart={(e) => {
e.dataTransfer.setData('text/plain', JSON.stringify(item));
e.dataTransfer.effectAllowed = 'copy';
}}
style={{ padding: '8px 12px', marginBottom: 8, cursor: 'grab' }}
>
{item.title}
</div>
))}
</Grid.Col>
<Grid.Col span={8}>
<Schedule events={events} onExternalEventDrop={handleExternalDrop} onEventDrop={handleEventDrop} />
</Grid.Col>
</Grid>
);
}
양방향 드래그 앤 드롭 (Bidirectional drag and drop)
onExternalEventDrop과 withEventsDragAndDrop을 함께 사용하면 양방향 드래그 앤 드롭을 활성화할 수 있어요. 사이드바에서 끌어온 항목은 목록에서 제거되고 일정에 추가돼요. 일정에서 사이드바로 끌어온 이벤트는 일정에서 제거돼요. 이벤트를 드래그할 때 일정은 dataTransfer에 { eventId }를 가진 application/json을 설정하며, 사이드바 드롭 존이 이를 읽어 이벤트를 식별해요.
import { useRef, useState } from 'react';
import dayjs from 'dayjs';
import { Box, Grid, Text } from '@mantine/core';
import { Schedule, ScheduleEventData } from '@mantine/schedule';
interface SidebarItem {
title: string;
duration: number;
color: string;
}
const initialSidebarItems: SidebarItem[] = [
{ title: 'Quick Sync', duration: 30, color: 'teal' },
{ title: 'Workshop', duration: 120, color: 'orange' },
{ title: 'One-on-One', duration: 60, color: 'violet' },
];
function Demo() {
const [sidebarItems, setSidebarItems] = useState(initialSidebarItems);
const [events, setEvents] = useState([]);
const [sidebarHighlight, setSidebarHighlight] = useState(false);
const nextId = useRef(1);
const handleExternalDrop = (dataTransfer: DataTransfer, dropDateTime: string) => {
const raw = dataTransfer.getData('text/plain');
if (!raw) {
return;
}
const data = JSON.parse(raw);
const start = dayjs(dropDateTime);
const end = start.add(data.duration, 'minutes');
setEvents((prev) => [
...prev,
{
id: nextId.current++,
title: data.title,
start: start.format('YYYY-MM-DD HH:mm:ss'),
end: end.format('YYYY-MM-DD HH:mm:ss'),
color: data.color,
},
]);
setSidebarItems((prev) => prev.filter((item) => item.title !== data.title));
};
const handleEventDrop = ({ eventId, newStart, newEnd }: { eventId: string | number; newStart: string; newEnd: string }) => {
setEvents((prev) =>
prev.map((event) =>
event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
)
);
};
const handleSidebarDrop = (e: React.DragEvent) => {
e.preventDefault();
setSidebarHighlight(false);
const raw = e.dataTransfer.getData('application/json');
if (!raw) {
return;
}
const { eventId } = JSON.parse(raw);
const event = events.find((ev) => String(ev.id) === String(eventId));
if (!event) {
return;
}
const duration = dayjs(event.end).diff(dayjs(event.start), 'minutes');
setSidebarItems((prev) => [
...prev,
{ title: event.title, duration, color: event.color || 'blue' },
]);
setEvents((prev) => prev.filter((ev) => ev.id !== event.id));
};
return (
<Grid>
<Grid.Col span={4}>
<div
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setSidebarHighlight(true);
}}
onDragLeave={() => setSidebarHighlight(false)}
onDrop={handleSidebarDrop}
style={{ padding: 8, borderRadius: 8 }}
>
<Text>Unscheduled</Text>
{sidebarItems.map((item) => (
<div
draggable
onDragStart={(e) => {
e.dataTransfer.setData('text/plain', JSON.stringify(item));
e.dataTransfer.effectAllowed = 'copy';
}}
style={{ padding: '8px 12px', marginBottom: 8, cursor: 'grab' }}
>
{item.title}
</div>
))}
{sidebarItems.length === 0 && (
<Text>Drag events here to unschedule</Text>
)}
</div>
</Grid.Col>
<Grid.Col span={8}>
<Schedule
events={events}
onExternalEventDrop={handleExternalDrop}
onEventDrop={handleEventDrop}
withEventsDragAndDrop
/>
</Grid.Col>
</Grid>
);
}
이벤트 리사이즈 (Event resize)
withEventResize prop으로 이벤트 리사이즈를 활성화할 수 있어요. 사용자는 이벤트의 위나 아래 가장자리를 끌어 시작 또는 종료 시간을 조정할 수 있어요. onEventResize 콜백으로 리사이즈를 처리해요. 이벤트 리사이즈는 Day와 Week 뷰에서 동작하며 드래그 앤 드롭과 결합할 수 있어요.
import { useState } from 'react';
import dayjs from 'dayjs';
import { Schedule, ScheduleEventData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const tomorrow = dayjs().add(1, 'day').format('YYYY-MM-DD');
const initialEvents: ScheduleEventData[] = [
{
id: 1,
title: 'Morning Standup',
start: `${today} 09:00:00`,
end: `${today} 09:30:00`,
color: 'blue',
},
{
id: 2,
title: 'Team Meeting',
start: `${today} 10:00:00`,
end: `${today} 11:30:00`,
color: 'green',
},
];
function Demo() {
const [events, setEvents] = useState(initialEvents);
const handleEventUpdate = ({ eventId, newStart, newEnd }: { eventId: string | number; newStart: string; newEnd: string }) => {
setEvents((prev) =>
prev.map((event) =>
event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
)
);
};
return (
<Schedule
events={events}
withEventResize
onEventResize={handleEventUpdate}
onEventDrop={handleEventUpdate}
withEventsDragAndDrop
/>
);
}
반복 이벤트 (Recurring events)
Schedule은 각 뷰에서 표시된 날짜 범위에 대해 반복 이벤트를 자동으로 확장해요. 전체 문서는 반복 이벤트 가이드를 참고해요.
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',
},
];
function Demo() {
const [date, setDate] = useState('2024-01-15');
return <Schedule date={date} onDateChange={setDate} events={events} />;
}
배경 이벤트 (Background events)
이벤트에 display="background"를 설정하면 전체 너비, 반투명, 비상호작용 블록으로 일반 이벤트 뒤에 렌더링돼요. 배경 이벤트는 부재, 점심 시간, 집중 시간 또는 다른 차단 기간을 표시할 때 유용해요. 배경 이벤트는 Day, Week, Month 뷰에서 동작해요.
import dayjs from 'dayjs';
import { useState } from 'react';
import { Schedule, ScheduleEventData } from '@mantine/schedule';
const today = dayjs().format('YYYY-MM-DD');
const events: ScheduleEventData[] = [
{
id: 'lunch-block',
title: 'Lunch break',
start: `${today} 12:00:00`,
end: `${today} 13:00:00`,
color: 'gray',
display: 'background',
},
{
id: 'focus-time',
title: 'Focus time',
start: `${today} 14:00:00`,
end: `${today} 16:00:00`,
color: 'blue',
display: 'background',
},
{
id: 'team-meeting',
title: 'Team meeting',
start: `${today} 10:00:00`,
end: `${today} 11:00:00`,
color: 'violet',
},
];
function Demo() {
const [date, setDate] = useState(today);
return (
<Schedule date={date} onDateChange={setDate} events={events} />
);
}
반응형 레이아웃 (Responsive layout)
Schedule은 layout="responsive" prop을 지원하며, 컨테이너 너비에 따라 뷰를 자동으로 전환해요. 컨테이너를 아래로 조정하면 뷰가 변경되는 것을 볼 수 있어요.
import { Box } from '@mantine/core';
import { Schedule } from '@mantine/schedule';
import { events } from './data';
function Demo() {
return (
<Box maw={800}>
<Schedule events={events} layout="responsive" />
</Box>
);
}
뷰 컴포넌트 (View components)
개별 뷰를 더 세밀하게 제어하려면 뷰 컴포넌트를 직접 사용할 수 있어요:
- DayView – 타임 슬롯이 있는 단일 날짜
- WeekView – 타임 슬롯이 있는 전체 주
- MonthView – 달력 월 그리드
- YearView – 전체 연도 개요