MonthView

MonthView

달력 월 그리드에 이벤트를 표시하는 독립 실행형 월별 뷰 컴포넌트예요. @mantine/schedule 패키지의 컴포넌트예요. 각 날짜 셀에 이벤트 배지를 표시하고 드래그 앤 드롭을 지원해요.

출처: 문서

본문

사용법 (Usage)

MonthView는 달력 월 그리드에 이벤트를 표시해요. 각 날짜 셀에 이벤트 배지를 보여주며 드래그 앤 드롭을 지원해요.

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

function Demo() {
  return (
    <MonthView
      events={events}
      onEventClick={() => {}}
      onDateClick={() => {}}
    />
  );
}

주 번호 표시 (With week numbers)

withWeekNumbers를 설정하면 첫 번째 열에 주 번호를 표시해요.

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

요일 없이 (Without week days)

withWeekDays={false}로 요일 이름 행을 숨길 수 있어요.

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

주말 숨기기 (Hide weekend days)

withWeekendDays={false}로 주말을 숨길 수 있어요. 그리드는 나머지 열로 줄어들고, 숨겨진 날짜에 걸친 이벤트는 보이는 날짜로 잘려요. 주말로 간주되는 날은 weekendDays prop(또는 DatesProvider, 기본값 [0, 6])이 제어해요.

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

주의 첫 요일 (First day of week)

firstDayOfWeek로 주가 시작하는 요일을 제어해요.

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

요일 형식 (Weekday format)

weekdayFormat prop으로 요일 이름을 커스터마이즈해요.

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

일관된 주 (Consistent weeks)

consistentWeeks={false}로 현재 달에 날짜가 있는 주만 표시할 수 있어요.

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

function Demo() {
  const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
  return <MonthView date={date} onDateChange={setDate} consistentWeeks={false} />;
}

오늘 강조 (Highlight today)

highlightToday={false}로 오늘 날짜 강조를 비활성화할 수 있어요.

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

외부 날짜 없음 (Without outside days)

withOutsideDays={false}로 인접한 달의 날짜를 숨길 수 있어요.

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

반경 (Radius)

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

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

헤더 없이 (Without header)

withHeader={false}로 헤더 컨트롤을 숨길 수 있어요.

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

커스텀 헤더 (Custom header)

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

import dayjs from 'dayjs';
import { useState } from 'react';
import { DateStringValue, MonthView, 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, '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.Control onClick={() => window.print()}>
          Print
        </ScheduleHeader.Control>
      </ScheduleHeader>
      <MonthView date={date} onDateChange={setDate} events={events} withHeader={false} />
    </>
  );
}

하루 최대 이벤트 수 (Max events per day)

maxEventsPerDay prop으로 "+more" 표시기가 나타나기 전에 하루에 표시할 이벤트 수를 제어해요. 값은 1과 10 사이로 제한돼요. 날짜 셀 높이는 자동으로 조정돼요.

import { MonthView, ScheduleEventData } from '@mantine/schedule';

const events: ScheduleEventData[] = [
  // ... 5 events on the same day
];

function Demo() {
  return (
    <MonthView events={events} maxEventsPerDay={4} />
  );
}

많은 이벤트 (Many events)

하루에 이벤트가 많으면 컴포넌트가 "More events" 표시기를 보여줘요.

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

드래그 앤 드롭 (Drag and drop)

드래그 앤 드롭을 활성화해 이벤트를 날짜 사이로 이동할 수 있어요.

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

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

const initialEvents: ScheduleEventData[] = [
  {
    id: 1,
    title: 'Team Meeting',
    start: `${today} 09:00:00`,
    end: `${today} 10:30:00`,
    color: 'blue',
  },
  {
    id: 2,
    title: 'Project Deadline',
    start: dayjs().add(5, 'day').format('YYYY-MM-DD 00:00:00'),
    end: dayjs().add(6, '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 (
    <MonthView
      events={events}
      onEventDrop={handleEventDrop}
      withDragDrop
    />
  );
}

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

const sidebarItems = [
  { title: 'Team Offsite', color: 'teal' },
  { title: 'Release Day', color: 'orange' },
  { title: 'All Hands', 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.endOf('day');

    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}>
        <MonthView 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 { MonthView, ScheduleEventData } from '@mantine/schedule';

interface SidebarItem {
  title: string;
  color: string;
}

const initialSidebarItems: SidebarItem[] = [
  { title: 'Team Offsite', color: 'teal' },
  { title: 'Release Day', color: 'orange' },
  { title: 'All Hands', 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.endOf('day');

    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;
    }

    setSidebarItems((prev) => [
      ...prev,
      { title: event.title, 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}>
        <MonthView
          events={events}
          onExternalEventDrop={handleExternalDrop}
          onEventDrop={handleEventDrop}
          withEventsDragAndDrop
        />
      </Grid.Col>
    </Grid>
  );
}

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

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

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

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

  return (
    <MonthView
      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>
      )}
    />
  );
}

시간이 있는 이벤트 (Timed events)

renderEvent를 사용해 종일 이벤트와 시간이 있는 이벤트를 시각적으로 구분할 수 있어요. 종일 이벤트는 일반적인 색상 바처럼 렌더링되고, 시간이 있는 이벤트는 시작 시간과 제목과 함께 색상 점으로 표시돼요.

import dayjs from 'dayjs';
import { Box, UnstyledButton } from '@mantine/core';
import { MonthView, ScheduleEventData } from '@mantine/schedule';

function isAllDayEvent(event: ScheduleEventData) {
  const start = dayjs(event.start);
  const end = dayjs(event.end);
  return start.isSame(start.startOf('day')) && end.isSame(end.startOf('day'));
}

const events: ScheduleEventData[] = [/* ...events */];

function Demo() {
  return (
    <MonthView
      events={events}
      renderEvent={(event, props) => {
        if (isAllDayEvent(event)) {
          return <UnstyledButton {...props} />;
        }

        const { children, className, style, ...others } = props;
        return (
          <UnstyledButton {...others} style={{ ...style, display: 'flex', gap: 4, alignItems: 'center' }}>
            <Box style={{ width: 8, height: 8, borderRadius: 4, background: event.color }} />
            {dayjs(event.start).format('h:mm')}
            <span>{event.title}</span>
          </UnstyledButton>
        );
      }}
    />
  );
}

반복 이벤트 (Recurring events)

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

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

const month = '2025-11-01';

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

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

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

배경 이벤트 (Background events)

이벤트에 display="background"를 설정하면 일반 이벤트 뒤의 전체 너비, 반투명 블록으로 렌더링돼요. 기본적으로 클릭, 드래그, 리사이즈할 수 없어요. 배경 이벤트는 월 뷰에서 날짜 셀의 전체 높이를 차지해요.

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

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

const initialEvents: ScheduleEventData[] = [
  {
    id: 'company-offsite',
    title: 'Company offsite',
    start: `${dayjs(today).startOf('week').add(1, 'day').format('YYYY-MM-DD')} 00:00:00`,
    end: `${dayjs(today).startOf('week').add(3, 'day').format('YYYY-MM-DD')} 23:59:59`,
    color: 'blue',
    display: 'background',
  },
  {
    id: 'team-meeting',
    title: 'Team meeting',
    start: `${today} 10:00:00`,
    end: `${today} 11:00:00`,
    color: 'violet',
  },
];

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

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

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

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

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

const weekStart = dayjs().startOf('week');
const day = (offset: number) => weekStart.add(offset, 'day').format('YYYY-MM-DD');

const events: ScheduleEventData[] = [
  {
    id: 'out-of-office',
    title: 'Out of office',
    start: `${day(1)} 00:00:00`,
    end: `${day(3)} 23:59:59`,
    color: 'red',
    display: 'background',
  },
  {
    id: 'team-meeting',
    title: 'Team meeting',
    start: `${day(2)} 10:00:00`,
    end: `${day(2)} 11:00:00`,
    color: 'violet',
  },
];

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

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

어젠다 뷰 (Agenda view)

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

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

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

const events = [
  {
    id: 'team-meeting',
    title: 'Team Meeting',
    start: `${startOfMonth} 09:00:00`,
    end: `${startOfMonth} 10:30:00`,
    color: 'blue',
  },
  {
    id: 'weekly-review-series',
    title: 'Weekly review',
    start: `${startOfMonth} 16:00:00`,
    end: `${startOfMonth} 17:00:00`,
    color: 'orange',
    recurrence: {
      rrule: 'FREQ=WEEKLY;COUNT=8',
    },
  },
];

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

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

정적 모드 (Static mode)

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

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

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

지역화 (Localization)

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

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

function Demo() {
  return (
    <MonthView
      events={events}
      labels={{
        moreEvents: (hiddenEventsCount) => `+${hiddenEventsCount} más`,
      }}
    />
  );
}

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

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

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

function Demo() {
  const [events, setEvents] = useState(events);
  const [formOpened, setFormOpened] = useState(false);
  const [selectedEventData, setSelectedEventData] = useState(null);

  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) {
      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((e) => e.id !== selectedEventData.id));
    }
  };

  return (
    <>
      <MonthView
        events={events}
        onDayClick={handleDayClick}
        onEventClick={handleEventClick}
        onSlotDragEnd={handleSlotDragEnd}
        withDragSlotSelect
      />
      <EventForm
        setFormOpened={setFormOpened}
        onExitTransitionEnd={() => setSelectedEventData(null)}
        values={selectedEventData}
        onSubmit={handleSubmit}
        onDelete={selectedEventData?.id ? handleDeleteEvent : undefined}
      />
    </>
  );
}

반응형 스타일 (Responsive styles)

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

접근성 (Accessibility)

포커스 관리 (Focus management)

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

  • 월 뷰의 첫 번째 날만 탭 순서에 포함돼요 (tabIndex={0})
  • 다른 모든 날은 tabIndex={-1}이며 화살표 키 내비게이션으로만 도달할 수 있어요
  • withOutsideDays가 true일 때 외부 날짜(인접한 달의 날짜)는 화살표 키로 탐색할 수 있어요
  • 비활성화된 날은 키보드 내비게이션 중 건너뛰어요

이 방식은 달력을 탐색할 때 탭 스톱 수를 줄여 키보드 사용자가 더 빠르게 이동하면서도 화살표 키로 모든 날짜에 완전히 접근할 수 있게 해줘요.

키보드 상호작용 (Keyboard interactions)

다음 이벤트는 날짜 컨트롤에 포커스가 있을 때만 트리거된다는 점에 주의해요.

Key Description
ArrowRight 다음 비활성화되지 않은 날짜에 포커스
ArrowLeft 이전 비활성화되지 않은 날짜에 포커스
ArrowDown 다음 주의 같은 날짜에 포커스
ArrowUp 이전 주의 같은 날짜에 포커스

날짜 라벨 (Day labels)

각 날짜 버튼에는 "Month Day, Year"(예: "November 15, 2025") 형식의 전체 날짜를 담은 aria-label 속성이 있어요. 이는 화면 리더 사용자에게 완전한 날짜 정보를 제공해요.

MonthView는 Month의 접근성 동작을 따르고 있어요.

더 알아보기 (Learn more)