ResourcesDayView

ResourcesDayView

리소스 간 일정 관리를 위해 리소스 열을 사용하는 일별 뷰 컴포넌트예요. @mantine/schedule 패키지의 컴포넌트예요. 리소스를 행으로, 타임 슬롯을 열로 표시해요.

출처: 문서

본문

사용법 (Usage)

ResourcesDayView는 리소스를 행으로, 타임 슬롯을 열로 표시해요. 각 행은 리소스(예: 회의실, 사람, 장비)를 나타내며, 이벤트 데이터의 resourceId 속성을 통해 해당 리소스에 할당된 이벤트를 보여줘요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));

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

종일 이벤트 (All-day events)

하루 전체에 걸친 이벤트(00:00:00에 시작해 다음 날 00:00:00에 끝나는)는 전체 너비 바 형태로 렌더링돼요. 전경(foreground) 종일 이벤트는 리소스 행의 맨 위에 고정되고 여러 개가 있으면 쌓여요. display: 'background'인 종일 이벤트는 행 전체를 색으로 표시해요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

상호작용 가능한 배경 이벤트 (Interactive background events)

withInteractiveBackgroundEvents prop으로 배경 이벤트를 클릭 가능하게 만들 수 있어요. 배경 이벤트를 클릭하면 일반 이벤트와 마찬가지로 onEventClick이 이벤트 객체로 호출돼요. 핸들러에서 event.display === 'background'로 구분해요.

상호작용 가능한 배경 이벤트는 그 아래의 타임 슬롯을 덮는다는 점에 주의하세요. 그 범위의 클릭과 드래그 선택은 그리드 대신 이벤트로 전달돼요. 상호작용 가능한 배경 이벤트는 여전히 드래그할 수 없어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { Text } from '@mantine/core';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';
import { events, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [clicked, setClicked] = useState<ScheduleEventData | null>(null);

  return (
    <>
      <Text>
        Last clicked:{' '}
        {clicked
          ? `${clicked.title} (${clicked.display === 'background' ? 'background' : 'regular'} event)`
          : 'nothing yet'}
      </Text>
      <ResourcesDayView
        date={date}
        onDateChange={setDate}
        events={events}
        resources={resources}
        onEventClick={setClicked}
        withInteractiveBackgroundEvents
      />
    </>
  );
}

리사이즈 가능한 배경 이벤트 (Resizable background events)

withEventResize를 withInteractiveBackgroundEvents와 함께 설정하면 배경 이벤트가 시작·종료 가장자리에 리사이즈 핸들을 렌더링해요. 핸들을 끌면 일반 이벤트와 마찬가지로 onEventResize가 배경 이벤트로 호출돼요. 핸들러에서 event.display === 'background'로 구분해요.

종일 배경 이벤트는 절대 리사이즈할 수 없어요. 배경 이벤트를 고정하고 일반 이벤트만 리사이즈 가능하게 하려면 canResizeEvent에서 false를 반환해요:

<ResourcesDayView
  events={events}
  resources={resources}
  withEventResize
  withInteractiveBackgroundEvents
  canResizeEvent={(event) => event.display !== 'background'}
/>

시간 범위와 간격 (Time range and intervals)

startTime, endTime, intervalMinutes props로 표시할 시간 범위와 슬롯 세밀도를 제어할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

다중 시간 간격 (Multi-hour intervals)

intervalMinutes는 시간에 균등하게 나누어지는 값(예: 15 또는 30) 또는 정수 시간(예: 120 또는 240)을 받아요. 다중 시간 간격은 각각 여러 시간을 포괄하는 더 넓은 열을 렌더링하며, 긴 시간 범위를 압축적으로 표시할 때 유용해요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { SegmentedControl, Stack } from '@mantine/core';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [intervalMinutes, setIntervalMinutes] = useState('120');

  return (
    <Stack>
      <SegmentedControl
        value={intervalMinutes}
        onChange={setIntervalMinutes}
        data={[
          { value: '60', label: '1 hour' },
          { value: '120', label: '2 hours' },
          { value: '240', label: '4 hours' },
        ]}
      />
      <ResourcesDayView
        date={date}
        onDateChange={setDate}
        events={events}
        resources={resources}
        intervalMinutes={Number(intervalMinutes)}
      />
    </Stack>
  );
}

시작 스크롤 시간 (Start scroll time)

startScrollTime prop으로 초기 가로 스크롤 위치를 특정 시간으로 설정할 수 있어요. 값은 HH:mm:ss 형식이어야 해요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

슬롯 너비와 행 높이 (Slot width and row height)

slotWidth로 각 타임 슬롯 열의 너비를, rowHeight로 각 리소스 행의 높이를 제어할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

커스텀 리소스 라벨 (Custom resource label)

renderResourceLabel prop으로 왼쪽 열의 리소스 라벨 렌더링을 커스터마이즈할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { Stack, Text } from '@mantine/core';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      renderResourceLabel={(resource) => (
        <Stack gap={0}>
          <Text fw={500}>{resource.label}</Text>
          <Text size="xs">Floor 2</Text>
        </Stack>
      )}
    />
  );
}

리소스 그룹 (Resource groups)

groups prop으로 리소스를 라벨이 있는 헤더 아래에 그룹화할 수 있어요. 그룹 라벨은 리소스 라벨 왼쪽에 열로 표시되며 리소스에 걸쳐 세로로 펼쳐져요. renderGroupLabel로 그룹 라벨 렌더링을, groupLabelWidth로 그룹 열 너비를 커스터마이즈해요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView, ScheduleResourceGroup } from '@mantine/schedule';
import { events } from './data';

const resources = [
  { id: 'tokyo', label: 'Meeting room: Tokyo' },
  { id: 'paris', label: 'Meeting room: Paris' },
  { id: 'new-york', label: 'Meeting room: New York' },
  { id: 'london', label: 'Meeting room: London' },
  { id: 'overflow', label: 'Overflow room' },
];

const groups: ScheduleResourceGroup[] = [
  { label: 'Floor 1', resourceIds: ['tokyo', 'paris'] },
  { label: 'Floor 2', resourceIds: ['new-york', 'london'] },
];

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      groups={groups}
    />
  );
}

커스텀 이벤트 렌더링 (Custom event rendering)

renderEvent prop으로 이벤트 렌더링을 완전히 커스터마이즈할 수 있어요.

import { useState } from 'react';
import dayjs from 'dayjs';
import { HoverCard, UnstyledButton } from '@mantine/core';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';
import { EventDetails } from './EventDetails';
import { events as initialEvents, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState(initialEvents);

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      onEventDrop={({ eventId, newStart, newEnd, resourceId }) => {
        setEvents((current) =>
          current.map((event) =>
            event.id === eventId
              ? { ...event, start: newStart, end: newEnd, resourceId }
              : event
          )
        );
      }}
      renderEvent={(event, props) => (
        <HoverCard>
          <HoverCard.Target>
            <UnstyledButton {...props}>{event.title}</UnstyledButton>
          </HoverCard.Target>
          <HoverCard.Dropdown>
            <EventDetails event={event} />
          </HoverCard.Dropdown>
        </HoverCard>
      )}
    />
  );
}

드래그 앤 드롭 (Drag and drop)

withEventsDragAndDrop prop으로 리소스 간 드래그 앤 드롭을 활성화해요. onEventDrop 콜백은 다섯 번째 인자로 대상 resourceId를 받아 이벤트의 리소스 할당을 갱신할 수 있게 해줘요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';
import { events as initialEvents, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState(initialEvents);

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      withEventsDragAndDrop
      onEventDrop={({ eventId, newStart, newEnd, resourceId }) => {
        setEvents((current) =>
          current.map((event) =>
            event.id === eventId
              ? { ...event, start: newStart, end: newEnd, resourceId }
              : event
          )
        );
      }}
    />
  );
}

드래그 간격 (Drag interval)

기본적으로 이벤트는 드래그 앤 드롭으로 이동할 때 intervalMinutes에 스냅돼요. 드래그에 다른 스냅 단계를 사용하려면 eventDragInterval을 설정해요. 예를 들어 30분 슬롯 그리드에서 이벤트를 15분 단위로 이동할 수 있게 해요. 고스트 미리보기가 이벤트가 위치할 곳을 보여줘요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';
import { events as initialEvents, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState(initialEvents);

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      intervalMinutes={30}
      eventDragInterval={15}
      withEventsDragAndDrop
      onEventDrop={({ eventId, newStart, newEnd, resourceId }) => {
        setEvents((current) =>
          current.map((event) =>
            event.id === eventId
              ? { ...event, start: newStart, end: newEnd, resourceId }
              : event
          )
        );
      }}
    />
  );
}

외부 드래그 앤 드롭 (External drag and drop)

onExternalEventDrop prop으로 외부 항목을 일정에 끌어올 수 있어요.

import { useRef, useState } from 'react';
import dayjs from 'dayjs';
import { Box, Grid, Text } from '@mantine/core';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';

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

const resources = [
  { id: 'tokyo', label: 'Tokyo' },
  { id: 'paris', label: 'Paris' },
  { id: 'new-york', label: 'New York' },
  { id: 'london', label: 'London' },
];

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 [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState([]);
  const nextId = useRef(1);

  const handleExternalDrop = ({
    dataTransfer,
    dropDateTime,
    resourceId,
  }: {
    dataTransfer: DataTransfer;
    dropDateTime: string;
    resourceId?: string | number;
  }) => {
    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,
        resourceId,
      },
    ]);
  };

  const handleEventDrop = ({
    eventId,
    newStart,
    newEnd,
    resourceId,
  }: {
    eventId: string | number;
    newStart: string;
    newEnd: string;
    event: ScheduleEventData;
    resourceId?: string | number;
  }) => {
    setEvents((prev) =>
      prev.map((event) =>
        event.id === eventId ? { ...event, start: newStart, end: newEnd, resourceId } : 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}>
        <ResourcesDayView
          date={date}
          onDateChange={setDate}
          events={events}
          resources={resources}
          onExternalEventDrop={handleExternalDrop}
          onEventDrop={handleEventDrop}
        />
      </Grid.Col>
    </Grid>
  );
}

양방향 드래그 앤 드롭 (Bidirectional drag and drop)

이벤트는 일정 안에서뿐 아니라 외부 영역으로도 드래그할 수 있어요. withEventsDragAndDrop과 onExternalEventDrop을 함께 사용하면 양방향 드래그 앤 드롭을 활성화할 수 있어요.

import { useRef, useState } from 'react';
import dayjs from 'dayjs';
import { Box, Grid, Text } from '@mantine/core';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';

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

const resources = [
  { id: 'tokyo', label: 'Tokyo' },
  { id: 'paris', label: 'Paris' },
  { id: 'new-york', label: 'New York' },
  { id: 'london', label: 'London' },
];

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 [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [sidebarItems, setSidebarItems] = useState(initialSidebarItems);
  const [events, setEvents] = useState([]);
  const [sidebarHighlight, setSidebarHighlight] = useState(false);
  const nextId = useRef(1);

  const handleExternalDrop = ({
    dataTransfer,
    dropDateTime,
    resourceId,
  }: {
    dataTransfer: DataTransfer;
    dropDateTime: string;
    resourceId?: string | number;
  }) => {
    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,
        resourceId,
      },
    ]);
    setSidebarItems((prev) => prev.filter((item) => item.title !== data.title));
  };

  const handleEventDrop = ({
    eventId,
    newStart,
    newEnd,
    resourceId,
  }: {
    eventId: string | number;
    newStart: string;
    newEnd: string;
    event: ScheduleEventData;
    resourceId?: string | number;
  }) => {
    setEvents((prev) =>
      prev.map((event) =>
        event.id === eventId ? { ...event, start: newStart, end: newEnd, resourceId } : 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}>
        <ResourcesDayView
          date={date}
          onDateChange={setDate}
          events={events}
          resources={resources}
          onExternalEventDrop={handleExternalDrop}
          onEventDrop={handleEventDrop}
          withEventsDragAndDrop
        />
      </Grid.Col>
    </Grid>
  );
}

이벤트 리사이즈 (Event resize)

withEventResize prop으로 이벤트 리사이즈를 활성화해요. 이벤트는 왼쪽이나 오른쪽 가장자리를 끌어 리사이즈할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';
import { events as initialEvents, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState(initialEvents);

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      withEventResize
      onEventResize={({ eventId, newStart, newEnd }) => {
        setEvents((current) =>
          current.map((event) =>
            event.id === eventId
              ? { ...event, start: newStart, end: newEnd }
              : event
          )
        );
      }}
    />
  );
}

리사이즈 간격 (Resize interval)

기본적으로 이벤트는 리사이즈할 때 intervalMinutes에 스냅돼요. eventResizeInterval로 다른 스냅 단계를 설정할 수 있어요. 예를 들어 30분 슬롯 그리드에서 이벤트를 15분 단위로 리사이즈할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';
import { events as initialEvents, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState(initialEvents);

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      intervalMinutes={30}
      eventResizeInterval={15}
      withEventResize
      onEventResize={({ eventId, newStart, newEnd }) => {
        setEvents((current) =>
          current.map((event) =>
            event.id === eventId
              ? { ...event, start: newStart, end: newEnd }
              : event
          )
        );
      }}
    />
  );
}

이벤트 폼 (Event form)

타임 슬롯을 클릭하거나 드래그해 범위를 선택하면 새 이벤트를 만들 수 있어요. 기존 이벤트를 클릭하면 편집할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { Select } from '@mantine/core';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';
import { EventData, EventForm } from './EventForm';
import { events as initialEvents, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  const [events, setEvents] = useState(initialEvents);
  const [formOpened, setFormOpened] = useState(false);
  const [selectedEventData, setSelectedEventData] = useState(null);
  const [selectedResourceId, setSelectedResourceId] = useState(
    String(resources[0].id)
  );

  const handleTimeSlotClick = ({
    slotStart,
    slotEnd,
    resourceId,
  }: {
    slotStart: string;
    slotEnd: string;
    nativeEvent: React.MouseEvent;
    resourceId?: string | number;
  }) => {
    setSelectedResourceId(resourceId ? String(resourceId) : String(resources[0].id));
    setSelectedEventData({
      title: '',
      start: new Date(slotStart),
      end: new Date(slotEnd),
      color: 'blue',
    });
    setFormOpened(true);
  };

  const handleEventClick = (event: ScheduleEventData) => {
    setSelectedResourceId(event.resourceId ? String(event.resourceId) : String(resources[0].id));
    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) {
      setEvents((prev) =>
        prev.map((event) =>
          event.id === values.id
            ? {
                ...event,
                title: values.title,
                start: dayjs(values.start).format('YYYY-MM-DD HH:mm:ss'),
                end: dayjs(values.end).format('YYYY-MM-DD HH:mm:ss'),
                color: values.color || 'blue',
                resourceId: selectedResourceId || resources[0].id,
              }
            : event
        )
      );
    } else {
      setEvents((prev) => [
        ...prev,
        {
          id: Date.now(),
          title: values.title,
          start: dayjs(values.start).format('YYYY-MM-DD HH:mm:ss'),
          end: dayjs(values.end).format('YYYY-MM-DD HH:mm:ss'),
          color: values.color || 'blue',
          resourceId: selectedResourceId || resources[0].id,
        },
      ]);
    }
  };

  const handleSlotDragEnd = ({
    rangeStart,
    rangeEnd,
    resourceId,
  }: {
    rangeStart: string;
    rangeEnd: string;
    resourceId?: string | number;
  }) => {
    setSelectedResourceId(resourceId ? String(resourceId) : String(resources[0].id));
    setSelectedEventData({
      title: '',
      start: new Date(rangeStart),
      end: new Date(rangeEnd),
      color: 'blue',
    });
    setFormOpened(true);
  };

  const handleDeleteEvent = () => {
    if (selectedEventData?.id) {
      setEvents((prev) => prev.filter((event) => event.id !== selectedEventData.id));
    }
  };

  return (
    <>
      <ResourcesDayView
        date={date}
        onDateChange={setDate}
        events={events}
        resources={resources}
        onTimeSlotClick={handleTimeSlotClick}
        onEventClick={handleEventClick}
        onSlotDragEnd={handleSlotDragEnd}
        withDragSlotSelect
      />
      <EventForm
        setFormOpened={setFormOpened}
        onExitTransitionEnd={() => setSelectedEventData(null)}
        values={selectedEventData}
        onSubmit={handleSubmit}
        onDelete={selectedEventData?.id ? handleDeleteEvent : undefined}
      >
        <Select
          data={resources.map((r) => ({ value: String(r.id), label: String(r.label) }))}
          value={selectedResourceId}
          onChange={setSelectedResourceId}
        />
      </EventForm>
    </>
  );
}

영업 시간 (Business hours)

highlightBusinessHours prop으로 모든 타임 슬롯 열에서 영업 시간과 비영업 시간을 시각적으로 구분할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      highlightBusinessHours
      businessHours={['09:00:00', '17:00:00']}
    />
  );
}

현재 시간 표시기 (Current time indicator)

withCurrentTimeIndicator로 현재 시간에 선을 표시할 수 있어요. withCurrentTimeBubble={false}로 시간 버블 라벨을 숨길 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      withCurrentTimeIndicator
      withCurrentTimeBubble
    />
  );
}

다른 시간대의 현재 시간 표시기 (Current time indicator in a different timezone)

@mantine/schedule은 시간대에 구애받지 않는 YYYY-MM-DD HH:mm:ss 문자열과 함께 동작하며 자체적으로 시간대 변환을 수행하지 않아요. 기본적으로 현재 시간 표시기는 사용자의 로컬 시간을 기준으로 배치돼요.

다른 시간대에 표시기를 표시하려면 getCurrentTime prop을 사용해요. 이 prop은 매 틱(tick)마다 호출되는 현재 시간 반환 함수이므로 표시기가 자동으로 계속 갱신돼요. 아래 예시는 dayjs timezone 플러그인으로 현재 시간을 선택한 시간대로 변환해요.

import { useState } from 'react';
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
import { Select, Stack } from '@mantine/core';
import { ResourcesDayView } from '@mantine/schedule';
import { getEvents, resources } from './data';

dayjs.extend(utc);
dayjs.extend(timezone);

const timezones = ['UTC', 'America/New_York', 'Europe/Berlin', 'Asia/Kolkata', 'Asia/Tokyo'];

function Demo() {
  const [tz, setTz] = useState('UTC');

  // getCurrentTime is called on every tick, so the indicator keeps updating
  const getCurrentTime = () => dayjs().tz(tz).format('YYYY-MM-DD HH:mm:ss');
  const currentDate = getCurrentTime().split(' ')[0];

  return (
    <Stack>
       setTz(value!)}
        allowDeselect={false}
      />
      <ResourcesDayView
        events={events}
        resources={resources}
        getCurrentTime={getCurrentTime}
        date={currentDate}
      />
    </Stack>
  );
}

이벤트 권한 (Event permissions)

canDragEvent과 canResizeEvent로 드래그하거나 리사이즈할 수 있는 이벤트를 제어할 수 있어요. payload.locked가 true로 설정된 이벤트는 이동하거나 리사이즈할 수 없어요.

import { useState } from 'react';
import dayjs from 'dayjs';
import { ResourcesDayView, ScheduleEventData } from '@mantine/schedule';

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

const resources = [
  { id: 'tokyo', label: 'Meeting room: Tokyo' },
  { id: 'paris', label: 'Meeting room: Paris' },
  { id: 'new-york', label: 'Meeting room: New York' },
];

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Draggable & Resizable',
    start: `${today} 09:00:00`,
    end: `${today} 10:00:00`,
    color: 'blue',
    resourceId: 'tokyo',
  },
  {
    id: 2,
    title: 'Locked Event',
    start: `${today} 11:00:00`,
    end: `${today} 12:00:00`,
    color: 'gray',
    resourceId: 'paris',
    payload: { locked: true },
  },
];

function Demo() {
  const [events, setEvents] = useState(initialEvents);

  const handleEventDrop = ({
    eventId,
    newStart,
    newEnd,
    resourceId,
  }: {
    eventId: string | number;
    newStart: string;
    newEnd: string;
    event: ScheduleEventData;
    resourceId?: string | number;
  }) => {
    setEvents((prev) =>
      prev.map((event) =>
        event.id === eventId ? { ...event, start: newStart, end: newEnd, resourceId } : event
      )
    );
  };

  const handleEventResize = ({ eventId, newStart, newEnd }: { eventId: string | number; newStart: string; newEnd: string; event: ScheduleEventData }) => {
    setEvents((prev) =>
      prev.map((event) =>
        event.id === eventId ? { ...event, start: newStart, end: newEnd } : event
      )
    );
  };

  return (
    <ResourcesDayView
      events={events}
      resources={resources}
      withDragDrop
      withEventResize
      onEventDrop={handleEventDrop}
      onEventResize={handleEventResize}
      canDragEvent={(event) => !event.payload?.locked}
      canResizeEvent={(event) => !event.payload?.locked}
    />
  );
}

반복 이벤트 (Recurring events)

ResourcesDayView는 표시된 날짜에 대해 반복 이벤트를 자동으로 확장해요. 전체 문서는 반복 이벤트 가이드를 참고해요.

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

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

const resources = [
  { id: 'tokyo', label: 'Meeting room: Tokyo' },
  { id: 'paris', label: 'Meeting room: Paris' },
];

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')} 11:00:00`,
    color: 'blue',
    resourceId: 'tokyo',
    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} 16:00:00`,
    color: 'grape',
    resourceId: 'tokyo',
    recurringEventId: 'daily-sync-series',
    recurrenceId: `${today} 09:00:00`,
  },
];

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

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

타임 슬롯당 최대 이벤트 수 (Max events per time slot)

maxEventsPerTimeSlot prop으로 타임 슬롯당 표시되는 겹치는 이벤트 수를 제한할 수 있어요. 제한을 초과하면 "+N more" 표시기가 표시돼요. 표시기를 클릭하면 그룹의 모든 이벤트가 담긴 팝오버가 열려요. moreEventsProps로 팝오버 동작을 커스터마이즈할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView, ScheduleEventData, ScheduleResourceData } from '@mantine/schedule';

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

const resources: ScheduleResourceData[] = [
  { id: 'tokyo', label: 'Meeting room: Tokyo' },
  { id: 'paris', label: 'Meeting room: Paris' },
];

const events: ScheduleEventData[] = [
  // ... many overlapping events per resource
];

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      maxEventsPerTimeSlot={2}
    />
  );
}

반경 (Radius)

radius prop으로 이벤트의 테두리 반경을 커스터마이즈할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

스크롤 영역 props (Scroll area props)

scrollAreaProps로 스크롤 영역을 커스터마이즈할 수 있어요. 예를 들어 스크롤바 크기를 키우거나 스크롤바를 오프셋할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

지역화 (Localization)

locale prop으로 날짜 형식 로케일을, slotLabelFormat으로 시간 형식을, labels로 UI 라벨을 재정의할 수 있어요.

import 'dayjs/locale/es';
import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

정적 모드 (Static mode)

mode="static"으로 설정하면 모든 상호작용을 비활성화해요. 이벤트와 타임 슬롯이 비상호작용이 되어 표시 전용 뷰에 유용해요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesDayView } from '@mantine/schedule';
import { events, resources } from './data';

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));

  return (
    <ResourcesDayView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      mode="static"
    />
  );
}

더 알아보기 (Learn more)