# Field 입력 컴포넌트에 라벨과 도움말, 오류 메시지, 글자 수를 붙여 하나의 완결된 입력 단위로 만드는 래퍼입니다. > 원본: `03_ASSET_GUIDE/catalog/field.js` · CSS: `03_ASSET_GUIDE/styles/elements/field.css` > kind `component` · category `display` · status `stable` · specSource `measured` > sub-asset: [Label](#label) (`__header` 안의 `__label` · `__required` · `__optional`) --- ## 1. 구성 Field 는 컨트롤을 직접 그리지 않습니다. **Control 은 슬롯**이고, 그 위아래에 놓이는 텍스트만 Field 의 책임입니다. ``` 라벨 * 선택 ← Header ①② (⑥) ┌──────────────────────────────────┐ │ 플레이스홀더 │ ← Control (slot) ③ └──────────────────────────────────┘ 도움말 텍스트 0/500 ← Footer ④ ⑤ ``` | # | 요소 | 필수 | 클래스 | 설명 | |---|---|---|---|---| | — | Root | 필수 | `atom-field` | 세로 배치, 요소 사이 6px. 상태는 `[data-invalid]` · `[data-disabled]` 로 표시합니다. | | — | Header | 필수 | `atom-field__header` | Label · Required mark · Optional 을 담는 가로 줄(`--atom-x2` 간격). | | ① | Label | 필수 | `atom-field__label` | 무엇을 입력하는 칸인지 알려줍니다. 자세한 규칙은 [Label](#label) 을 보세요. | | ② | Required mark | 선택 | `atom-field__required` | 필수 항목임을 나타내는 `*`. 선택 항목에는 대신 '선택' 을 붙입니다. | | ⑥ | Optional | 선택 | `atom-field__optional` | 선택 항목임을 나타내는 텍스트. | | ③ | Control | 필수 | `atom-field__control` | [Input](./text-input.md), [Select](./select.md), [Textarea](./text-input.md#textarea) 등 어떤 입력 컴포넌트든 들어갑니다. | | — | Separator | 선택 | `atom-field__separator` | 범위 입력의 두 입력 사이 텍스트(‘~’ 나 ‘부터’). | | — | Body | `horizontal` 일 때 | `atom-field__body` | 가로 배치에서 Control 과 Footer 를 묶는 세로 열. | | — | Footer | 선택 | `atom-field__footer` | Description(또는 Error)과 Counter 를 한 줄에 담습니다. | | ④ | Description | 선택 | `atom-field__description` | 입력 형식이나 이유를 설명합니다. 오류가 나면 Error 로 교체됩니다. | | — | Error | `invalid` 일 때 | `atom-field__error` · `atom-field__error-icon` | Description 을 대신하는 오류 메시지와 13px 아이콘. | | ⑤ | Counter | 선택 | `atom-field__counter` | 글자 수 제한이 있을 때만 노출합니다. | > [!NOTE] > 이 컴포넌트는 **어떤 컨트롤과도 조합됩니다.** 데모의 [Input](./text-input.md) 은 > 예시입니다. ### Control 슬롯의 조합 Control 슬롯에는 입력 하나, 입력과 버튼(값을 다른 화면에서 가져올 때), 구분 텍스트를 사이에 둔 두 입력(범위)이 들어갈 수 있습니다. 어느 경우든 **같은 슬롯 하나**이며, 아래 두 규칙은 슬롯이 적용합니다. | 조합 | 구성 | 쓰는 곳 | | --- | --- | --- | | 단독 | Input | 대부분의 입력 | | Input + Button | Input + [Button](./button.md) | 우편번호 찾기, 중복 확인처럼 값을 다른 화면에서 가져올 때 | | 범위 입력 | Input + `__separator` + Input | 기간 · 금액 · 수량의 최소와 최대 | | Input + Icon Button | Input + `--layout_icon-only` Button | 라벨을 쓸 자리가 없는 좁은 필터 줄 | | 선택 컨트롤 단독 | Checkbox · Radio · Switch (그룹 포함) | 켜고 끄는 조건, 다중 선택 조건 | - **요소 사이 간격은 4px** — 슬롯이 `gap: var(--atom-x1)` 으로 줍니다. 조합을 쓰는 쪽에서 여백을 따로 만들지 않습니다. - **슬롯 안의 요소는 모두 같은 높이** — 한 줄로 읽히는 하나의 입력 단위여야 합니다. 컨트롤은 모두 `--atom-control-h` 를 쓰므로 같은 `size` 를 주면 이미 맞고, 슬롯의 `align-items: stretch` 가 나머지를 끌어올립니다. 폭은 지정하지 않습니다. 슬롯의 요소는 모두 `width: 100%` 에서 출발해 함께 줄어들기 때문에, 줄지 않는 요소(Button, `__separator`)가 자기 몫을 먼저 가져가고 남은 폭이 입력에 돌아갑니다. 범위 입력의 두 입력도 같은 이유로 같은 폭이 됩니다. `flex` 는 지정하지 않습니다. > [!WARNING] > **Button 의 고정 높이는 슬롯이 풀어줍니다** > 컨트롤은 모두 `--atom-control-h`(48px)를 쓰지만 Button 만 `height: 47px` 로 고정이라, > 그대로 두면 48px 옆에 47px 이 섭니다. 그래서 슬롯은 **조합일 때만** > (`:not(:only-child)`) Button 의 height 를 풀어 행 높이에 맞춥니다. 혼자 있을 때 풀면 > 맞출 행 높이가 없어 글자 높이로 줄어들므로 풀지 않습니다. 슬롯 안의 포커스 링은 > `--atom-control-border-focus` 를 씁니다. `base.css` 의 링은 요소 **밖** 2px 에 그려져 > 4px 간격 옆의 요소와 겹치기 때문입니다. #### Icon Button 은 정사각형입니다 `--layout_icon-only` Button 의 고유 너비(32 · 38 · 47 · 54px)는 Button 의 높이를 따라간 값입니다. 슬롯에서는 행이 높이를 정하므로, 너비를 **컨트롤 높이 토큰**으로 다시 걸어 1:1 이 되게 합니다. 어느 단계를 쓸지는 Button 에 이미 붙어 있는 `size` modifier 가 고릅니다 — 옆 컨트롤에 주는 것과 같은 modifier 입니다. | Button 의 `size` | 슬롯 안에서의 크기 | 아이콘 | | --- | --- | --- | | `xsmall` · `small` | 40 × 40 | 16px | | `medium` (기본) | 48 × 48 | 19.2px | | `large` | 56 × 56 | 22.4px | 아이콘 크기는 상자의 **40%** 입니다. Button 자체 비율(38–42%) 범위 안의 값이라 세 높이에서 차이가 거의 없습니다. > [!NOTE] > **1:1 은 옆 컨트롤과 `size` 를 맞췄을 때 성립합니다** > 높이는 행이 정하고 너비는 modifier 가 정하므로, `medium` Icon Button 을 > `large` Input 옆에 두면 56 × 48 이 됩니다. 높이는 여전히 맞지만 정사각형이 아닙니다. > 슬롯 안에서는 모든 요소에 같은 `size` 를 줍니다. #### Checkbox · Radio · Switch 만 있으면 가운데 정렬 이 셋은 **마커가 붙은 한 줄의 텍스트**라 컨트롤 높이가 없습니다. 그대로 두면 슬롯이 글자 높이만큼만 되어 옆 필드의 48px Input 보다 위에 놓입니다. 슬롯에 컨트롤 높이를 주고 가운데 정렬해, 마커를 옆 Input 의 글자와 같은 선에 맞춥니다. | 슬롯 내용 | 슬롯 높이 | 정렬 | | --- | --- | --- | | Checkbox · Radio · Switch 단독 (그룹 포함) | `--atom-control-h` 이상 | 세로 가운데 | | `--size_small` Field 안 | `--atom-control-h-small` 이상 | 세로 가운데 | `min-height` 이므로 항목이 많은 세로 그룹은 그대로 늘어납니다. 다른 컨트롤과 함께 있는 조합은 행 높이가 이미 정해져 있으므로 적용되지 않습니다. --- ## 2. 종류 / 크기 ### 필수 · 선택 표시 한 폼 안에서 필수와 선택 중 **더 적은 쪽만** 표시하세요. 대부분이 필수라면 선택 항목에 '선택' 을 붙이고, 대부분이 선택이라면 필수에 `*` 를 붙입니다. | 상황 | 표시 방법 | | --- | --- | | 필드의 2/3 이상이 필수 | 선택 항목에만 **"(선택)"** | | 그 외 | 필수 항목에만 **`*`** | 한 폼 안에서 두 방식을 섞지 않습니다. `*` 를 쓸 때는 폼 어딘가에 그 뜻을 밝히는 안내를 함께 둡니다. ### Layout 기본은 라벨이 위에 오는 `vertical` 입니다. 라벨 길이가 고르고 폼이 넓은 데스크톱 화면에서만 `horizontal` 을 씁니다. | Layout | 구조 | | --- | --- | | `vertical` (기본) | 세로 배치, 요소 사이 6px | | `horizontal` | Header 폭 120px 고정 + 위 여백 12px, 본문과 `--atom-x5`(20px) 간격 | 세로가 기본인 이유는 긴 라벨과 번역에 강하기 때문입니다. 가로 배치는 라벨 폭이 고정이라 언어가 바뀌면 잘립니다. ### Size | Size | Label | Footer | | --- | --- | --- | | `medium` (기본) | 13px | 12px | | `small` | 12px | 11px | 컨트롤의 `size` 와는 별개입니다. Field 를 `small` 로 두고 컨트롤을 `medium` 으로 둘 수 있으므로, 함께 맞추는 것은 사용하는 쪽의 책임입니다. ### Counter 글자 수 제한이 실제로 걸려 있을 때만 노출합니다. 제한이 없는데 세는 것은 사용자에게 불필요한 부담입니다. Counter 는 Footer 의 오른쪽 끝에 `margin-left: auto` 로 붙고, `font-variant-numeric: tabular-nums` 가 걸려 숫자가 바뀔 때 폭이 흔들리지 않습니다. ### Footer 의 구조 Footer 는 Description(또는 Error)과 Counter 를 한 줄에 담는 가로 배치입니다. `justify-content: space-between` 이라 왼쪽에 설명, 오른쪽에 카운터가 놓입니다. Error 에는 13px 아이콘이 붙고, `align-items: flex-start` 에 아이콘만 `margin-top: 2px` 로 첫 줄 글자 중심에 맞춥니다. Description 과 Error 에는 `word-break: keep-all` 이 걸려 있어 **한국어가 단어 중간에서 끊기지 않습니다.** 이 컴포넌트는 그 규칙을 CSS 에 직접 담고 있는 유일한 엘리먼트입니다. ### 클래스 계약 `styles/elements/field.css` 헤더와 원본 `registry.json` `css.blocks` 기준입니다. 클래스가 없는 값(—)은 기본 스타일 그대로입니다. | Prop | 값 | 클래스 | |---|---|---| | `layout` | `vertical` · `horizontal` | — · `--layout_horizontal` | | `size` | `small` · `medium` | `--size_small` · — | | (레거시) | `invalid` | `--invalid` — CSS 에만 정의, 헤더 미선언. 새 코드는 `[data-invalid]` | Parts: `__header` · `__label` · `__required` · `__optional` · `__control` · `__separator` · `__body` · `__footer` · `__description` · `__error` · `__error-icon` · `__counter` 상태 선택자: `[data-invalid]` · `[data-disabled]` · `:focus-visible`(슬롯 안 조합 Button) --- ## 3. 상태 `invalid` 는 Description 을 Error 로 교체하고, `disabled` 는 라벨까지 포함해 전체를 흐리게 만듭니다. | State | 표현 | 선택자 | | --- | --- | --- | | `invalid` | Description 을 `--atom-fg-critical` 로 | `[data-invalid]` | | `disabled` | 전체 `opacity: 0.45` | `[data-disabled]` | > [!WARNING] > **오류는 컨트롤에도 함께 표시해야 합니다** > `data-invalid` 는 Field 의 **텍스트만** 바꿉니다. 컨트롤의 테두리도 함께 오류 색이 > 되어야 하므로, 컨트롤 자신의 invalid 상태(`.atom-text-input--invalid`, > `.atom-select[data-invalid]`)도 같이 켜세요. 두 곳을 따로 켜야 하는 구조입니다. > (구현에서는 Field 가 `useFieldControl()` 로 `invalid` 를 내려주고, 슬롯의 컨트롤이 자기 invalid 표현을 함께 켭니다.) --- ## 4. Props | Prop | Type | Default | 필수 | 설명 | |---|---|---|---|---| | `label` | `ReactNode` | | ● | 입력 칸의 이름입니다. | | `required` | `boolean` | `false` | | 라벨 뒤에 `*` 를 붙입니다. | | `optionalText` | `string` | | | 선택 항목임을 알리는 텍스트입니다. 예: `"선택"` | | `description` | `ReactNode` | | | 컨트롤 아래 도움말입니다. | | `errorMessage` | `ReactNode` | | | `invalid` 일 때 description 을 대신합니다. | | `invalid` | `boolean` | `false` | | 오류 상태로 바꿉니다. | | `disabled` | `boolean` | `false` | | 라벨까지 포함해 전체를 흐리게 만듭니다. | | `layout` | `"vertical" \| "horizontal"` | `"vertical"` | | 라벨의 위치를 결정합니다. | | `size` | `"small" \| "medium"` | `"medium"` | | 라벨과 푸터의 폰트 크기입니다. | | `count` | `{ current: number; max: number }` | | | 글자 수 카운터를 노출합니다. | | `children` | `ReactNode` | | ● | 감쌀 컨트롤입니다. 하나여도 되고, Button 이나 `__separator` 를 포함한 조합이어도 됩니다. | ### Parts (compound) `label` 을 주면 Field 가 Header · Control · Footer 를 직접 그립니다. `label` 을 생략하면 아래 파트로 직접 구성합니다. | Part | 요소 | 설명 | |---|---|---| | `Field` | `
` | Root. 위 Props 를 받고 Control 슬롯에 `useFieldControl()` 계약(controlId · labelId · describedBy · invalid · disabled · required · size)을 내려줍니다. `horizontal` 이면 Header 외의 요소를 `__body` 로 감쌉니다. | | `Field.Header` | `
` | Label · Optional 을 담는 가로 줄. | | `Field.Label` | `