# Segmented Text Input
URL: /react/components/segmented-text-input
Source: https://github.com/daangn/seed-design/blob/dev/docs/content/react/components/segmented-text-input.mdx
주민등록번호·카드번호처럼 일부 구간을 가려야 하는 값을 구간마다 별도 입력으로 나눠 받는 컴포넌트입니다.
사용 가능 버전: @seed-design/react@3.1.0, @seed-design/css@3.1.0
## Preview
```tsx
import { Box } from "@seed-design/react";
import { ResidentNumberField } from "seed-design/ui/resident-number-field";
export default function SegmentedTextInputPreview() {
return (
);
}
```
주민등록번호 뒷자리 첫 번째 자리만 받거나 카드번호 뒷자리를 가려야 하는 경우처럼, 하나의 Text Field로 표현하기 어려운 국내 입력 형식에 사용합니다. 대부분의 형식이 정해진 입력은 [Text Field](/react/components/text-field-input)로 충분합니다. 아래 [Text Field와의 차이](#text-field와의-차이)를 보고 고르세요.
Segment마다 입력 하나를 두고, 값은 입력 Segment 순서대로 `string[]`로 다룹니다.
- 끝에서 입력해 Segment를 채우면 다음 Segment로 이동합니다. 가운데에 넣어 자리 수를 넘는 입력은 반영하지 않습니다.
- 빈 Segment에서 Backspace를 누르면 이전 Segment의 마지막 글자를 지웁니다. 끝에서 → 키, 처음에서 ← 키를 누르면 옆 Segment로 이동합니다.
- 가린(`masked`) Segment는 입력한 글자 대신 자리 수만큼 점을 그립니다. 가린 값 안에서는 caret을 옮기지 않고 끝에서만 입력·삭제하며, ←·→ 키는 바로 옆 Segment로 이동합니다.
- 붙여넣은 값은 구분자를 지우고 다음 Segment까지 차례로 채웁니다. 가린(`masked`) Segment 앞에서 멈춥니다.
- `autoComplete="bday"` Segment는 OS가 넣은 생년월일(`yyyy-MM-dd` 등)을 자리 수에 맞춰 바꿉니다.
- 한글 조합 중에는 다음 Segment로 이동하지 않습니다.
## Text Field와의 차이
Text Field는 값 전체를 input 하나로 받고, Segmented Text Input은 Segment마다 input을 따로 둡니다. 이 차이 때문에 두 컴포넌트가 잘하는 일이 다릅니다.
- Text Field: 값 전체를 한 번에 선택(Cmd/Ctrl+A)·복사·수정할 수 있고, 붙여넣기와 OS 자동완성이 그대로 동작합니다. 하이픈 같은 구분자는 formatter로 값에 넣어 보여 줍니다.
- Segmented Text Input: Segment마다 가리기(`masked`)·자리 수·placeholder를 다르게 줄 수 있고, 구분자를 입력값과 다른 색상·레이아웃의 정적 텍스트로 그립니다. 대신 값 전체를 한 번에 선택하거나 수정할 수 없습니다.
전화번호·이메일, 카드 유효기간(`MM / YY`)처럼 가릴 필요가 없는 일반 정보는 Text Field에 formatter를 적용해 받으세요([Number Formatting](/react/components/text-field-input#number-formatting) 참고). 형식이 정해져 있다는 이유만으로 입력을 여러 칸으로 나누지 않습니다.
Segmented Text Input은 Text Field와 formatter로 같은 표현을 손실 없이 만들 수 없을 때 고릅니다. 주민등록번호·국내 카드번호처럼 값의 일부만 가려야 하거나, 구분자와 입력값을 다른 색상·레이아웃으로 보여야 하는 경우가 여기에 해당합니다. 가리기 여부만으로 정해지지는 않으니, Text Field로 충분한지 먼저 확인하세요.
## Installation
주민등록번호와 카드번호는 snippet으로 제공합니다. 자리 구성과 문구(`aria-label`, placeholder)는 snippet에 있으니 필요하면 설치한 파일에서 고쳐 쓰세요. 두 snippet은 label·description·errorMessage를 그리는 `segmented-text-field` snippet 위에 만들어져 있어 함께 설치됩니다.
- npm: npx @seed-design/cli@latest add ui:resident-number-field
- pnpm: pnpm dlx @seed-design/cli@latest add ui:resident-number-field
- yarn: yarn dlx @seed-design/cli@latest add ui:resident-number-field
- bun: bun x @seed-design/cli@latest add ui:resident-number-field
* npm: npx @seed-design/cli@latest add ui:card-number-field
* pnpm: pnpm dlx @seed-design/cli@latest add ui:card-number-field
* yarn: yarn dlx @seed-design/cli@latest add ui:card-number-field
* bun: bun x @seed-design/cli@latest add ui:card-number-field
## Props
### `ResidentNumberField`
### `CardNumberField`
### `SegmentedTextField`
children에 넣는 컴포넌트는 `@seed-design/react`의 컴포넌트를 그대로 내보낸 것입니다.
- `SegmentedTextFieldSegment` → [`SegmentedTextInput.Segment`](#segmentedtextinputsegment)
- `SegmentedTextFieldDots` → [`SegmentedTextInput.Dots`](#segmentedtextinputdots)
- `SegmentedTextFieldSeparator` → `SegmentedTextInput.Separator`
- `SegmentedTextFieldSuffix` → `SegmentedTextInput.Suffix`
### `SegmentedTextInput.Root`
### `SegmentedTextInput.Segment`
`staticText`를 주면 입력 없이 정적 텍스트만 표시하는 Segment가 됩니다. 정적 Segment는 값 배열에 포함하지 않습니다.
### `SegmentedTextInput.Dots`
주민등록번호 뒷자리처럼 가린 자리를 7×7 점으로 표시합니다. `staticText`·`staticSuffix`·`placeholder`에 넣습니다.
```tsx
}
staticSuffix={}
/>
```
## Examples
### Resident Number
`digits`로 입력받을 자리 수를 정합니다. (default: `7`)
```tsx
import { VStack } from "@seed-design/react";
import { ResidentNumberField } from "seed-design/ui/resident-number-field";
export default function SegmentedTextInputResidentNumberDigits() {
return (
);
}
```
### Card Number
`maskedDigits`로 가릴 뒷자리 수를 정하고, `suffix`에 카드사 로고를 넣습니다. 로고 크기는 넣는 쪽에서 정합니다(Figma 기준 30×20).
```tsx
import { VStack } from "@seed-design/react";
import { CardNumberField } from "seed-design/ui/card-number-field";
export default function SegmentedTextInputCardNumber() {
return (
}
/>
);
}
// 예시용 카드사 로고입니다. 실제 서비스에서는 카드사 로고 자산을 크기와 함께 넣습니다.
function CardLogo() {
return (
);
}
```
### State
```tsx
import { VStack } from "@seed-design/react";
import { ResidentNumberField } from "seed-design/ui/resident-number-field";
export default function SegmentedTextInputState() {
return (
);
}
```
### Size
`medium`은 데스크톱(Breakpoint `lg` 이상)에서만 사용합니다. (default: `large`)
```tsx
import { VStack } from "@seed-design/react";
import { CardNumberField } from "seed-design/ui/card-number-field";
export default function SegmentedTextInputSize() {
return (
);
}
```
### Controlled
```tsx
import { useState } from "react";
import { Text, VStack } from "@seed-design/react";
import { ResidentNumberField } from "seed-design/ui/resident-number-field";
export default function SegmentedTextInputControlled() {
const [value, setValue] = useState(["", ""]);
return (
value: {JSON.stringify(value)}
);
}
```
### React Hook Form
`useController`의 `value`·`onChange`를 `value`·`onValueChange`에 연결합니다. 값은 Segment별 `string[]`이므로 `validate`에서 Segment마다 자리 수를 확인합니다.
```tsx
import { HStack, VStack } from "@seed-design/react";
import { useCallback, type FormEvent } from "react";
import { useController, useForm } from "react-hook-form";
import { ActionButton } from "seed-design/ui/action-button";
import { CardNumberField } from "seed-design/ui/card-number-field";
import { ResidentNumberField } from "seed-design/ui/resident-number-field";
interface FormValues {
residentNumber: string[];
cardNumber: string[];
}
export default function SegmentedTextInputReactHookForm() {
const { handleSubmit, reset, control } = useForm({
reValidateMode: "onSubmit",
defaultValues: {
residentNumber: ["", ""],
cardNumber: ["", "", "", ""],
},
});
const { field: residentNumberField, fieldState: residentNumberFieldState } = useController({
name: "residentNumber",
control,
rules: {
validate: ([birth = "", back = ""]) =>
(birth.length === 6 && back.length === 1) || "주민등록번호 앞 7자리를 모두 입력해주세요",
},
});
const { field: cardNumberField, fieldState: cardNumberFieldState } = useController({
name: "cardNumber",
control,
rules: {
validate: (value) =>
value.every((segment) => segment.length === 4) || "카드번호 16자리를 모두 입력해주세요",
},
});
const onValid = useCallback((data: FormValues) => {
window.alert(JSON.stringify(data, null, 2));
}, []);
const onReset = useCallback(
(event: FormEvent) => {
event.preventDefault();
reset();
},
[reset],
);
return (
초기화
제출
);
}
```
### External Keypad
보안 키패드처럼 값을 밖에서 채워야 하면 해당 Segment를 `readOnly`로 두고 값을 controlled로 관리합니다. Segment를 누르면 키패드를 열고, 키패드가 `value`를 바꿉니다. 키패드 연동과 값 암호화는 서비스에서 구현합니다.
```tsx
import { Fragment, useState } from "react";
import { HStack, VStack } from "@seed-design/react";
import { ActionButton } from "seed-design/ui/action-button";
import {
SegmentedTextField,
SegmentedTextFieldSegment,
SegmentedTextFieldSeparator,
} from "seed-design/ui/segmented-text-field";
const LABELS = ["1-4번째 자리", "5-8번째 자리", "9-12번째 자리", "13-16번째 자리"];
const KEYPAD_SEGMENTS = [2, 3];
const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"];
export default function SegmentedTextInputExternalKeypad() {
const [value, setValue] = useState(["", "", "", ""]);
const [keypadOpen, setKeypadOpen] = useState(false);
const press = (key: string) =>
setValue((prev) => {
const index = KEYPAD_SEGMENTS.find((i) => prev[i].length < 4);
if (index === undefined) return prev;
return prev.map((segment, i) => (i === index ? segment + key : segment));
});
const erase = () =>
setValue((prev) => {
const index = KEYPAD_SEGMENTS.findLast((i) => prev[i].length > 0);
if (index === undefined) return prev;
return prev.map((segment, i) => (i === index ? segment.slice(0, -1) : segment));
});
return (
{LABELS.map((label, i) => {
const usesKeypad = KEYPAD_SEGMENTS.includes(i);
return (
{i > 0 && - }
setKeypadOpen(usesKeypad)}
/>
);
})}
{keypadOpen && (
// 키패드를 눌러도 입력의 focus가 유지되도록 mousedown 기본 동작을 막는다.
event.preventDefault()}>
{KEYS.map((key) => (
press(key)}>
{key}
))}
지우기
)}
);
}
```