Calendar
@mycrm-ui/components의 Calendar와 DatePicker입니다. 단일 날짜, 기간, 여러 날짜 선택과 marker, disabled date, pending 상태, DatePicker popover를 제공합니다.
Calendar는 달력 본문에 집중한 headless 컴포넌트입니다. 월 이동 버튼, API 호출, 폼 연결이 필요하면 DatePicker를 사용하거나 외부 상태로 Calendar를 조립합니다.
year와 month로 표시 월을 정하고,selectedDate와 onDateSelect로 단일 날짜 선택을 제어합니다.markedDates는 날짜 하단 dot과 선택 시 반환되는 marker 메타데이터를 제공합니다.
선택된 날짜
{
"selectedDate": "2026-09-10",
"selectedMarkers": []
}selectionMode="range"를 사용하면 첫 번째 클릭으로 시작일을 만들고, 두 번째 클릭으로 시작일/종료일을 정규화해 반환합니다.
범위 선택 상태
{
"draft": {
"startDate": "2026-09-10",
"endDate": null
},
"selectedRange": null
}selectionMode="multiple"은 날짜를 토글 방식으로 선택합니다.maxSelectedDates로 선택 가능한 최대 개수를 제한할 수 있습니다.
다중 선택 날짜
{
"selectedDates": [
"2026-09-10",
"2026-09-20"
]
}월 변경 시 API를 호출하는 구조에서는 외부에서 pending을 켜고, 응답으로 받은 날짜를 isDateDisabled 또는 markedDates에 반영합니다. pending 중에는 dimmed overlay와 원형 spinner가 표시됩니다.
API 연동 예시
{
"month": {
"year": 2026,
"month": 9
},
"pending": true,
"disabledDates": []
}DatePicker는 input-like trigger, Calendar popover, clear 버튼, hidden input, 연도/월 선택 popover를 포함합니다. range 또는 multi shorthand로 같은 컴포넌트에서 기간/다중 선택 모드를 사용할 수 있습니다.
Single
Range
Multiple
DatePicker 선택 상태
{
"single": "2026-09-10",
"range": {
"startDate": "2026-09-10",
"endDate": "2026-09-20"
},
"multiple": {
"selectedDates": [
"2026-09-10",
"2026-09-20"
]
}
}onMonthChange로 API 호출을 연결하고, pending,markedDates, isDateDisabled, getDayClassName을 조합해 업무 규칙을 주입합니다. theme은 내장 popover의 light/dark 테마를 바꿉니다.
Advanced
고급 옵션 선택 상태
{
"value": "2026-09-01",
"events": [],
"month": {
"year": 2026,
"month": 9
},
"pending": false,
"theme": "light"
}Calendar API
| Prop | Type | 설명 |
|---|---|---|
| year / month | number | 렌더링할 연도와 월입니다. 둘 다 필수이며 month는 1~12 값입니다. |
| selectionMode | 'single' | 'range' | 'multiple' | 선택 모드를 전환합니다. 기본값은 single입니다. |
| selectedDate | Date | string | null | single 모드의 controlled 선택값입니다. 생략하면 Calendar 내부 상태로 선택값을 관리합니다. |
| selectedDates | (Date | string)[] | multiple 모드의 controlled 선택 목록입니다. 생략하면 내부 상태로 선택 목록을 관리합니다. |
| rangeStart / rangeEnd | Date | string | null | range 모드의 controlled 시작일과 종료일입니다. 생략하면 내부 상태로 범위를 관리합니다. |
| onDateSelect | (date, markers) => void | single/multiple 모드에서 날짜 선택 시 호출됩니다. date는 dateSelectValueType 포맷이며 해당 날짜 marker 목록을 함께 받습니다. |
| onRangeDraftChange | (range) => void | range 모드에서 시작일 선택 또는 범위 확정 전/후 상태가 바뀔 때 호출됩니다. |
| onRangeSelect | (range) => void | range 모드에서 시작일과 종료일이 모두 확정되면 호출됩니다. 시작/종료 순서는 자동 정규화됩니다. |
| dateSelectValueType | 'date' | 'yyyyMMdd' | 'yyyy-MM-dd' | 'yyyy.MM.dd' | 선택 콜백으로 반환할 날짜 타입/문자열 포맷입니다. Calendar 기본값은 date입니다. |
| selectableStartDate / selectableEndDate | Date | string | null | 선택 가능한 날짜 범위를 제한합니다. 현재 월이 아닌 날짜는 선택 대상에서 제외됩니다. |
| isDateDisabled | (date: Date) => boolean | 특정 날짜를 비활성화합니다. 현재 range 모드에서는 이 함수보다 선택 가능 기간 제한을 중심으로 동작합니다. |
| markedDates | (Date | string | { date, color?, meta? })[] | 날짜 하단 dot marker를 렌더링합니다. 같은 날짜에 여러 marker를 넣을 수 있고 선택 콜백에서 markers로 반환됩니다. |
| maxSelectedDates | number | multiple 모드에서 선택 가능한 최대 개수입니다. 0 이하나 정수가 아니면 제한하지 않습니다. |
| pending | boolean | 달력 위에 dimmed overlay와 원형 spinner를 표시하고 날짜 상호작용을 막습니다. 기본값은 false입니다. |
| weekStartsOn | 0 | 1 | 0은 일요일 시작, 1은 월요일 시작입니다. 기본값은 0입니다. |
| weekdayLabelType | 'en' | 'ko' | 기본 요일 라벨 언어입니다. 기본값은 en입니다. |
| weekdayLabels / weekdayLabelsJson | readonly string[] / { sun, mon, ... } | 요일 라벨을 직접 주입합니다. weekdayLabels가 있으면 배열 순서가 우선 적용됩니다. |
| showAdjacentMonthDays | boolean | 이전/다음 달 날짜 표시 여부입니다. 기본값은 true이며 adjacent 날짜는 선택할 수 없습니다. |
| showToday | boolean | 오늘 날짜 강조 표시 여부입니다. 기본값은 true입니다. |
| showHover | boolean | hover 스타일과 range preview hover 추적 여부입니다. 기본값은 true입니다. |
| hoveredDate / onHoverDateChange | Date | string | null / (date: Date | null) => void | hover 날짜를 외부에서 제어하거나 추적합니다. |
| getDayClassName | (date, cell) => string | undefined | CalendarCell 상태를 보고 특정 날짜 cell에 className을 추가합니다. |
| className / classNames | string / CalendarClassNames | root className과 grid, weekday, day, selectedDay, rangeInsideDay 등 내부 슬롯 className입니다. |
DatePicker API
| Prop | Type | 설명 |
|---|---|---|
| selectionMode / range / multi | 'single' | 'range' | 'multiple' / boolean | 선택 모드입니다. range 또는 multi boolean shorthand를 사용할 수 있습니다. range가 multi보다 우선합니다. |
| value / defaultValue | Date | string | null | single 모드의 controlled 값과 uncontrolled 초기값입니다. |
| onChange | (date, markers) => void | single/multiple 모드에서 선택 날짜와 marker 목록을 받습니다. clear 시 date는 null입니다. |
| rangeStart / rangeEnd | Date | string | null | range 모드의 controlled 시작일/종료일입니다. |
| defaultRangeStart / defaultRangeEnd | Date | string | null | range 모드의 uncontrolled 초기 시작일/종료일입니다. |
| onRangeDraftChange / onRangeChange | (range) => void | 기간 선택 중간 상태와 확정 상태를 받습니다. clear 시 둘 다 null로 호출됩니다. |
| selectedDates / defaultSelectedDates | (Date | string)[] | multiple 모드의 controlled 선택 목록과 uncontrolled 초기값입니다. |
| maxSelectedDates | number | multiple 모드에서 선택 가능한 최대 개수입니다. |
| onMultipleChange | ({ selectedDates }) => void | multiple DatePicker의 전체 선택 목록을 받습니다. clear 시 빈 배열로 호출됩니다. |
| name | string | form submit용 hidden input을 렌더링합니다. range는 nameStart/nameEnd, multiple은 쉼표로 연결한 값을 생성합니다. |
| placeholder | string | 선택값이 없을 때 trigger에 표시할 문구입니다. 기본값은 Select date입니다. |
| clearable / clearLabel | boolean / string | 지우기 버튼 표시 여부와 라벨입니다. 기본값은 true / Clear입니다. |
| disabled / readOnly / required | boolean | trigger, hidden input, 상호작용 제어에 사용합니다. |
| closeOnSelect | boolean | 선택 후 popover를 닫을지 제어합니다. 기본값은 single/range true, multiple false입니다. |
| theme | 'light' | 'dark' | DatePicker 내장 trigger, popover, 연도/월 선택 popover의 기본 테마입니다. 기본값은 light입니다. |
| dateSelectValueType | 'date' | 'yyyyMMdd' | 'yyyy-MM-dd' | 'yyyy.MM.dd' | 선택 콜백과 내부 표시값에 사용할 날짜 포맷입니다. DatePicker 기본값은 yyyy-MM-dd입니다. |
| className / classNames | string / DatePickerClassNames | root className과 field, trigger, clearButton, popover, monthPickerOption 등 DatePicker 슬롯 className입니다. |
| calendarClassNames | CalendarClassNames | 내장 Calendar 슬롯 스타일입니다. |
| popoverLabel | string | Calendar popover dialog의 aria-label입니다. 기본값은 Choose date입니다. |
| previousMonthLabel / nextMonthLabel | string | popover 내부 이전/다음 달 버튼 문구입니다. |
| yearSelectLabel / monthSelectLabel | string | 연도/월 선택 popover의 섹션 라벨입니다. 없으면 weekdayLabelType에 따라 Year/Month 또는 연도/월을 사용합니다. |
| onMonthChange | ({ year, month }) => void | popover 내부 월 변경 또는 연도/월 직접 선택 시 호출됩니다. API로 비활성 날짜나 marker를 다시 가져올 때 사용합니다. |
| pending | boolean | 내장 Calendar에 pending overlay/spinner를 표시합니다. 기본값은 false입니다. |
| weekStartsOn | 0 | 1 | 내장 Calendar의 주 시작 요일입니다. 기본값은 0입니다. |
| weekdayLabelType | 'en' | 'ko' | 내장 Calendar의 기본 요일 라벨 언어입니다. 기본값은 en입니다. |
| weekdayLabels / weekdayLabelsJson | readonly string[] / { sun, mon, ... } | 내장 Calendar의 요일 라벨을 직접 주입합니다. |
| showAdjacentMonthDays / showToday / showHover | boolean | 내장 Calendar의 adjacent 날짜, 오늘 강조, hover/range preview 표시 여부입니다. |
| selectableStartDate / selectableEndDate | Date | string | null | 내장 Calendar의 선택 가능 날짜 범위를 제한합니다. |
| markedDates | (Date | string | { date, color?, meta? })[] | 내장 Calendar에 marker dot을 표시하고 선택 콜백에서 markers로 반환합니다. |
| isDateDisabled | (date: Date) => boolean | 내장 Calendar에서 특정 날짜를 비활성화합니다. range 모드에서는 선택 가능 기간 제한을 중심으로 동작합니다. |
| getDayClassName | (date, cell) => string | undefined | 내장 Calendar의 특정 날짜 cell에 className을 추가합니다. |
아래 항목은 기본 데모 화면만으로는 눈에 잘 드러나지 않지만, 현재mycrm-ui 소스에서 지원하는 입력 포맷, 접근성, 키보드, 테스트 계약, DatePicker 역할입니다.
날짜 입력 포맷
Calendar와 DatePicker는 Date 객체와 yyyyMMdd, yyyy-MM-dd, yyyy.MM.dd 문자열 입력을 파싱합니다. 선택 콜백 반환값은 dateSelectValueType으로 date, yyyyMMdd, yyyy-MM-dd, yyyy.MM.dd 중 선택할 수 있습니다.
<Calendar
selectedDate="20260910"
dateSelectValueType="yyyy.MM.dd"
onDateSelect={(date) => console.log(date)}
/>Controlled hover
hoveredDate와 onHoverDateChange를 함께 사용하면 hover 상태를 외부 상태로 제어할 수 있습니다. range 모드에서는 시작일 선택 후 hover 날짜를 기준으로 preview 범위가 표시됩니다.
<Calendar
year={2026}
month={9}
selectionMode="range"
hoveredDate={hoveredDate}
onHoverDateChange={setHoveredDate}
/>접근성 / 키보드
Calendar는 role="grid" 구조와 aria-selected, aria-disabled, aria-busy 속성을 사용합니다. 키보드는 방향키 이동, Home/End 행 이동, Enter/Space 선택을 지원합니다.
// 날짜 cell 키보드 동작
Arrow keys: focus 이동
Home / End: 행 시작/끝 이동
Enter / Space: 날짜 선택Focus 격리 / 테스트
각 Calendar 인스턴스는 자체 focus 상태를 갖습니다. 날짜 cell에는 data-mycrm-ui-calendar-date가 제공되어 테스트에서 특정 날짜를 안정적으로 찾을 수 있습니다.
screen.getByRole('grid', { name: '2026-09' })
container.querySelector('[data-mycrm-ui-calendar-date="2026-09-10"]')DatePicker의 현재 역할
DatePicker는 더 이상 Calendar를 그대로 반환하는 pass-through wrapper가 아닙니다. trigger, popover, clear 버튼, hidden input, 연도/월 선택 UI를 포함한 상위 컴포넌트입니다.
<DatePicker
name="eventDate"
clearable
previousMonthLabel="이전 달"
nextMonthLabel="다음 달"
/>Pending UI
현재 pending UI는 skeleton이 아니라 dimmed overlay와 원형 spinner입니다. pending=true 동안 날짜 선택과 focus 대상 상호작용이 차단됩니다.
<Calendar
year={2026}
month={9}
pending={isLoading}
/>컴포넌트는 기본 inline style을 제공하지만, classNames와calendarClassNames로 프로젝트 CSS를 우선 적용할 수 있습니다. 외부 className이 있으면 주요 기본 스타일은 생략되도록 설계되어 있습니다.
Calendar
className은 root에, classNames는 달력 내부 슬롯에 적용합니다.
DatePicker
classNames는 trigger, clear 버튼, popover, 연도/월 선택 UI를 제어합니다.
내장 Calendar
DatePicker 안의 달력은 calendarClassNames로 별도 스타일링합니다.
<Calendar
className="w-80 rounded-xl border p-4"
classNames={{
weekdaySat: 'text-blue-600',
weekdaySun: 'text-red-600',
selectedDay: 'bg-blue-600 text-white',
rangeInsideDay: 'bg-blue-100 text-blue-900',
adjacentMonthDay: 'text-slate-400 opacity-60',
disabledDay: 'text-slate-400 opacity-45',
dayMarker: 'ring-1 ring-white',
}}
/>
<DatePicker
classNames={{
root: 'w-64',
trigger: 'w-full rounded-lg border px-3',
triggerValue: 'truncate whitespace-nowrap',
clearButton: 'rounded-lg border px-2.5',
popover: 'rounded-xl bg-white shadow-xl',
monthPickerOptionActive: 'bg-blue-600 text-white',
}}
calendarClassNames={{
selectedDay: 'bg-blue-600 text-white',
rangeInsideDay: 'bg-blue-100',
disabledDay: 'opacity-40 text-slate-400',
}}
/>적용 기준
className은 최외곽 root에 추가되는 단일 class입니다.classNames는 컴포넌트가 노출하는 슬롯별 class입니다. 슬롯을 제공하면 해당 슬롯의 주요 기본 inline style이 생략되는 경우가 있어 프로젝트 CSS가 우선됩니다.DatePicker.classNames와DatePicker.calendarClassNames는 적용 대상이 다릅니다. trigger/popup shell은classNames, 달력 날짜 셀은calendarClassNames를 사용합니다.
CalendarClassNames 슬롯
| 슬롯 | 적용 요소 | 관련 기능 | CSS 예시 | Tailwind 예시 |
|---|---|---|---|---|
| 기본 레이아웃 | ||||
| root | Calendar 최외곽 div | 달력 전체 크기, 테두리, 배경 | width: 320px; padding: 16px; border: 1px solid #e5e7eb | w-80 rounded-xl border p-4 |
| grid | 요일/날짜 grid | grid 간격, 폰트 크기 | gap: 8px; font-size: 14px | gap-2 text-sm |
| weekday | 요일 헤더 공통 | 요일 라벨 높이, 정렬, 글꼴 | height: 30px; font-weight: 600 | h-8 font-semibold |
| weekdaySun / weekdaySat | 일/토 요일 헤더 | 주말 색상 | color: #dc2626 / #2563eb | text-red-600 / text-blue-600 |
| weekdayMon~weekdayFri | 월~금 요일 헤더 | 특정 요일 색상 | color: #374151 | text-slate-700 |
| 날짜 셀 | ||||
| day | 날짜 cell wrapper | 셀 높이, 정렬, 상태별 wrapper 스타일 | min-height: 40px; display: grid; place-items: center | min-h-10 place-items-center |
| dayContent | 날짜 숫자 주변 content | 선택/범위 배경이 적용되는 영역 | width: 36px; height: 36px; border-radius: 9999px | h-9 w-9 rounded-full |
| dayHover | hover 가능한 날짜 내부 | hover 시 배경/색상 | background: #eff6ff; color: #2563eb | bg-blue-50 text-blue-600 |
| daySun / daySat | 일/토 날짜 | 주말 날짜 색상 | color: #dc2626 / #2563eb | text-red-600 / text-blue-600 |
| dayMon~dayFri | 월~금 날짜 | 특정 요일 날짜 색상 | color: #111827 | text-slate-900 |
| currentMonthDay | 현재 월 날짜 cell | 현재 월 날짜 강조 | color: #111827 | text-slate-900 |
| adjacentMonthDay | 이전/다음 월 날짜 cell | 인접 월 날짜 흐림 처리 | color: #9ca3af; opacity: .6 | text-slate-400 opacity-60 |
| disabledDay | 비활성 날짜 내부 | 비활성 날짜 흐림 처리 | color: #9ca3af; opacity: .45 | text-slate-400 opacity-45 |
| todayDay | 오늘 날짜 내부 | 오늘 강조 | font-weight: 800; text-decoration: underline | font-extrabold underline underline-offset-4 |
| 선택 / 범위 | ||||
| selectedDay | single/multiple 선택 날짜 content | 선택 날짜 배경 | background: #2563eb; color: #fff | bg-blue-600 text-white |
| rangeStartDay | range 시작 날짜 content | 기간 시작일 | background: #2563eb; color: #fff | bg-blue-600 text-white |
| rangeEndDay | range 종료 날짜 content | 기간 종료일 | background: #2563eb; color: #fff | bg-blue-600 text-white |
| rangeInsideDay | range 내부 날짜 content | 기간 내부 배경 | background: #dbeafe; color: #1e3a8a | bg-blue-100 text-blue-900 |
| rangePreviewDay | range hover preview content | 종료일 선택 전 미리보기 | background: #eff6ff; color: #2563eb | bg-blue-50 text-blue-600 |
| rangeSingleDay | 시작/종료가 같은 range content | 하루짜리 기간 | background: #2563eb; color: #fff | bg-blue-600 text-white |
| 마커 / 상태 | ||||
| dayMarker | 날짜 하단 dot | markedDates dot 스타일 | width: 6px; height: 6px; border-radius: 9999px | h-1.5 w-1.5 rounded-full |
DatePickerClassNames 슬롯
| 슬롯 | 적용 요소 | 관련 기능 | CSS 예시 | Tailwind 예시 |
|---|---|---|---|---|
| 필드 / 트리거 | ||||
| root | DatePicker 최외곽 div | 전체 폭, 배치, form 주변 간격 | width: 280px; position: relative | w-70 relative |
| field | trigger와 clear 버튼 wrapper | 버튼 간격, 정렬 | display: inline-flex; gap: 8px | inline-flex gap-2 |
| trigger | input-like trigger button | 입력 박스 형태, 테두리, 배경 | height: 40px; border: 1px solid #d1d5db | h-10 rounded-lg border px-3 |
| triggerValue | 선택값 텍스트 | 긴 날짜/기간 말줄임 | overflow: hidden; text-overflow: ellipsis; white-space: nowrap | truncate whitespace-nowrap |
| placeholder | placeholder 텍스트 | 미선택 상태 색상 | color: #6b7280 | text-slate-500 |
| icon | 오른쪽 calendar icon | 아이콘 크기/색상 | color: #6b7280; flex-shrink: 0 | shrink-0 text-slate-500 |
| clearButton | 지우기 버튼 | clearable 버튼 | border: 1px solid #d1d5db; padding: 0 10px | rounded-lg border px-2.5 |
| Calendar Popover | ||||
| popover | Calendar popover dialog | 달력 팝오버 배경, z-index, shadow | z-index: 50; border-radius: 12px; box-shadow: 0 18px 45px rgba(0,0,0,.18) | z-50 rounded-xl shadow-xl |
| popoverHeader | popover 상단 헤더 | 이전/다음 버튼과 월 선택 정렬 | display: flex; justify-content: space-between | flex items-center justify-between |
| popoverTitle | 현재 연도/월 텍스트 | 월 선택 버튼 텍스트 | font-weight: 700 | font-bold |
| monthButton | 이전/다음 달 버튼 | 월 이동 버튼 | height: 32px; border: 1px solid #d1d5db | h-8 rounded-lg border px-2.5 |
| 연도/월 선택 Popover | ||||
| monthPickerWrap | 연도/월 선택 wrapper | 내부 popover 기준점 | position: relative | relative |
| monthPickerToggle | 연도/월 선택 버튼 | 연도/월 리스트 열기 버튼 | min-width: 96px; border-radius: 8px | min-w-24 rounded-lg |
| monthPickerPopover | 연도/월 리스트 popover | 내부 popover 패널 | width: 280px; max-width: calc(100vw - 64px) | w-70 max-w-[calc(100vw-64px)] |
| monthPickerSection | 연도 또는 월 section | 옵션 grid | display: grid; grid-template-columns: repeat(4, 1fr) | grid grid-cols-4 gap-1.5 |
| monthPickerSectionTitle | section 제목 | 연도/월 라벨 | font-size: 12px; font-weight: 700 | text-xs font-bold |
| monthPickerOption | 연도/월 option button | 각 연도/월 버튼 | height: 32px; border-radius: 8px | h-8 rounded-lg |
| monthPickerOptionActive | 선택된 연도/월 option | active option | background: #4f46e5; color: #fff | bg-indigo-600 text-white |