ResourcesMonthView

ResourcesMonthView

리소스 간 일정 관리를 위해 리소스 레인(lane)을 사용하는 월별 뷰 컴포넌트예요. @mantine/schedule 패키지의 컴포넌트예요. 각 날짜 셀에 리소스 레인이 포함된 월 그리드를 표시해요.

출처: 문서

본문

사용법 (Usage)

ResourcesMonthView는 각 날짜 셀에 리소스 레인이 포함된 월 그리드를 표시해요. 이벤트는 리소스의 레인 안에 표시되어 한 달 동안의 리소스 활용도를 쉽게 파악할 수 있어요.

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

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

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

정적 모드 (Static mode)

mode="static"으로 설정하면 모든 상호작용을 비활성화해요.

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

드래그 앤 드롭 (Drag and drop)

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

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, 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 (
    <ResourcesMonthView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      withEventsDragAndDrop
      startScrollDate={dayjs().format('YYYY-MM-DD')}
      onEventDrop={({ eventId, newStart, newEnd, resourceId }) => {
        setEvents((current) =>
          current.map((event) =>
            event.id === eventId
              ? { ...event, start: newStart, end: newEnd, resourceId }
              : event
          )
        );
      }}
    />
  );
}

이벤트 리사이즈 (Event resize)

withEventResize prop으로 이벤트 리사이즈를 활성화해요. 이벤트는 시작 또는 종료 가장자리를 끌어 리사이즈할 수 있으며, onEventResize 콜백이 갱신된 시작·종료 날짜로 호출돼요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, 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 (
    <ResourcesMonthView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      withEventResize
      startScrollDate={dayjs().format('YYYY-MM-DD')}
      onEventResize={({ eventId, newStart, newEnd }) => {
        setEvents((current) =>
          current.map((event) =>
            event.id === eventId
              ? { ...event, start: newStart, end: newEnd }
              : event
          )
        );
      }}
    />
  );
}

리사이즈는 하루 단위로 스냅되며 이벤트의 원래 시간을 보존해요. 예를 들어 17:00에 끝나는 이벤트의 종료 가장자리를 다른 날로 끌어도 17:00 종료 시간이 유지돼요. 정확히 자정에 끝나는 이벤트는 이전 날에 끝나는 것으로 취급되므로 리사이즈 후에도 종료 시간은 배타적(exclusive)으로 유지돼요.

리사이즈 핸들은 이벤트가 실제로 시작하고 끝나는 가장자리에만 렌더링돼요. 표시된 달 밖으로 계속되거나 여러 행에 걸쳐 분할된 이벤트는 해당 가장자리에 핸들을 렌더링하지 않아요. "+N more" 표시기 뒤에 숨겨진 이벤트는 리사이즈할 수 없어요.

리사이즈는 이벤트의 리소스를 변경하지 않아요. 다른 리소스로 이벤트를 옮기려면 이벤트 자체를 드래그해요.

canResizeEvent로 리사이즈할 수 있는 이벤트를 제어할 수 있어요.

이벤트 폼 (Event form)

날짜 셀을 클릭하면 새 이벤트를 만들고, 기존 이벤트를 클릭하면 편집할 수 있어요. withDragSlotSelect와 onSlotDragEnd를 사용하면 날짜 셀을 가로질러 드래그해 생성할 수 있어요. onSlotDragEnd 콜백은 드래그가 시작된 리소스의 resourceId를 포함해요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { Select } from '@mantine/core';
import { ResourcesMonthView, 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(null);

  const handleDayClick = ({
    date: clickedDate,
    resourceId,
  }: {
    date: string;
    nativeEvent: React.MouseEvent;
    resourceId?: string | number;
  }) => {
    setSelectedResourceId(resourceId ? String(resourceId) : null);
    setSelectedEventData({
      title: '',
      start: dayjs(clickedDate).startOf('day').toDate(),
      end: dayjs(clickedDate).endOf('day').toDate(),
      color: 'blue',
    });
    setFormOpened(true);
  };

  const handleEventClick = (event: ScheduleEventData) => {
    setSelectedResourceId(event.resourceId ? String(event.resourceId) : null);
    setSelectedEventData({
      id: event.id,
      title: event.title,
      start: new Date(event.start),
      end: new Date(event.end),
      color: event.color || 'blue',
    });
    setFormOpened(true);
  };

  const handleSlotDragEnd = ({
    rangeStart,
    rangeEnd,
    resourceId,
  }: {
    rangeStart: string;
    rangeEnd: string;
    resourceId?: string | number;
  }) => {
    setSelectedResourceId(resourceId ? String(resourceId) : null);
    setSelectedEventData({
      title: '',
      start: new Date(rangeStart),
      end: new Date(rangeEnd),
      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).toISOString(),
                end: dayjs(values.end).toISOString(),
                color: values.color || 'blue',
                resourceId: selectedResourceId || event.resourceId,
              }
            : event
        )
      );
    } else {
      setEvents((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',
          resourceId: selectedResourceId || undefined,
        },
      ]);
    }
  };

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

  return (
    <>
      <ResourcesMonthView
        date={date}
        onDateChange={setDate}
        events={events}
        resources={resources}
        onDayClick={handleDayClick}
        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: r.label }))}
          value={selectedResourceId}
          onChange={setSelectedResourceId}
        />
      </EventForm>
    </>
  );
}

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

renderEvent prop으로 이벤트 렌더링 방식을 커스터마이즈할 수 있어요. 아래 예시는 호버 시 이벤트 세부 정보를 보여주는 HoverCard를 사용해요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { HoverCard, UnstyledButton } from '@mantine/core';
import { ResourcesMonthView, 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 (
    <ResourcesMonthView
      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>
      )}
      startScrollDate={dayjs().format('YYYY-MM-DD')}
    />
  );
}

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

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

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

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

  return (
    <ResourcesMonthView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      renderResourceLabel={(resource: ScheduleResourceData) => (
        <Stack gap={0}>
          <Text fw={500}>{resource.label}</Text>
          <Text size="xs">Office</Text>
        </Stack>
      )}
      startScrollDate={dayjs().format('YYYY-MM-DD')}
    />
  );
}

리소스 그룹 (Resource groups)

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

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, 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: 'overflow', label: 'Overflow room' },
];

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

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

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

지역화 (Localization)

locale prop으로 언어를 변경할 수 있어요. labels prop으로 내장 라벨을 재정의할 수 있어요.

import 'dayjs/locale/es';

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

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

  return (
    <ResourcesMonthView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      labels={{
        moreEvents: (hiddenEventsCount) => `+${hiddenEventsCount} más`,
      }}
      startScrollDate={dayjs().format('YYYY-MM-DD')}
    />
  );
}

반복 이벤트 (Recurring events)

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

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView } 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')} 09:30: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} 14:30:00`,
    color: 'grape',
    resourceId: 'tokyo',
    recurringEventId: 'daily-sync-series',
    recurrenceId: `${today} 09:00:00`,
  },
];

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

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

셀당 최대 이벤트 수 (Max events per cell)

maxEventsPerTimeSlot prop으로 각 셀에 표시할 이벤트 수를 제한할 수 있어요. 제한보다 이벤트가 많으면 "+more" 표시기가 표시돼요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, 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 events per resource
];

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

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

날짜 너비와 행 높이 (Day width and row height)

dayWidth와 rowHeight props로 날짜 열과 리소스 행의 크기를 커스터마이즈할 수 있어요.

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

시작 스크롤 날짜 (Start scroll date)

startScrollDate prop으로 초기 렌더링 시 특정 날짜로 스크롤할 수 있어요. 달에 날짜가 많고 오늘과 같은 특정 날짜에서 시작하고 싶을 때 유용해요.

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

주말 없이 (Without weekend days)

withWeekendDays={false}로 월 그리드에서 주말 열을 숨길 수 있어요.

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

더 많은 이벤트 표시기 (More events indicator)

날짜 셀에 maxEventsPerTimeSlot보다 많은 이벤트가 있으면 "+X more" 표시기가 표시돼요. 표시기를 클릭하면 그 날의 모든 이벤트를 볼 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, 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 events on the same day
];

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

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

더 많은 이벤트 props (More events props)

moreEventsProps로 더 많은 이벤트 드롭다운을 커스터마이즈할 수 있어요. 예를 들어 팝오버 대신 모달을 사용할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { ResourcesMonthView, 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 events on the same day
];

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

  return (
    <ResourcesMonthView
      date={date}
      onDateChange={setDate}
      events={events}
      resources={resources}
      moreEventsProps={{ transitionProps: { duration: 0 } }}
    />
  );
}

커스텀 헤더 (Custom header)

withHeader={false}로 기본 헤더를 숨기고 ScheduleHeader를 사용해 내비게이션 컨트롤이 있는 커스텀 헤더를 만들 수 있어요.

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

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

  return (
    <>
      <ScheduleHeader>
        <ScheduleHeader.Control
          onClick={() =>
            setDate(dayjs(date).subtract(1, 'month').startOf('month').format('YYYY-MM-DD') as DateStringValue)
          }
        >
          ‹
        </ScheduleHeader.Control>
        <ScheduleHeader.Select
          onYearChange={(year) =>
            setDate(dayjs(date).year(year).startOf('month').format('YYYY-MM-DD') as DateStringValue)
          }
          onMonthChange={(month) =>
            setDate(dayjs(date).month(month).startOf('month').format('YYYY-MM-DD') as DateStringValue)
          }
        />
        <ScheduleHeader.Control
          onClick={() =>
            setDate(dayjs(date).add(1, 'month').startOf('month').format('YYYY-MM-DD') as DateStringValue)
          }
        >
          ›
        </ScheduleHeader.Control>
        <ScheduleHeader.Today
          onClick={() => setDate(dayjs().format('YYYY-MM-DD') as DateStringValue)}
        />
      </ScheduleHeader>
      <ResourcesMonthView
        date={date}
        onDateChange={setDate}
        events={events}
        resources={resources}
        withHeader={false}
      />
    </>
  );
}

반경 (Radius)

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

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

스크롤 영역 props (Scroll area props)

scrollAreaProps로 스크롤 영역 동작을 커스터마이즈할 수 있어요.

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

더 알아보기 (Learn more)