# 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} ))} 지우기 )} ); } ```