DateRangePicker
업무 기간 조회용 범위 선택기 — 임의 선택(프리셋)/달력 선택(듀얼 먼스) 2탭 팝업. 비제어 defaultValue + onChange(즉시)/onConfirm(선택완료 확정). 외부 클릭·Escape는 이전 확정값을 복원합니다 (DK WizDatePickerEx 사양 승격).
마지막 업데이트 2026-07-02
한눈에#
업무 조회용 기간(시작~끝) 선택기입니다 — 임의 선택(프리셋)/달력 선택(듀얼 먼스) 2탭 팝업으로, 연·월·분기·반기 프리셋과 듀얼 먼스 달력을 함께 제공합니다.
아직 확정 없음
- 임의 선택·달력 선택 2탭
- 프리셋 + 듀얼 먼스
- onChange 즉시 / onConfirm 확정
- 비제어 + BottomSheet 모바일
임시 변경은 onChange로 즉시, 선택완료로 확정될 때 onConfirm — 외부 클릭·Escape는 직전 확정값 복원
트리거는 Input 어휘이고, 팝업 상단의 “임의 선택”/“달력 선택” 2탭으로 입력
방식을 전환합니다. 모든 임시 변경은 onChange로 즉시, “선택완료”로 확정될
때 onConfirm이 호출되며 트리거 표시값도 그 시점에 갱신됩니다.
defaultValue가 없으면 시작일과 종료일은 모두 오늘로 초기화됩니다.
사용 시점#
매출·로그 조회처럼 업무 단위 기간을 프리셋+달력으로 고르면 DateRangePicker — 단일 날짜면 DatePicker입니다.
권장 — 이렇게 쓰세요
지양 — 이러지 마세요
쓴다 — 매출·로그 조회처럼 연·월·분기·반기 프리셋과 듀얼 먼스로 시작~끝 기간을
대신 DatePicker — 하나의 날짜만(범위가 가끔 필요하면 mode=range로도 충분)
플레이그라운드#
컨트롤로 props를 조작하면 미리보기와 코드가 실시간 갱신됩니다.
DateRangePicker를 직접 조작해 보세요
속성·테마·토큰을 바꾸고 React·Flutter 코드를 확인하는 풀스크린 빌더로 엽니다.
플레이그라운드는 넓은 작업 영역이 필요해 웹·태블릿에서 편집할 수 있어요.
변형#
입력 방식 2탭이 변형 축입니다.
임의 선택 (프리셋) — 가까운 날짜별(오늘·이번주·이번달) · 연도별(올해·작년·재작년) · 월별(6열 그리드) ·
분기별 · 반기별 버튼 그리드입니다. 프리셋은 항상 단일 항목 선택입니다. 3월을
누른 뒤 6월을 누르면 3~6월 결합이 아니라 6월 하나로 대체되고, aria-pressed
상태도 마지막 항목 하나만 유지됩니다. 연도 버튼을 누르면 월/분기/반기 프리셋이
그 연도 기준으로 바뀝니다. 이번주는 weekStartsOn 기준을 따르며 기본값은
월요일 시작(월~일), weekStartsOn="sunday"이면 일요일 시작(일~토)입니다.
임의 선택 탭에도 달력 선택과 같은 시작일·종료일 요약
필드가 표시됩니다. showQuickSelect/showQuarter/showHalf로 각 절을 끄고,
years로 연도 버튼 목록(기본 최근 3년)을 바꿉니다.
달력 선택 (듀얼 먼스) — 시작일·종료일 두 개의 Calendar를 나란히 띄웁니다.
두 달력의 위 또는 아래에는 공유 요약 영역이 표시되어 시작일/종료일 제목과
현재 선택값(미선택 시 “선택 안 됨”)을 한눈에 보여주며, 필드를 직접 클릭해
20260625처럼 연속 숫자를 입력하면 YYYY-MM-DD로 자동 정규화됩니다
(startLabel/endLabel, rangeSummaryPlacement로 변경 가능). 두 달력은 선택된
시작일~종료일 사이를 primary-subtle 밴드로 함께 칠해 범위를 시각적으로 보여줍니다.
시작일만 선택된 상태에서는 종료 후보에 hover 또는 keyboard focus가 닿는 즉시 같은
밴드와 endpoint marker로 확정 전 범위를 preview합니다. 이 preview는 시각 피드백일
뿐이라 후보 날짜의 aria-selected나 onChange 값은 종료일을 실제 선택할 때까지
바꾸지 않습니다.
요일 헤더와 일반 날짜는 토요일을 primary 계열, 일요일을 error 계열 텍스트로 구분하되
선택·범위·비활성·외부월 상태 색상을 우선합니다.
듀얼 먼스에서는 이전/다음 달 날짜를 보여주지 않아 각 달이 1일부터 시작하고,
시작일 달력은 시작 marker와 이후 기간 색상, 종료일 달력은 기간 색상과 종료
marker를 나누어 표시합니다. 달력 헤더의 월/연도 라벨을 누르면 월 선택 패널이
열리고, 다시 연도를 눌러 연도 선택 패널로 이동할 수 있어 좌우 버튼만으로 넘기기
어려운 긴 기간도 빠르게 이동합니다. 두 달력은 이미 접근 이름을 갖고 있어 스크린리더에는
동일한 구분이 전달되며, 요약 영역은 그 구분을 눈으로도 보이게 합니다.
시작일을 종료일 이후로 바꾸면 종료일이 비워지고 종료 달력이 재마운트됩니다.
이때 종료 달력은 오늘 월로 튀지 않고 새 시작일의 월을 표시해 이어지는 날짜를
바로 고를 수 있게 하며, 종료 달력의 최소 선택일은 선택된 시작일로 제한됩니다.
달력으로 고르면 프리셋 선택 표시(aria-pressed)는 해제됩니다.
PDA/업무 옵션 — 업무 화면별 버튼 배치와 라벨만 바꾸고, 시작일/종료일
두 Calendar 구조는 그대로 유지합니다. 달력 요약은 rangeSummaryPlacement="bottom"으로
하단에 둘 수 있고, footerMode="confirmOnly"와 requireCompleteRange를 함께 쓰면
PDA/Figma 패턴처럼 시작일·종료일이 모두 선택된 뒤 하나의 확인 버튼으로 확정됩니다.
presetSectionLabels는 프로젝트별 프리셋 절 라벨을 주입할 때 사용합니다.
<DateRangePicker
rangeSummaryPlacement="bottom"
weekStartsOn="monday"
footerMode="confirmOnly"
requireCompleteRange
presetSectionLabels={{
quickSelect: '가까운 날짜별',
years: '연도별',
months: '월별',
quarters: '분기별',
halves: '반기별',
}}
onConfirm={(value) => applyPeriod(value)}
/>
푸터의 초기화는 임시 선택을 오늘~오늘 범위로 되돌리고(프리셋 표시·달력 모두 리셋), 선택완료는 현재 임시 선택을 확정합니다.
크기#
단일 크기입니다 — 트리거 높이는 input.height(44px), 최소 폭 16rem으로 Input과
같은 폼 컨트롤 줄맞춤입니다. 팝업은 내용 폭(max-content)이되 뷰포트를 넘지
않도록 max-width: calc(100vw - space.8)로 제한되고, 좁으면 듀얼 먼스가 세로로
스택됩니다(bp.mobile 767px 기준). 모바일 BottomSheet는 복합 달력 조작을 위해
full detent를 사용하고, 7열 달력이 좁은 화면에서도 눌리지 않도록 시트 body gutter를
DateRangePicker 내부에서 회수합니다.
상태#
비제어 컴포넌트입니다 — defaultValue가 있으면 그 값으로, 없으면 오늘~오늘로
시작하고, 임시 변경(draft)은 onChange로, 확정은 onConfirm으로 통지합니다.
열림/닫힘도 내부 소유:
트리거 클릭으로 열고, 선택완료·Escape·외부 클릭으로 닫힙니다. 단, Escape와
외부 클릭은 취소라 직전 확정값을 복원합니다(선택완료만 확정). 트리거의
Hover/Focus/Disabled는 CSS 의사클래스로 표현되고, 열림 동안
aria-expanded="true"가 유지됩니다.
모바일#
DK 모바일 재활용 표준입니다 — 기본 responsive="auto"는 compact 뷰포트에서
BottomSheet로 전환합니다. 강제로 확인하려면
responsive="mobile"을 사용하고, mobile prop은 하위호환 alias로만 둡니다.
모바일 시트는 full detent로 열리며 2탭 + 패널을 같은 표면 안에 렌더합니다. 기본
footer는 업무 조회 패턴에 맞춰 초기화/선택완료이고, 취소 액션이 필요한 화면은
footerMode="cancelConfirm"으로 전환합니다.
시트의 닫힘 경로(배경 클릭·Escape·핸들 드래그 다운)는 취소로 동작해 직전 확정값을
복원합니다. 시트 제목은 mobileTitle(기본 placeholder)입니다. 달력 탭의 Calendar는
시트 안에서 별도 카드 테두리 없이 본문 폭을 채우고, DateRangePicker 내부 wrapper가
좌우 gutter를 회수해 320px급 화면에서도 7열 날짜 그리드, 월/연도 선택 버튼, 요약 입력이
44px 전후 터치 타깃을 유지합니다. footer는 BottomSheet slot에 넣어 sticky 위치와
safe-area padding을 BottomSheet가 소유합니다. 라이브 데모는 데스크톱 뷰포트라 코드로
안내합니다.
<DateRangePicker
responsive="mobile"
mobileTitle="조회 기간"
years={[2024, 2025, 2026]}
onConfirm={(value) => applyPeriod(value)}
/>
Props#
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
defaultValue | { readonly start: Date; readonly end: Date; } | — | 초기 범위 (비제어) — 이후 상태는 내부 소유 |
onChange | (value: DateRangeValue) => void | — | 모든 임시 변경(프리셋 클릭·달력 클릭·초기화)에 즉시 호출 |
onConfirm | (value: DateRangeValue) => void | — | "선택완료"로 확정될 때 호출 — 트리거 표시값도 이 시점에 갱신 |
years | readonly number[] | — | 프리셋 연도 버튼 — 기본 최근 3년 |
showQuickSelect | boolean | true | 빠른선택(오늘·이번주·이번달) 절 노출 |
showQuarter | boolean | true | 분기 절 노출 |
showHalf | boolean | true | 반기 절 노출 |
min | Date | — | 달력 최소 선택일 — Calendar로 전달 |
max | Date | — | 달력 최대 선택일 — Calendar로 전달 |
disabled | (date: Date) => boolean | — | 비활성 날짜 판별 — Calendar로 전달 |
placeholder | string | 기간 선택 | 값이 없을 때 트리거 텍스트 + 팝업 dialog의 aria-label |
confirmText | string | — | 확정 버튼 라벨 |
resetText | string | — | 초기화 버튼 라벨 |
presetTabText | string | 임의 선택 | 프리셋 탭 라벨 |
calendarTabText | string | 달력 선택 | 달력 탭 라벨 |
startLabel | string | 시작일 | 듀얼 먼스 달력 상단 제목 — 시각 헤더이자 각 달력의 접근 이름(aria-label) |
endLabel | string | 종료일 | 오른쪽(종료) 달력 상단 제목 — 시각 헤더이자 그 달력의 접근 이름 |
showCalendarHeader | boolean | true | 달력 상단 시작일/종료일 제목 헤더 표시 (false면 달력만) |
weekStartsOn | 'monday' | 'sunday' | monday | 주 시작 요일 — 기본 monday. sunday면 프리셋 이번주/달력/키보드가 일~토 기준 |
rangeSummaryPlacement | 'top' | 'bottom' | 'none' | — | 달력 선택 요약 표시 위치 |
emptyDateLabel | string | — | 선택 전 날짜 요약 텍스트 |
presetSectionLabels | Partial<Record<DateRangePresetSection, string>> | — | 프리셋 섹션 라벨 override |
footerMode | 'none' | 'resetConfirm' | 'cancelConfirm' | 'confirmOnly' | — | 하단 액션 구성 |
cancelLabel | string | — | 취소 버튼 라벨 |
confirmLabel | string | — | 확정 버튼 라벨 |
resetLabel | string | — | 초기화 버튼 라벨 |
requireCompleteRange | boolean | — | 시작일/종료일이 모두 있어야 확정 가능 |
formatDate | DateRangeDateFormatter | — | 단일 날짜 표시 포맷터 |
formatRange | DateRangeFormatter | — | 범위 표시 포맷터 |
responsive | 'auto' | 'mobile' | 'desktop' | — | 적응형 표시 (ADR-012) — auto(기본): compact 뷰포트에서 borderless/full-width Calendar BottomSheet 전환. mobile/desktop은 강제 override |
mobile | boolean | false | @deprecated responsive='mobile' 사용. true면 BottomSheet 강제 — 하위호환 alias |
mobileTitle | string | — | 시트 제목 — 기본 placeholder |
ref | Ref<HTMLButtonElement> | — | 트리거 <button>으로 (병합) 전달되는 ref (React 19 ref-as-prop) |
DateRangeValue(onChange/onConfirm 인자):
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
start | Date | null | — | 시작일 (없으면 null) |
end | Date | null | — | 종료일 (없으면 null) |
presetType | DateRangePresetType | — | 프리셋으로 만든 범위면 유형 — 'today'|'thisWeek'|'thisMonth'|'month'|'quarter'|'half'|'year' |
presetLabel | string | — | 프리셋 라벨 — 트리거 표시는 이 값을 우선 (예: "2026년 3월~6월") |
접근성#
트리거가 aria-haspopup="dialog" + aria-expanded, 팝업이
role="dialog"(이름은 placeholder)입니다. 2탭은 aria-pressed 토글
버튼이고, 프리셋 절은 각각 role="group" + aria-label로 묶입니다.
| 키 | 동작 |
|---|---|
트리거 Enter / Space | 팝업 열기 (네이티브 button) — 다시 누르면 취소 닫기 |
Escape (팝업 내부 어디서든) | 취소 — 직전 확정값 복원 + 트리거 포커스 복원 |
Tab | 탭·프리셋 버튼·달력·푸터 버튼 순회 |
| 달력 키보드 | 화살표(일)·Home/End(주 시작·끝)·PageUp/Down(월) — Calendar APG grid |
달력 월/연도 라벨 Enter / Space | 월 선택 패널 열기, 연도 라벨에서 연도 선택 패널 열기 |
- 외부 클릭도 취소(직전 확정값 복원, 포커스는 클릭 대상에 양보)
- 모바일 시트의 배경 클릭·Escape·핸들 드래그 다운도 취소로 처리하며, 확정값과 트리거 포커스를 복원
- 프리셋 선택 표시는
aria-pressed, 마지막 선택 항목 하나만 눌림 표시 disabledprop은 트리거 비활성화가 아니라 날짜별 비활성 판별 함수입니다. 비활성 날짜는 선택·포커스·직접 입력에서 제외됩니다.- 달력 월 라벨은 버튼으로 노출되며
aria-live="polite"로 갱신 안내 (Calendar 합성) - 팝업 등장 모션은 opacity/transform만 +
prefers-reduced-motion존중
토큰#
component 토큰 없이 semantic(+input 어휘)을 직접 소비합니다(신설 기준 §4 미충족).
| 속성 | 토큰 |
|---|---|
| 트리거 | input.height/bg/border/radius/fg · placeholder input.placeholder |
| 트리거 포커스/열림 | color.focus-ring(아웃라인) · input.border-focus |
| 팝업 | color.surface-raised · color.border · radius.lg · shadow.lg · z.dropdown |
| 2탭 | color.surface-muted(트랙) · 활성 color.surface-raised + color.primary-text + shadow.sm |
| 프리셋 버튼 | color.surface · color.border · 높이 space.8 · hover color.surface-hover → 눌림 color.surface-pressed — 선택 color.primary + color.on-primary |
| 듀얼 먼스 | Calendar 토큰(color.primary·color.on-primary·color.primary-subtle) · 주말 primary-text/error-text · start-only hover/focus preview |
| 모바일 시트 | BottomSheet full detent · wrapper gutter reclaim · Calendar 카드 크롬 제거 · 44px 요약 입력/월·연도 선택 |
| 푸터 | 초기화 color.surface+border-strong+primary-text(눌림 color.surface-pressed) · 선택완료 button.bg/fg · BottomSheet safe-area |
| 모션 | duration.fast + 팝업 등장 ease.emphasized-decelerate / ease.standard |