← Examples

Calendar 데모

DS Calendar는 월 이동·요일 헤더·날짜 그리드를 제공하는 공통 캘린더입니다. year, month, 월 이동 핸들러를 생략하면 로컬 타임존 기준 현재 달을 자체 관리합니다. 날짜 선택 상태는 getDayState 콜백으로 외부에서 주입합니다.

기본 calendar 컴포넌트

props 없이 `<Calendar />`만 렌더하면 로컬 타임존 기준 현재 달이 표시됩니다. 월 이동은 컴포넌트 내부에서 자체 관리되며, 기본적으로 이전·다음 달 이동에 제한이 없습니다.

<Calendar />
2026년 7월

import

named export로 가져옵니다.

import {
  Calendar,
  useCalendarView,
  type CalendarDayState,
} from "@/shared/components/ds/Calendar";

외부에서 월 이동 제어 (useCalendarView)

대부분의 경우 Calendar props만으로 충분합니다. 연·월 상태를 외부에서 읽거나 동기화해야 할 때만 `useCalendarView`를 사용합니다. (예: URL 쿼리와 연동, 열릴 때 특정 달로 강제 이동)

// 일반적인 사용 — Calendar가 월 이동을 자체 관리
<Calendar maxDate={new Date()} getDayState={getDayState} onDayClick={handleClick} />

// 고급 — 외부에서 연·월 상태를 직접 제어
const view = useCalendarView({ initialDate: new Date(2026, 2, 1) });

<Calendar
  year={view.viewYear}
  month={view.viewMonth}
  onPrevMonth={view.handlePrevMonth}
  onNextMonth={view.handleNextMonth}
/>
2026년 7월

단일 날짜 선택

`getDayState`로 `isRangeStart` / `isRangeEnd`를 같은 날짜에 설정하면 단일 선택 UI가 됩니다. 미래 날짜는 `isDisabled: true`로 비활성화하고, `maxDate` prop으로 미래 달 이동도 막을 수 있습니다.

const todayIso = getTodayIso();

function getDayState(isoDate: string): CalendarDayState {
  return {
    isDisabled: isoDate > todayIso,
    isToday: isoDate === todayIso,
    isRangeStart: isoDate === selectedDate,
    isRangeEnd: isoDate === selectedDate,
  };
}

<Calendar
  maxDate={new Date()}
  getDayState={getDayState}
  onDayClick={handleDayClick}
/>
2026년 7월

선택된 날짜: 없음

기간 선택 (range)

`isRangeStart`, `isRangeEnd`, `isInRange`를 설정하면 Calendar가 시작~종료 사이 하늘색 밴드를 자동으로 연결합니다. 주 단위 줄바꿈 시 반원 라운딩도 Calendar 내부에서 처리합니다.

const todayIso = getTodayIso();

function getDayState(isoDate: string): CalendarDayState {
  const isRangeComplete =
    rangeStart !== undefined &&
    rangeEnd !== undefined &&
    rangeStart !== rangeEnd;

  return {
    isDisabled: isoDate > todayIso,
    isToday: isoDate === todayIso,
    isRangeStart: isoDate === rangeStart,
    isRangeEnd: isoDate === rangeEnd,
    isInRange:
      isRangeComplete &&
      isoDate > rangeStart &&
      isoDate < rangeEnd,
  };
}

<Calendar
  maxDate={new Date()}
  getDayState={getDayState}
  onDayClick={handleRangeDayClick}
/>
2026년 7월

선택된 기간: 없음

기간 범위 Picker

`PeriodRangePicker`는 기간 설정 캘린더를 trigger/content 형태로 조합하는 bonbon 공통 UI입니다. 저장 API, 토스트, 검증 정책은 호출부에서 처리합니다.

import {
  PeriodRangePicker,
  type PeriodSettingCalendarValue,
} from "@/shared/components/bonbon";

const [open, setOpen] = useState(false);
const [value, setValue] = useState<PeriodSettingCalendarValue>({});
const [draftValue, setDraftValue] = useState<PeriodSettingCalendarValue>({});

<PeriodRangePicker.Root open={open} onOpenChange={setOpen}>
  <PeriodRangePicker.Trigger asChild>
    <button type="button">기간 설정</button>
  </PeriodRangePicker.Trigger>
  <PeriodRangePicker.Content
    value={draftValue}
    onValueChange={setDraftValue}
    onConfirm={(nextValue) => {
      setValue(nextValue);
      setOpen(false);
    }}
    title="기간 설정"
    confirmLabel="확인"
    closeAriaLabel="기간 설정 닫기"
  />
</PeriodRangePicker.Root>

설정된 기간: 없음

CalendarDayState 요약

각 날짜 셀의 표시 상태를 `getDayState(isoDate)` 반환값으로 제어합니다.

type CalendarDayState = {
  isDisabled?: boolean;       // 클릭 불가 (미래 날짜 등)
  isToday?: boolean;          // 오늘 날짜 강조
  isRangeStart?: boolean;     // 범위 시작일 (파란 원)
  isRangeEnd?: boolean;       // 범위 종료일 (파란 원)
  isInRange?: boolean;        // 시작~종료 사이 (하늘색 밴드는 Calendar가 자동 연결)
};

관련 유틸

날짜 계산·표시는 `@/shared/lib/calendarUtils`를 함께 사용합니다. `getTodayIso`와 `toIsoDate`는 동일한 `YYYY-MM-DD` 형식을 보장하므로 문자열 비교가 안전합니다.

import {
  getDaysInMonth,
  getFirstDayOfWeek,
  getWeekdayLabels,
  toIsoDate,
  getTodayIso,
  addDaysToIso,
  isoToLocalDate,
  isoToDotDisplay,
  formatCalendarMonthTitle,
  formatDayAriaLabel,
} from "@/shared/lib/calendarUtils";
아트봉봉스쿨

아트봉봉스쿨은더 큰 화면에서 쓸 수 있어요