DayView

DayView

단일 날짜의 이벤트를 시간 슬롯과 함께 표시하는 독립 실행형 일별 뷰 컴포넌트예요. @mantine/schedule 패키지의 컴포넌트예요.

출처: 문서

본문

사용법 (Usage)

DayView는 시간 슬롯과 함께 하루의 이벤트를 표시해요. 종일 이벤트, 겹치는 이벤트, 드래그 앤 드롭, 커스텀 시간 범위 등을 지원해요.

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

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

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

시간 범위 (Time range)

startTime과 endTime props로 표시할 시간 범위를 설정해요. 시간은 HH:mm:ss 형식이어야 해요. 영업 시간 등 특정 시간대에 집중할 때 유용해요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  return (
    <DayView
      events={events}
      startTime="09:00:00"
      endTime="17:00:00"
    />
  );
}

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

startScrollTime prop으로 초기 스크롤 위치를 특정 시간으로 설정할 수 있어요. 값은 HH:mm:ss 형식이어야 해요. 뷰를 자정 대신 특정 시간(예: 영업 시간 시작)에서 열고 싶을 때 유용해요.

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

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 (
    <DayView
      events={events}
      startScrollTime="09:00:00"
      onEventDrop={handleEventDrop}
    />
  );
}

간격 분 (Interval minutes)

intervalMinutes prop은 시간 슬롯의 세밀도를 제어해요. 기본값은 15분이에요. 주로 사용하는 값은 15, 30, 60분이에요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  return (
    <DayView
      events={events}
      intervalMinutes={30}
    />
  );
}

서브 아워 그리드 라인 (Sub-hour grid lines)

intervalMinutes가 60보다 작을 때 withSubHourGridLines={false}로 설정하면 시간당 하나의 그리드 라인만 표시하면서도 이벤트 생성·리사이즈에는 더 작은 간격을 유지할 수 있어요. Google 캘린더와 같은 레이아웃을 구현할 때 유용해요. 이벤트는 15분이나 30분 단위로 스냅되지만 그리드는 시간 단위 라인으로 깔끔하게 유지돼요.

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

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

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Morning Standup',
    start: `${today} 09:00:00`,
    end: `${today} 09:30:00`,
    color: 'blue',
  },
];

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

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

  // Events snap to 15 minutes increments, but only one grid line per hour is displayed
  return (
    <DayView
      events={events}
      intervalMinutes={15}
      withSubHourGridLines={false}
      onEventResize={handleEventResize}
    />
  );
}

종일 이벤트 (All-day events)

하루 전체에 걸친 이벤트는 상단의 전용 종일(All day) 섹션에 표시돼요. 종일 이벤트가 2개보다 많아지면 컴포넌트는 "More events" 표시기를 보여줘요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

겹치는 이벤트 (Overlapping events)

여러 이벤트가 시간상 겹치면 자동으로 적절한 너비와 오프셋으로 나란히 배치돼요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

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

withCurrentTimeIndicator를 설정하면 현재 시간을 보여주는 라인이 표시돼요. 기본적으로 오늘 날짜에만 표시돼요. withCurrentTimeBubble={false}로 설정하면 시간 버블을 숨길 수 있어요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  return (
    <DayView
      events={events}
      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 { DayView } from '@mantine/schedule';
import { getEvents } 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}
      />
      <DayView
        events={events}
        getCurrentTime={getCurrentTime}
        date={currentDate}
      />
    </Stack>
  );
}

영업 시간 (Business hours)

highlightBusinessHours와 businessHours props로 영업 시간과 비영업 시간을 시각적으로 구분할 수 있어요. businessHours prop은 HH:mm:ss 형식의 시작·종료 시간 튜플을 받아요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  return (
    <DayView
      events={events}
      highlightBusinessHours
      businessHours={['09:00:00', '17:00:00']}
    />
  );
}

커스텀 타임 슬롯 props (Custom time slot props)

getTimeSlotProps를 사용하면 시간 범위를 기준으로 개별 타임 슬롯에 커스텀 props를 추가할 수 있어요. 이 함수는 YYYY-MM-DD HH:mm:ss 형식의 { start, end } 날짜시간 문자열을 받아 슬롯 엘리먼트에 펼칠 props 객체 또는 undefined를 반환해야 해요.

기본 9:00–17:00과 다른 커스텀 범위에 data-business-hours를 설정하거나 특정 슬롯에 커스텀 이벤트 핸들러를 연결할 때 유용해요. getTimeSlotProps가 반환한 onClick 같은 이벤트 핸들러는 내부 핸들러(onTimeSlotClick)와 합성돼요. 둘 다 서로를 덮어쓰지 않고 모두 실행돼요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  return (
    <DayView
      events={events}
      getTimeSlotProps={({ start, end }) => {
        const hour = parseInt(start.split(' ')[1], 10);
        if (hour >= 7 && hour < 9) {
          return { 'data-business-hours': true };
        }
        return undefined;
      }}
    />
  );
}

슬롯 높이 (Slot height)

slotHeight와 allDaySlotHeight props로 타임 슬롯과 종일 섹션의 높이를 커스터마이즈할 수 있어요. slotHeight는 1시간 슬롯의 높이를 나타내요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

헤더 없이 (Without header)

withHeader={false}로 설정하면 헤더 컨트롤을 숨길 수 있어요. 커스텀 헤더를 만들거나 뷰를 더 큰 컴포넌트에 내장할 때 유용해요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

커스텀 헤더 (Custom header)

ScheduleHeader 복합 컴포넌트를 자체 컨트롤과 함께 사용해 커스텀 헤더를 만들 수 있어요. 뷰에 withHeader={false}를 설정하고 헤더를 외부에서 구성해요.

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

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

  return (
    <>
      <ScheduleHeader>
        <ScheduleHeader.Control
          onClick={() => setDate(dayjs(date).subtract(1, 'day').format('YYYY-MM-DD') as DateStringValue)}
        >
          ‹
        </ScheduleHeader.Control>
        {dayjs(date).format('dddd, MMMM D, YYYY')}
        <ScheduleHeader.Control
          onClick={() => setDate(dayjs(date).add(1, 'day').format('YYYY-MM-DD') as DateStringValue)}
        >
          ›
        </ScheduleHeader.Control>
      </ScheduleHeader>
      <DayView
        date={date}
        onDateChange={setDate}
        events={events}
        withHeader={false}
      />
    </>
  );
}

헤더 형식 (Header format)

headerFormat prop으로 헤더의 날짜 형식을 커스터마이즈할 수 있어요. 형식은 dayjs 형식 문법을 사용하거나 포맷된 문자열을 반환하는 함수일 수 있어요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

슬롯 라벨 형식 (Slot label format)

slotLabelFormat prop은 시간 라벨의 형식을 제어해요. dayjs 형식 문자열이나 포맷된 문자열을 반환하는 함수를 받아요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  return (
    <DayView
      events={events}
      slotLabelFormat="h:mm A"
    />
  );
}

반경 (Radius)

radius prop으로 일별 뷰 컨테이너의 테두리 반경을 제어할 수 있어요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

겹침 모드 (Overlap mode)

eventOverlapMode prop은 시간상 겹치는 이벤트가 가로 공간을 나누는 방식을 제어해요:

  • columns (기본값) – 사용 가능한 너비를 겹치는 이벤트 사이에 균등하게 나눠요. 모든 이벤트가 완전히 보이지만, 더 많은 이벤트가 겹칠수록 각각은 더 좁아져요.
  • cascade – 각 이벤트는 이전 이벤트에서 들여쓰기(indent)되고 그 위에 그려지며 그룹의 오른쪽 가장자리까지 확장돼요. 얼마나 많은 이벤트가 겹치든 이벤트는 읽을 수 있는 너비를 유지하지만, 아래의 이벤트를 가리는 대가가 있어요.

데이터가 자주 큰 동시 이벤트 그룹을 만들 때 cascade를 선호하세요. columns는 각 이벤트를 읽을 수 없는 조각으로 줄여버릴 수 있기 때문이에요.

import { useState } from 'react';
import { SegmentedControl, Stack } from '@mantine/core';
import { DayView, ScheduleEventOverlapMode } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  const [mode, setMode] = useState('cascade');

  return (
    <Stack>
       setMode(value as ScheduleEventOverlapMode)}
        data={[
          { value: 'columns', label: 'columns' },
          { value: 'cascade', label: 'cascade' },
        ]}
      />
      <DayView events={events} eventOverlapMode={mode as ScheduleEventOverlapMode} />
    </Stack>
  );
}

들여쓰기는 그룹이 커질수록 자동으로 줄어들어 최상단 이벤트는 항상 사용 가능한 너비의 최소 60%를 유지해요. 가려진 이벤트도 읽고 상호작용할 수 있어요. 포인터를 올리면 그 위로 올라서 리사이즈 핸들에 도달할 수 있어요. 키보드 사용자는 Tab으로 이벤트에 포커스해 지연 없이 같은 결과를 얻을 수 있어요.

eventOverlapRaiseDelay prop은 이벤트 위에 포인터를 올려야 올라오기까지 걸리는 시간(밀리초)을 제어해요. 기본값 600은 그룹을 가로질러 특정 이벤트에 도달하기 위해 포인터를 움직일 때 지나치는 이벤트가 올라오지 않을 만큼 충분히 길어요. 더 작은 값으로 설정하면 가려진 이벤트가 더 빨리 드러나요.

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

function Demo() {
  return <DayView eventOverlapMode="cascade" eventOverlapRaiseDelay={200} />;
}

드래그 앤 드롭 (Drag and drop)

withDragDrop prop으로 드래그 앤 드롭을 활성화해요. onEventDrop 콜백으로 이벤트 드롭을 처리해요. 종일 이벤트는 드래그할 수 없어요.

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

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

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Morning Standup',
    start: `${today} 09:00:00`,
    end: `${today} 09:30:00`,
    color: 'blue',
  },
];

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 (
    <DayView
      events={events}
      onEventDrop={handleEventDrop}
      withDragDrop
    />
  );
}

드래그 간격 (Drag interval)

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

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

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

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Morning Standup',
    start: `${today} 09:00:00`,
    end: `${today} 09:30:00`,
    color: 'blue',
  },
];

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 (
    <DayView
      events={events}
      intervalMinutes={30}
      eventDragInterval={15}
      onEventDrop={handleEventDrop}
    />
  );
}

외부 드래그 앤 드롭 (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 { DayView, ScheduleEventData } from '@mantine/schedule';

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

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,
      },
    ]);
  };

  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}>
        <DayView events={events} onExternalEventDrop={handleExternalDrop} />
      </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 { DayView, ScheduleEventData } from '@mantine/schedule';

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

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}>
        <DayView
          events={events}
          onExternalEventDrop={handleExternalDrop}
          onEventDrop={handleEventDrop}
          withEventsDragAndDrop
        />
      </Grid.Col>
    </Grid>
  );
}

선택적 드래그 권한 (Selective drag permissions)

canDragEvent 콜백으로 드래그할 수 있는 이벤트를 제어할 수 있어요. 잠긴(locked) 또는 읽기 전용 이벤트를 구현할 때 유용해요.

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

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

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

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 (
    <DayView
      events={events}
      onEventDrop={handleEventDrop}
      withDragDrop
      canDragEvent={(event) => !event.payload?.locked}
    />
  );
}

이벤트 리사이즈 (Event resize)

withEventResize prop으로 이벤트 리사이즈를 활성화해요. 사용자는 이벤트의 위나 아래 가장자리를 끌어 시작 또는 종료 시간을 조정할 수 있어요. onEventResize 콜백으로 리사이즈를 처리해요.

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

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

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Morning Standup',
    start: `${today} 09:00:00`,
    end: `${today} 09:30:00`,
    color: 'blue',
  },
];

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

  const handleEventResize = ({ 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 (
    <DayView
      events={events}
      onEventResize={handleEventResize}
      withEventResize
    />
  );
}

리사이즈 간격 (Resize interval)

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

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

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

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Morning Standup',
    start: `${today} 09:00:00`,
    end: `${today} 09:30:00`,
    color: 'blue',
  },
];

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

  const handleEventResize = ({ 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 (
    <DayView
      events={events}
      intervalMinutes={30}
      eventResizeInterval={15}
      onEventResize={handleEventResize}
    />
  );
}

선택적 리사이즈 권한 (Selective resize permissions)

canResizeEvent 콜백으로 리사이즈할 수 있는 이벤트를 제어할 수 있어요. 리사이즈하면 안 되는 잠긴 또는 읽기 전용 이벤트를 구현할 때 유용해요.

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

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

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

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

  const handleEventResize = ({ 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 (
    <DayView
      events={events}
      onEventResize={handleEventResize}
      withEventResize
      canResizeEvent={(event) => !event.payload?.locked}
    />
  );
}

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

renderEventBody prop으로 이벤트가 표시되는 방식을 커스터마이즈할 수 있어요. 이벤트에 커스텀 아이콘, 배지, 또는 다른 콘텐츠를 추가할 수 있어요.

import dayjs from 'dayjs';
import { ClockIcon, MapPinIcon } from '@phosphor-icons/react';
import { Group, Text } from '@mantine/core';
import { DayView, ScheduleEventData } from '@mantine/schedule';

const events: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Morning Standup',
    start: `${today} 09:00:00`,
    end: `${today} 09:30:00`,
    color: 'blue',
    payload: { location: 'Room A' },
  },
];

function Demo() {
  return (
    <DayView
      events={events}
      renderEventBody={(event) => (
        <div>
          <Text fw={500}>{event.title}</Text>
          {event.payload?.location && (
            <Group gap={4}>
              <MapPinIcon size={12} />
              <Text>{event.payload.location}</Text>
            </Group>
          )}
        </div>
      )}
    />
  );
}

전체 이벤트 커스터마이즈 (Full event customization)

renderEvent prop으로 이벤트 렌더링을 완전히 커스터마이즈할 수 있어요. 이 함수는 첫 번째 인자로 이벤트 데이터를, 두 번째 인자로 이벤트 루트 엘리먼트에 전달될 모든 props(children 포함)를 받아요. HoverCard, Tooltip 또는 커스텀 래퍼 같은 커스텀 컴포넌트로 이벤트를 감쌀 수 있게 해줘요.

import { useState } from 'react';
import dayjs from 'dayjs';
import { HoverCard, UnstyledButton } from '@mantine/core';
import { DayView, ScheduleEventData } from '@mantine/schedule';
import { EventDetails } from './EventDetails';
import { eventsData } from './events';

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

  return (
    <DayView
      events={events}
      withDragDrop
      onEventDrop={({ eventId, newStart, newEnd }) => {
        setEvents((current) =>
          current.map((event) =>
            event.id === eventId
              ? {
                  ...event,
                  start: dayjs(newStart).format('YYYY-MM-DD HH:mm:ss'),
                  end: dayjs(newEnd).format('YYYY-MM-DD HH:mm:ss'),
                }
              : event
          )
        );
      }}
      renderEvent={(event, props) => (
        <HoverCard>
          <HoverCard.Target>
            <UnstyledButton {...props}>{event.title}</UnstyledButton>
          </HoverCard.Target>
          <HoverCard.Dropdown>
            <EventDetails event={event} />
          </HoverCard.Dropdown>
        </HoverCard>
      )}
    />
  );
}

반복 이벤트 (Recurring events)

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

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`,
  },
];

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

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

배경 이벤트 (Background events)

이벤트에 display="background"를 설정하면 일반 이벤트 뒤의 전체 너비, 반투명, 비상호작용 블록으로 렌더링돼요. 배경 이벤트는 부재, 점심 시간, 집중 시간 또는 다른 차단 기간을 표시할 때 유용해요. 기본적으로 클릭, 드래그, 리사이즈할 수 없어요.

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

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

const events = [
  {
    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',
  },
];

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

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

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

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

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

이 prop은 DayView, WeekView, MonthView, ResourcesDayView, ResourcesWeekView에서 지원돼요. YearView와 작은 화면에서 layout="responsive"로 Schedule이 렌더링하는 MobileMonthView는 배경 이벤트를 전혀 표시하지 않으므로 prop이 효과가 없어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { Text } from '@mantine/core';
import { DayView, ScheduleEventData } from '@mantine/schedule';

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

const events: ScheduleEventData[] = [
  {
    id: 'out-of-office',
    title: 'Out of office',
    start: `${today} 12:00:00`,
    end: `${today} 16:00:00`,
    color: 'red',
    display: 'background',
  },
];

function Demo() {
  const [date, setDate] = useState(today);
  const [clicked, setClicked] = useState<ScheduleEventData | null>(null);

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

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

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

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

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

드래그 앤 드롭 차단이 있는 커스텀 배경 이벤트 스타일 (Custom background event styles with drag and drop blocking)

Styles API로 배경 이벤트의 모양을 커스터마이즈하고 차단된 시간 범위로 일반 이벤트를 드롭하는 것을 막을 수 있어요. 이 예시는 대각선 빨간 선으로 차단된 시간을 표시하고 배경 이벤트와 겹치는 드롭을 거부해요.

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

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

const initialEvents: ScheduleEventData[] = [
  {
    id: 'blocked-time',
    title: 'Blocked',
    start: `${today} 12:00:00`,
    end: `${today} 14:00:00`,
    color: 'red',
    display: 'background',
  },
];

function overlapsBackground(
  newStart: string,
  newEnd: string,
  events: ScheduleEventData[]
) {
  return events.some(
    (e) =>
      e.display === 'background' && newStart < e.end && newEnd > e.start
  );
}

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

  const handleEventDrop = ({
    eventId,
    newStart,
    newEnd,
  }: {
    eventId: string | number;
    newStart: string;
    newEnd: string;
  }) => {
    if (overlapsBackground(newStart, newEnd, events)) {
      return;
    }

    setEvents((prev) =>
      prev.map((event) =>
        event.id === eventId
          ? { ...event, start: newStart, end: newEnd }
          : event
      )
    );
  };

  return (
    <DayView
      events={events}
      onEventDrop={handleEventDrop}
      withDragDrop
      withInteractiveBackgroundEvents
    />
  );
}

어젠다 뷰 (Agenda view)

withAgenda prop으로 헤더에 "Agenda" 버튼을 표시할 수 있어요. 클릭하면 현재 날짜의 이벤트를 목록으로 보여주는 AgendaView가 열려요.

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

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

const events = [
  {
    id: 'standup',
    title: 'Morning Standup',
    start: `${today} 09:00:00`,
    end: `${today} 09:30:00`,
    color: 'blue',
  },
];

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

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

정적 모드 (Static mode)

mode="static"으로 설정하면 모든 상호작용을 비활성화해요. 정적 모드에서는 이벤트와 타임 슬롯이 클릭, 드래그, 호버되지 않아요. 읽기 전용 디스플레이 또는 보고서에 유용해요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  return (
    <DayView events={events} mode="static" />
  );
}

커스텀 라벨 (Custom labels)

labels prop으로 기본 라벨을 국제화나 커스텀 텍스트로 재정의할 수 있어요.

import { DayView } from '@mantine/schedule';
import { events } from './data';

function Demo() {
  return (
    <DayView
      events={events}
      labels={{
        today: 'Hoy',
        allDay: 'Todo el día',
        week: 'Semana',
        month: 'Mes',
        year: 'Año',
      }}
    />
  );
}

지역화 (Localization)

locale prop으로 날짜 형식에 사용할 dayjs 로케일을 설정해요. labels prop과 결합하면 모든 UI 텍스트를 번역할 수 있어요.

import 'dayjs/locale/es';
import { DayView } from '@mantine/schedule';
import { events } from './data';

제어된 날짜 (Controlled date)

date prop과 onDateChange 콜백으로 날짜를 외부에서 제어할 수 있어요. 커스텀 내비게이션을 만들거나 다른 컴포넌트와 통합할 수 있게 해줘요.

import { useState } from 'react';
import dayjs from 'dayjs';
import { DayView } from '@mantine/schedule';
import { Button, Group, Text } from '@mantine/core';
import { events } from './data';

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

  return (
    <>
      <Group>
        <Button onClick={() => setDate(dayjs(date).subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss'))}>
          Previous Day
        </Button>
        <Text>{dayjs(date).format('MMMM D, YYYY')}</Text>
        <Button onClick={() => setDate(dayjs(date).add(1, 'day').format('YYYY-MM-DD HH:mm:ss'))}>
          Next Day
        </Button>
      </Group>
      <DayView events={events} date={date} onDateChange={setDate} />
    </>
  );
}

뷰 변경 (View change)

onViewChange prop으로 사용자가 뷰 선택 버튼을 클릭할 때 뷰 레벨 변경을 처리할 수 있어요.

import { useState } from 'react';
import { DayView, ScheduleViewLevel } from '@mantine/schedule';
import { Text } from '@mantine/core';
import { events } from './data';

function Demo() {
  const [view, setView] = useState('day');

  return (
    <>
      <Text>Selected view: {view}</Text>
      <DayView
        events={events}
        view={view as ScheduleViewLevel}
        onViewChange={setView as (v: ScheduleViewLevel) => void}
      />
    </>
  );
}

이벤트 생성 및 업데이트 (Create and update events)

withDragSlotSelect prop으로 사용자가 타임 슬롯을 가로질러 드래그해 시간 범위를 선택할 수 있게 해요. 드래그가 끝나면 onSlotDragEnd 콜백이 범위의 시작·종료 날짜로 호출돼요. onTimeSlotClick, onAllDaySlotClick, onEventClick 콜백과 결합하면 완전한 이벤트 생성·편집 경험을 구현할 수 있어요.

import dayjs from 'dayjs';
import { useState } from 'react';
import { DayView, ScheduleEventData } from '@mantine/schedule';
import { EventData, EventForm } from './EventForm';
import { events } from './events';

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

  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 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) {
      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',
              }
            : 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',
        },
      ]);
    }
  };

  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) {
      setEvents((prev) => prev.filter((event) => event.id !== selectedEventData.id));
    }
  };

  return (
    <>
      <DayView
        date={date}
        onDateChange={setDate}
        events={events}
        onTimeSlotClick={handleTimeSlotClick}
        onAllDaySlotClick={handleAllDaySlotClick}
        onEventClick={handleEventClick}
        onSlotDragEnd={handleSlotDragEnd}
        withDragSlotSelect
      />
      <EventForm
        setFormOpened={setFormOpened}
        onExitTransitionEnd={() => setSelectedEventData(null)}
        values={selectedEventData}
        onSubmit={handleSubmit}
        onDelete={selectedEventData?.id ? handleDeleteEvent : undefined}
      />
    </>
  );
}

반응형 스타일 (Responsive styles)

DayView는 반응형 스타일에 @container queries를 사용해요. 컴포넌트는 컨테이너 너비에 따라 레이아웃을 자동으로 조정하며, 작은 화면에서는 라벨을 숨기고 패딩을 줄여요. 컨테이너 쿼리는 모든 현대 브라우저에서 지원돼요.

접근성 (Accessibility)

포커스 관리 (Focus management)

DayView 컴포넌트에서 포커스는 효율적인 키보드 내비게이션을 제공하도록 관리돼요:

  • 처음 타임 슬롯만 탭 순서에 포함돼요 (tabIndex={0})
  • 다른 모든 타임 슬롯은 tabIndex={-1}이며 화살표 키 내비게이션으로만 도달할 수 있어요
  • 이 방식은 일정 내비게이션 시 탭 스톱 수를 줄여줘요

키보드 상호작용 (Keyboard interactions)

Key Description
ArrowDown 다음 타임 슬롯에 포커스
ArrowUp 이전 타임 슬롯에 포커스

슬롯 라벨 (Slot labels)

각 타임 슬롯 버튼에는 시간 범위를 포함한 완전한 슬롯 정보를 담은 aria-label 속성이 있어요 (예: "Time slot 08:00:00 - 09:00:00"). 이는 화면 리더 사용자에게 각 슬롯의 완전한 맥락을 제공해요.

더 알아보기 (Learn more)