# 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`) 동작.