# Time Input 시와 분을 세그먼트 단위로 타이핑해 넣거나, 시계 버튼으로 값 목록 패널을 열어 고르는 시간 입력 필드입니다. > 원본: `03_ASSET_GUIDE/catalog/time-input.js` · CSS: `03_ASSET_GUIDE/styles/elements/time-input.css` > kind `component` · category `action-input` · status `stable` · specSource `measured` --- ## 1. 구성 하나의 상자 안에 **Segment** 여러 개가 들어 있고, 그 사이를 **Separator** 가 나눕니다. 초점은 세그먼트 하나에 놓이고, 표시는 두 겹입니다. 상자에는 초점 링이, 그 안의 세그먼트 하나에는 채움 표시가 붙습니다. 상자 밖으로는 **Popover** 가 한 겹 더 있습니다. Trigger 를 누르면 열리는 값 목록이고, 같은 컨트롤의 두 번째 입력 경로입니다. [Panel](#panel) 에서 다룹니다. ``` ┌─────────────────────────────┐ │ [07] : [30] [AM] [🕒] │ └─────────────────────────────┘ ① ②③ ④ ⑤ ⑥ ⑦ ┌──────┬──────┬──────┐ │ 08 │ 29 │ 오전 │ ← ⑧ Popover · ⑨ Column · ⑩ Cell │ [09] │ [30] │ 오후 │ └──────┴──────┴──────┘ ``` | # | 요소 | 필수 | 클래스 | 설명 | |---|---|---|---|---| | ① | Field | 필수 | `atom-time-input` | 상자와 초점 링을 소유합니다. 높이·모서리·테두리는 [Text Input](./text-input.md) 과 같습니다. | | ② | Group | 필수 | `atom-time-input__group` | `role="group"` 으로 세그먼트들을 하나의 컨트롤로 묶고, 필드의 이름을 답니다. | | ③ | Hour segment | 필수 | `atom-time-input__segment` `[data-segment="hour"]` | `HH`. `role="spinbutton"` 이고 따로 초점을 받습니다. | | ④ | Separator | 필수 | `atom-time-input__separator` | 세그먼트 사이의 `:` 입니다. 초점을 받지 않고 스크린리더에서도 감춥니다. | | ⑤ | Minute segment | 필수 | `atom-time-input__segment` `[data-segment="minute"]` | `mm`. (`withSeconds` 면 `ss` 가 뒤에 붙습니다) | | ⑥ | Meridiem segment | 12시간제만 | `atom-time-input__segment` `[data-segment="meridiem"]` | `AM/PM`. | | ⑦ | Trigger | 선택 | `atom-time-input__trigger` | Popover 를 여는 아이콘 버튼입니다. 선택 사항입니다. | | ⑧ | Popover | Trigger 가 있을 때 | `atom-time-input__popover` | 필드 아래에 뜨는 판입니다. 필드가 앵커이고, 열리지 않았을 때는 `[data-state="closed"]` 로 그려지지 않습니다. | | ⑨ | Column | Popover 안 | `atom-time-input__column` | 단위 하나당 하나입니다. 각각 `role="listbox"` 이고 이름은 `aria-label` 이 답니다. 눈에 보이는 열 제목은 두지 않습니다. | | ⑩ | Cell | Popover 안 | `atom-time-input__cell` | 열 안의 값 하나입니다. `role="option"` + `aria-selected` 이고, 고를 수 없는 값은 `aria-disabled` 입니다. | > [!NOTE] > **초점 표시가 두 개인 이유** > 상자의 링은 "지금 이 컨트롤 안에 있다", 세그먼트의 채움은 "화살표 키가 바꿀 숫자는 이것" > 입니다. 둘을 같은 색·같은 모양으로 그리면 `↑` 를 눌렀을 때 바뀔 값을 구분할 수 > 없습니다. --- ## 2. 종류 / 크기 ### Format 24시간제는 `HH` 가 `00`–`23` 이고 세그먼트가 둘입니다. 12시간제는 `hh` 가 `01`–`12` 이고 뒤에 `AM`/`PM` 세그먼트가 하나 더 붙습니다. | format | 시 범위 | 세그먼트 | 표시 예 | | --- | --- | --- | --- | | `24h` | 00–23 | HH · mm | `18:05` | | `12h` | 01–12 | hh · mm · AM/PM | `06:05 PM` | > [!WARNING] > **형식은 로케일이 정합니다** > `ko-KR` · `de-DE` 는 24시간제, `en-US` 는 12시간제로 읽고 씁니다. 화면 안에 12/24 를 > 바꾸는 토글을 두면 같은 폼의 두 필드가 서로 다른 형식이 되고, 저장된 값을 다시 열었을 때 > 어느 쪽으로 읽어야 할지 알 수 없습니다. `format` 은 로케일에서 끌어오고, 개별 화면에서 > 덮어쓰지 마세요. > [!NOTE] > **표시 형식과 저장 형식은 별개입니다** > 화면에 `06:05 PM` 이 보여도 값은 24시간제 `18:05` 로 다룹니다. 12시간제를 그대로 > 저장하면 자정·정오(`12 AM` · `12 PM`)에서 해석이 갈립니다. ### Seconds `ss` 세그먼트는 **선택 사항이고 기본은 꺼짐**입니다. 켜면 채워야 할 칸이 둘에서 셋으로 늘고, 키 입력이 4번에서 6번으로 늘어납니다. 대부분의 폼에서는 끕니다. 예약·근무 시간·알림처럼 사람이 정하는 시각은 분 단위이므로 초는 늘 `00` 입니다. 로그 시각·타이머·측정값처럼 **초가 실제로 다른 값을 가질 때만** 켜세요. ### Step 세그먼트마다 `↑` `↓` 가 한 번에 얼마나 옮길지 정합니다. `hourStep` · `minuteStep` · `secondStep` 세 축이고 기본은 모두 `1` 입니다. 30분 단위로만 예약을 받는 화면이라면 `minuteStep: 30` 을 줍니다. | minuteStep | 동작 | 쓰는 곳 | | --- | --- | --- | | `1` | 1분씩 | 정확한 시각을 그대로 적는 경우 (기본) | | `5` | 다음 5의 배수로 | 회의·통화 예약 | | `10` | 다음 10의 배수로 | 배차·픽업 시간 | | `30` | 다음 30의 배수로 | 진료·상담 슬롯 | > [!NOTE] > **step 은 패널의 칸 수도 정합니다** > step 은 세그먼트의 증감 폭이면서 [Panel](#panel) 해당 열의 값 목록입니다. > `minuteStep: 30` 이면 분 열에는 `00` 과 `30` 두 칸만 있습니다. 두 경로의 유효값 > 집합을 같게 유지하기 위해서입니다. `step` 이 1보다 크면 증감은 **다음 배수로 스냅**합니다. `14:31` 에서 `step: 5` 로 `↑` 를 누르면 `14:36` 이 아니라 `14:35` 입니다. 다음 시로 넘어갈 때도 같습니다. `14:45` 에서 `minuteStep: 30` 이면 `15:00` 입니다. > [!WARNING] > **타이핑까지 step 으로 막지 않습니다** > `14:37` 을 직접 입력하면 `14:35` 로 고치지 않고 그대로 둡니다. `step` 은 `↑` `↓` 의 > 폭에만 적용되고, 타이핑한 값은 **커밋 시점에 검증**해 [Field](./field.md) 의 오류로 > 알립니다. > [!CAUTION] > **원본 안의 충돌** — 위 "`14:45` 에서 `minuteStep: 30` 이면 `15:00`" 은 시가 함께 올라가는 동작이지만, > [접근성](#6-접근성) 의 "23시에서 `↑` 는 0시로 순환합니다" 는 **"분이 `59` 에서 `00` 으로 넘어가도 시는 올라가지 않습니다"** 라고 적습니다. > 구현은 접근성 쪽(세그먼트 안에서만 순환)을 따라 `14:45` → `14:00` 입니다. ### Size 높이는 [Text Input](./text-input.md) 과 같은 축을 씁니다. 시간 필드와 텍스트 필드가 한 줄에 섞이는 폼에서 두 상자의 아랫변이 어긋나지 않게 하려는 것입니다. | size | 높이 | 좌우 여백 | 글자 | 모서리 | | --- | --- | --- | --- | --- | | `small` | 40px | 12px | 14px | `8px` | | `medium` | 48px | 16px | 15px | `8px` | | `large` | 56px | 20px | 16px | `12px` | ### Variant `outline` 이 기본이고, 회색 표면 위에서는 `weak` 를 씁니다. 두 값 모두 초점·오류 링은 같습니다. ### Trigger 오른쪽 끝의 시계 아이콘 버튼입니다. 누르면 [Panel](#panel) 이 열려서, **아는 시각은 타이핑으로 · 찾아야 하는 시각은 목록으로** 두 경로를 한 필드에서 제공합니다. 트리거 없이 타이핑만 받는 형태도 그대로 지원합니다. 필드의 기본 폭은 **내용 폭**입니다. 시간 값은 길이가 고정되어 있기 때문입니다. 다른 컨트롤과 왼쪽·오른쪽 끝을 맞춰야 하면 `--block` 을 주세요. 트리거는 그때 오른쪽 끝으로 밀려납니다. ### Panel Trigger 로 열리는 값 목록입니다. 열마다 그 단위의 값 전체가 들어 있고 스크롤됩니다. 패널은 휠이 아닌 스크롤 목록입니다. 패널은 고를 수 있는 시각을 훑어보는 용도이고, 한 칸씩만 보이는 휠은 여기에 맞지 않습니다. 아는 시각은 세그먼트에서 4번의 키 입력으로 넣습니다. [Ant Design TimePicker](https://ant.design/components/time-picker) 를 따랐습니다. 한 열에 8칸이 보이고 9번째가 아래 경계에서 잘립니다. 잘린 반 칸으로 값이 더 있음을 표시합니다. 목록이 경계에 맞춰 끝나면 목록이 끝난 것으로 보입니다. 패널은 필드의 **글줄 시작 쪽 끝에 맞춰** 열립니다. 오른쪽으로 넘칠 자리가 없으면 `--align_end` 로 반대쪽 끝에 붙입니다. 넘침 여부는 CSS 로 판단할 수 없으므로, 구현에서 필드의 좌표를 측정해 이 modifier 를 바꿉니다. #### 열 구성은 format 이 정합니다 별도 프롭 없이 `format` 과 `withSeconds` 가 열을 결정합니다. | 설정 | 열 | | --- | --- | | `24h` (기본) | 시 `00`–`23` · 분 | | `24h` + `withSeconds` | 시 · 분 · 초 | | `12h` | 시 `01`–`12` · 분 · 오전 오후 | **오전·오후 열은 맨 오른쪽**입니다. 한국어 어순만 보면 "오전 10시 30분" 이므로 왼쪽이 자연스럽지만, 12시간제 필드의 세그먼트가 `hour : minute meridiem` 순서로 `AM` 을 뒤에 둡니다. 두 경로의 순서를 맞추기 위해 필드 쪽을 따릅니다. 값은 패널에서도 24시간제입니다. 화면에 `06:05 PM` 이 보여도 값은 `18:05` 입니다. #### 언제 확정되나 셀을 누르면 그 열의 값이 **즉시** 반영됩니다. 확인 버튼은 없습니다. 마지막 열까지 채워지면 패널이 닫히고 초점은 필드로 돌아옵니다. `readonly` · `disabled` 필드에서는 열리지 않습니다. Trigger 가 그 두 상태를 함께 받습니다. ### 선택할 수 없는 시각 `disabledTime` 이 열별로 막을 값을 돌려줍니다. 마감된 예약 슬롯, 이미 지난 시각, 영업 시간 밖이 여기에 해당합니다. ```ts disabledTime?: () => { disabledHours?: () => number[]; disabledMinutes?: (hour: number) => number[]; disabledSeconds?: (hour: number, minute: number) => number[]; }; ``` 분과 초는 앞 단위를 인자로 받습니다. "09시에는 30분부터" 처럼 시각에 따라 달라지는 규칙을 표현하려면 그 정보가 필요합니다. #### min · max 와 역할이 다릅니다 두 프롭은 적용 시점과 대상이 다릅니다. | | 시점 | 대상 | 동작 | | --- | --- | --- | --- | | `min` · `max` | 값이 완성된 뒤 | 필드 | **검증만.** 타이핑을 막지 않습니다 | | `disabledTime` | 패널을 그릴 때 | 패널 | 해당 셀을 **고를 수 없게** 만듭니다 | `min`/`max` 는 입력 후에 검증하고, `disabledTime` 은 입력 전에 선택지를 막습니다. > [!WARNING] > **패널이 있으면 둘을 함께 채웁니다** > `min`/`max` 만 두고 `disabledTime` 을 비우면, 패널이 고를 수 있게 보여준 값이 고른 > 직후에 오류가 됩니다. 반대로 `disabledTime` 만 두면 타이핑 경로에는 아무 규칙이 없습니다. ### 클래스 계약 `styles/elements/time-input.css` 헤더와 원본 `registry.json` `css.blocks` 기준입니다. 클래스가 없는 값(—)은 기본 스타일 그대로입니다. | Prop · 축 | 값 | 클래스 | |---|---|---| | `size` | `small` · `medium` · `large` | `--size_small` · — · `--size_large` | | `variant` | `outline` · `weak` | — · `--variant_weak` | | `block` | `false` · `true` | — · `--block` | | (align) | 패널 끝 맞춤 | `__popover--align_end` | 상태 선택자: `:hover` · `:focus-within` · `[data-focused]` · `[data-invalid]` · `[data-readonly]` · `[data-disabled]` 세그먼트: `[data-segment="hour|minute|second|meridiem"]` · `[data-placeholder]` · `[data-active]` · `:focus-visible` 트리거: `[disabled]` · 패널: `[data-state="open|closed"]` · 셀: `[aria-selected]` · `[aria-disabled]` · `:focus-visible` --- ## 3. 상태 상태는 필드 전체에 붙는 것과 세그먼트 하나에 붙는 것으로 나뉩니다. 둘을 구분해야 화살표 키가 바꿀 세그먼트가 화면에 드러납니다. | 대상 | 속성 | 결과 | | --- | --- | --- | | Field | `:focus-within` | 2px 초점 링. `data-focused` 는 같은 모양의 정적 대역입니다 | | Field | `data-invalid` | 링이 critical 색. 초점보다 우선합니다 | | Field | `data-readonly` | 약한 배경 + 테두리 없음. Tab 은 그대로 되고 값만 못 바꿉니다 | | Field | `data-disabled` | 투명도 45% + 포인터 차단. 탭 순서에서도 뺍니다 | | Segment | `data-active` | 브랜드 채움. `:focus-visible` 과 같은 모양입니다 | | Segment | `data-placeholder` | `HH` · `mm` 을 subtle 색으로 | > [!NOTE] > **`readonly` 와 `disabled` 는 다릅니다** > `readonly` 는 값을 보여주는 상태로, 초점을 받고 복사할 수 있고 스크린리더가 > 읽습니다. `disabled` 는 지금 상관없는 필드입니다. 확정된 예약 시각을 보여주는 자리에 > `disabled` 를 쓰면 값 자체가 읽히지 않습니다. > [!NOTE] > 구현은 `[data-focused]` 를 붙이지 않고 `:focus-within` 에 맡깁니다. 초점을 가진 세그먼트에는 `data-active` 를 붙입니다 > (포인터로 누른 세그먼트는 `:focus-visible` 이 맞지 않아서). `min`/`max` 를 벗어난 완성 값은 필드를 떠난 뒤 `data-invalid` 가 됩니다. --- ## 4. Props 값은 24시간제 `HH:mm` 또는 `HH:mm:ss` 문자열로 주고받습니다. 화면 표시가 12시간제여도 마찬가지입니다. | Name | Type | Default | Required | Description | | --- | --- | --- | --- | --- | | `value` | `string \| null` | | | 현재 값입니다. 24시간제 `"14:30"` · `"14:30:12"` 형식이고, 비었으면 `null` 입니다. | | `defaultValue` | `string \| null` | | | 비제어로 쓸 때의 처음 값입니다. | | `format` | `"12h" \| "24h"` | locale | | 로케일에서 끌어옵니다. 개별 화면에서 덮어쓰지 마세요. | | `withSeconds` | `boolean` | `false` | | `ss` 세그먼트를 붙입니다. | | `hourStep` | `number` | `1` | | 시 세그먼트의 증감 폭이자 패널 시 열의 칸 간격입니다. | | `minuteStep` | `number` | `1` | | 분 세그먼트의 증감 폭이자 패널 분 열의 칸 간격입니다. 1보다 크면 다음 배수로 스냅합니다. | | `secondStep` | `number` | `1` | | 초 세그먼트의 증감 폭이자 패널 초 열의 칸 간격입니다. `withSeconds` 일 때만 씁니다. | | `min` | `string` | | | 허용 하한입니다. 검증에만 쓰이고 타이핑을 막지 않습니다. | | `max` | `string` | | | 허용 상한입니다. `min` 과 같은 규칙입니다. | | `disabledTime` | `() => DisabledTime` | | | 패널에서 고를 수 없는 값을 열별로 돌려줍니다. `min`/`max` 와 역할이 다릅니다. | | `hideDisabledOptions` | `boolean` | `false` | | 고를 수 없는 셀을 흐리게 남기지 않고 목록에서 뺍니다. | | `size` | `"small" \| "medium" \| "large"` | `"medium"` | | 상자 높이와 좌우 여백입니다. Text Input 과 같은 축입니다. | | `variant` | `"outline" \| "weak"` | `"outline"` | | 테두리형과 약한 배경형입니다. | | `block` | `boolean` | `false` | | 폼 행의 폭을 채웁니다. 기본은 내용 폭입니다. | | `withTrigger` | `boolean` | `false` | | 오른쪽 끝에 패널을 여는 시계 버튼을 둡니다. | | `open` | `boolean` | | | 패널 열림을 밖에서 제어합니다. 비제어로 쓰면 Trigger 가 소유합니다. | | `invalid` | `boolean` | `false` | | 오류 링을 켭니다. 문구는 Field 가 그립니다. | | `readOnly` | `boolean` | `false` | | 초점은 받되 값이 바뀌지 않습니다. | | `disabled` | `boolean` | `false` | | 탭 순서에서 빠지고 포인터도 받지 않습니다. | | `label` | `string` | | ● | `role="group"` 에 붙는 필드 이름입니다. Field 로 감싸면 그쪽에서 연결됩니다. | | `onChange` | `(value: string \| null) => void` | | | 세그먼트가 모두 채워져 값이 완성될 때마다 호출됩니다. | | `onSegmentChange` | `(segment, value) => void` | | | 세그먼트 하나가 바뀔 때마다 호출됩니다. 미완성 상태를 관찰할 때 씁니다. | | `onOpenChange` | `(open: boolean) => void` | | | 패널이 열리거나 닫힐 때 호출됩니다. | > [!NOTE] > 구현(`@atom/ui`)에서 `label` 은 타입상 선택입니다 — Field 안에서는 Field 의 라벨이 `aria-labelledby` 로 연결되기 때문입니다. Field 밖에서는 반드시 줍니다. > `DisabledTime` 타입은 `TimeInputDisabledTime` 으로 export 됩니다. 완성됐던 값이 지워지면 `onChange(null)` 이 호출됩니다. ### Parts (compound) | Part | 요소 | 설명 | |---|---|---| | `TimeInput` | `