Files
HMDesign/docs/components/calendar.md

121 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Calendar
현재 HM 구현의 기준 문서. 공통 원칙은 `docs/guidelines/프로젝트-지침.md`(§6, §7), Token 구조 원칙은 `테마-토큰-가이드.md`를 따른다. 이 문서는 Calendar 고유의 API·Token·상태·결정만 기록한다.
## 1. 상태와 성격
| 항목 | 내용 |
|---|---|
| 성격 | **Primitive component** (날짜 선택 UI). Date Picker 자체가 아니다 |
| 구현 상태 | 구현 완료 (현재 HM 규격) |
| 기준일 | 2026-10-02 |
| 기반 | shadcn/ui Calendar = **react-day-picker v10** |
| 대표 Story | `Components/Search & Date/Calendar` → `All` (이름 "Calendar") |
| 보류 | 아래 §11 |
## 2. 적용 근거
- **참고 근거**: 외부 ui_viz Reference (ZIP 미포함)의 기존 테스트 규격에는 Calendar가 없다(인벤토리에 Date Picker 이름만). 참고할 수치가 없었다.
- **현재 HM 규격**: 아래 §3~§4. 현재 HM Semantic/Primitive로 새로 정했다. Button/Input/Popover Token은 참조하지 않는다(Navigation 버튼이 Button처럼 보여도 Calendar Token으로 관리).
- **구현 방식**: react-day-picker가 날짜 계산, 월 이동, 키보드, 선택 모드, ARIA grid를 소유한다. HM은 시각만 입힌다. 라이브러리의 기본 class(`rdp-*`)와 날짜 셀의 `data-*` 상태 속성을 그대로 두고 `[data-slot="calendar"]` 아래에서 CSS로 스타일한다.
- **보류**: §11.
## 3. 기본 규격 (현재 HM 규격)
| 항목 | 값 |
|---|---|
| Surface | 없음(투명). 감싸는 컨테이너(Popover, Card 등)가 면과 윤곽을 가진다 |
| Padding / 폭 | 12px / 내용 폭(`fit-content`, 한 달 276px) |
| Caption | 가운데, 높이 32px, 14 / 20 / 700 |
| Navigation 버튼 | 28 × 28px, radius 4px, 아이콘 16px(lucide chevron), Caption 줄 양끝에 절대 배치 |
| Weekday | 12 / 18 / 500, `--hm-sem-fg-secondary`, 폭 36px |
| Day | 36 × 36px 셀과 버튼, radius 4px, 14 / 20 |
| 여러 달 | `numberOfMonths`에 따라 flex로 나란히, 좁으면 줄바꿈(gap 16px) |
| Locale | 기본 **한국어**(`react-day-picker/locale`의 `ko`, 추가 dependency 없음). `locale` prop으로 교체 |
## 4. 상태 규격
| 상태 | 값 |
|---|---|
| Default | 투명 |
| Hover | 배경 `--accent` 45% |
| Focus (`:focus-visible`) | ring 모양 `0 0 0 3px ring 50%` |
| Today (`data-today`) | 배경 `--hm-sem-bg-subtle`, weight 700 |
| Selected (`data-selected`) | 배경 `--hm-sem-state-active`(Light 2 `#266651`), 글자 `--hm-sem-on-state-active`(흰색) (hover해도 유지, focus 시 fill + Focus ring) |
| Outside (`data-outside`) | 글자 `--hm-sem-fg-secondary`, opacity 0.6 |
| Disabled (`data-disabled`) | opacity 0.4(이 컴포넌트 값, 전역 Disabled 규칙으로 승격 안 함), cursor not-allowed, 선택 불가 |
| Range start / middle / end (`rdp-range_*`) | start·end는 Active 면(`--hm-calendar-range-edge-*` → `--hm-sem-state-active`, hover해도 유지, 안쪽 모서리 직각), middle은 `--accent` 45% 면 + 직각 |
| Nav 버튼 disabled | opacity 0.4 (`startMonth`/`endMonth` 경계) |
기반 라이브러리가 실제로 제공하는 상태만 쓴다. HM 전용 상태 속성은 없다.
## 5. API
react-day-picker `DayPicker` props 그대로. export: `Calendar`.
- `mode` `"single" | "multiple" | "range"`, `selected`, `onSelect`, `required`, `defaultMonth`, `month`/`onMonthChange`, `disabled`(Matcher), `startMonth`/`endMonth`, `numberOfMonths`, `captionLayout`, `weekStartsOn`, `today`, `locale` 등.
- Calendar가 바꾸는 기본값: `showOutsideDays=true`, `locale=ko`, Chevron 아이콘을 lucide로 교체(`components.Chevron`).
- 출력 `data-slot="calendar"`. 날짜 셀은 `td.rdp-day`(`data-selected`, `data-today`, `data-outside`, `data-disabled`, `data-focused`, `aria-selected`).
- `variant`, `size`는 만들지 않았다.
## 6. Dependency
- `react-day-picker` ^10.0.2 (`packages/ui`에 추가, shadcn Calendar의 기반). 날짜 계산용으로 `date-fns`와 `@date-fns/tz`가 이 패키지의 dependency로 따라 들어온다. HM이 직접 date 라이브러리를 추가하지 않았다.
- locale은 같은 패키지의 `react-day-picker/locale`(`ko`)을 사용(추가 설치 없음).
- CENTER 패키지에는 추가하지 않았다.
## 7. Token
Component Mapping 위치: `packages/ui/src/theme/calendar.css` (`--hm-calendar-*`).
`padding`, `text`, `month-gap`, `caption-*`, `nav-size|radius|icon-size|hover-background`, `weekday-*`, `day-size|radius|font-size|line-height|hover-background`, `today-background|font-weight`, `selected-background|text`, `range-edge-background|text`, `range-middle-background|text`, `outside-color|opacity`, `disabled-opacity`. Semantic/Theme palette를 참조한다. Selected·Range edge는 `--hm-sem-state-active` / `--hm-sem-on-state-active`(2026-10-06 Active Semantic)를 참조한다. 신규 Primitive 없음.
## 8. react-day-picker에서 유지한 항목
날짜 계산, 월 이동, 키보드 모델(방향키, Home/End, PageUp/PageDown), 선택 모드와 `required`, disabled/outside/hidden 날짜, 숫자·날짜 접근성 label(locale 기반), `role="grid"`/`gridcell`, `aria-selected`, roving tabindex.
## 9. Storybook
- 대표 Story `All`("Calendar"): Single(오늘 10/2, 선택 10/15), Disabled dates(5일 이전·일요일), Range(10/8–10/14), Two months를 한 페이지에 둔다. `today`는 라이브러리 prop으로 2026-10-02에 고정(어느 날 열어도 같게 보이기 위함).
- 2026-10-06 Visual QA: Two months 구역만 `StateBoard`의 `wide`를 사용한다. Desktop에서 252px month 두 개 + 16px gap + Calendar padding 24px + frame border 2px가 들어가며 좁은 폭에서는 기본 wrap을 유지한다. Production Calendar API/CSS는 변경하지 않았다.
- verification: `Single`, `Disabled Dates`, `Range`.
- meta에 `component`를 지정하지 않는다(props가 선택 모드 union이라 args 없는 Story가 타입을 통과하지 못함).
## 10. 사용자 확정 결정
- 2026-10-02: ui-viz는 참고 자료이며 최종 기준은 현재 HM 구현과 이 문서다(작업 지시). 개별 수치에 대한 별도 결정은 없다.
## 11. 미확정 / 보류
- 수치(36px 셀, 12px padding, Today 면, Selected 면)는 현재 HM 규격이며 디자인 확정 시 재검토.
- 기본 locale을 한국어로 둔 것은 현재 HM 서비스 기준이며, 다국어는 소비자가 `locale`을 넘긴다. i18n 시스템은 만들지 않았다.
- 주 시작 요일은 locale 기본(한국어는 일요일).
- `captionLayout="dropdown"`(월/연도 select)의 시각은 스타일링하지 않았다(`.rdp-dropdown*`). 사용이 정해지면 추가.
- 주 번호(`showWeekNumber`), footer, 달력 애니메이션(`animate`)은 스타일링하지 않았다.
- **모바일**: 한 달은 276px로 390px 폭에 들어간다. `numberOfMonths > 1`은 좁은 화면에서 줄바꿈으로 세로로 쌓이며 별도 responsive calendar는 만들지 않았다.
- Range의 hover 미리보기, 최소/최대 길이 표현은 없음.
## 12. 관련 파일
```
packages/ui/src/components/ui/calendar.tsx
packages/ui/src/theme/calendar.css
packages/ui/package.json (dependency: react-day-picker)
packages/ui/src/theme.css (import)
packages/ui/src/index.ts (export)
src/components/calendar.stories.tsx
```
## 13. 검증 상태
- 2026-10-06 Visual QA: Two months는 1280/1024/768px 한 행, 390px wrap, 모든 month 폭 252px 및 페이지 가로 overflow 없음. Production CSS 변경 없음. `typecheck`, `build`, `build-storybook` 통과.
- `npm run typecheck`, `npm run build`: 통과(2026-10-02).
- 실제 렌더/DOM(Browser pane): 한국어 caption "2026년 10월", 요일 "일월화수목금토", `role="grid"`, 날짜 폭 36px, Selected `rgb(18,51,40)`·흰 글자, Today `rgb(244,241,234)`·700, 이전/다음 달 버튼 label이 한국어("이전 달로 이동"/"다음 달로 이동").
- pointer: 날짜 클릭으로 선택(`aria-selected=true`), 다음/이전 달 버튼으로 월 이동(10월 → 11월, 10월 → 9월).
- keyboard: 포커스 후 ArrowRight 10 → 11, ArrowDown 11 → 18(다음 주), Enter와 Space로 선택, PageDown으로 다음 달(11월), Home으로 주의 첫 날. roving tabindex가 선택된 날짜에 있음.
- disabled: 5일 이전과 일요일은 `data-disabled`, 버튼 `disabled`, opacity 0.4, 클릭해도 선택 안 됨. 가능한 날짜는 선택됨.
- range: 시작/끝은 Active 면(안쪽 직각), 중간은 accent 45% 면(모서리 0)으로 구분됨(처음에는 `data-selected` 규칙이 중간 면을 덮는 버그가 있어 specificity를 올려 수정).
- Responsive(390×844): Calendar 폭 276px(두 달은 줄바꿈되어 356px), 페이지 가로 overflow 없음.
- 미확인: 실제 pointer hover 면 시각, focus ring 시각, `multiple` 모드, `captionLayout="dropdown"`, RTL, 타임존(`@date-fns/tz`) 동작.