# BestOW UI — 전체 문서 (LLM 용) > 한국 서비스를 만들 때 바로 쓰는, 접근성이 검증된 React 디자인 시스템. npm `@bestow-ui/react`. > 저장소 main 의 소스(@bestow-ui/react 2.1.0 기준)에서 자동으로 만든 파일이에요 — npm 최신 버전보다 앞선 내용이 있을 수 있어요. 사용 규칙 요약: - 앱 진입점에서 `import '@bestow-ui/react/styles.css'` 를 한 번, 최상위를 `` 로 감싼다(Toast 를 쓰면 `` 도). - Next.js App Router: Provider 는 'use client' 파일로. Card·Typography·Stack 같은 표시용 컴포넌트와 서식 함수는 서버 컴포넌트에서도 쓴다. - 폼 컴포넌트는 `label`·`helperText`·`error`·`required` 로 접근성 연결이 자동이다. 값을 고르는 컴포넌트는 `value`/`onValueChange`. - 한국 서비스 입력(전화번호·사업자번호·금액 등)은 숫자만 든 문자열·number 를 값으로 주고받는다(하이픈·쉼표는 화면 표시용). - 색·간격은 하드코딩하지 말고 테마(`createTheme`·`createIntentColors`·`vars`)로. ## README — @bestow-ui/react **한국 서비스를 만들 때 바로 쓰는, 접근성이 검증된 React 디자인 시스템이에요.** [vanilla-extract](https://vanilla-extract.style) 와 [Radix UI](https://www.radix-ui.com) 위에 만들었어요. > A React design system for Korean services, with accessibility verified in CI. Docs are in Korean. - **한국 서비스 키트** — 약관 동의(전체 동의), 인증번호(문자 자동 채우기·남은 시간), 전화번호·사업자등록번호(검증)· 금액("12만 5,000원" 읽기)·주소(카카오 우편번호)·날짜(일요일 시작·공휴일)·기간(빠른 선택) 입력. - **검사로 증명하는 접근성** — 모든 스토리를 실제 브라우저(Chromium·WebKit·Firefox)에서 라이트·다크 두 테마로 axe 검사해요. 고대비 모드·320px 리플로·기본 글꼴 2배도 CI 로 확인하고, [KWCAG 2.2 항목별 대응 근거](https://dev-bestow.github.io/BestOW-UI/?path=/docs/accessibility-kwcag-2-2-%EB%8C%80%EC%9D%91--docs)를 문서로 정리했어요. 주요 컴포넌트는 스크린리더(VoiceOver·NVDA)로 직접 들어 보고 [점검 기록](https://github.com/Dev-BestOW/BestOW-UI/blob/main/docs/SCREEN-READER-CHECKLIST.md)을 남겼어요. - **대표색 하나로 테마** — 브랜드 색을 넣어도 글자 대비 4.5:1(hover 포함)과 포커스 링 3:1 을 라이트·다크 모두 지켜요. [테마 빌더](https://dev-bestow.github.io/BestOW-UI/?path=/story/foundations-theme-builder--builder)에서 바로 해 볼 수 있어요. - **가볍고 서버 컴포넌트 친화적** — 런타임 CSS 0, Next.js App Router 지원, 쓰는 컴포넌트만 번들에 들어가요 (Button 하나 약 1.7 kB, NativeSelect 로 만든 입력 폼 7종 약 4 kB, 모든 컴포넌트 CSS 를 합쳐도 약 11 kB — minify + brotli). - **기본 문구는 한국어** — 닫기·결과 없음·달력 등 기본 문구가 한국어이고, `LocaleProvider` 로 한 번에 바꿀 수 있어요(영어 묶음 포함). ⚡ **[StackBlitz 에서 바로 써 보기](https://stackblitz.com/github/Dev-BestOW/BestOW-UI/tree/main/examples/vite-starter?file=src%2FApp.tsx)** (설치 없이 브라우저에서) 📖 **문서와 실제 예제:** [Storybook](https://dev-bestow.github.io/BestOW-UI/) · [Next.js 예제 앱](https://dev-bestow.github.io/BestOW-UI/playground/) ### 설치 ```bash npm install @bestow-ui/react ``` React 18 이상이 필요해요. [`@bestow-ui/icons`](https://www.npmjs.com/package/@bestow-ui/icons) 와 [`@bestow-ui/tokens`](https://www.npmjs.com/package/@bestow-ui/tokens) 는 함께 설치돼요. ### 시작하기 ```tsx import '@bestow-ui/react/styles.css'; // 앱 진입점에서 한 번 import { ThemeProvider, ToastProvider, Button, useToast } from '@bestow-ui/react'; export function App() { return ( ); } function SaveButton() { const { toast } = useToast(); return ; } ``` Next.js App Router 에서는 Provider 를 `'use client'` 파일로 분리해 `app/layout.tsx` 에서 감싸면 돼요. 컴포넌트는 서버 컴포넌트에서도 바로 쓸 수 있어요. 상태가 없는 표시용 컴포넌트(`Typography`, `Stack`, `Grid`, `Container`, `Card`, `Badge`, `Divider`, `EmptyState`, `Progress`, `Skeleton`, `DescriptionList`, `VisuallyHidden`)는 서버 컴포넌트로 렌더링되고(클라이언트 JS 없음), `createTheme`·`createIntentColors`·`vars`·`formatKoreanAmount` 같은 함수·값도 서버에서 그대로 불러요. 자세한 설정은 [Storybook 시작하기](https://dev-bestow.github.io/BestOW-UI/)에 있어요. `styles.css` 의 스타일은 CSS 레이어 `bestow` 안에 있어요. 레이어 밖에 쓴 CSS(`className` 등)가 선택자 우선순위와 관계없이 이기고, 리셋 CSS 는 `@layer base { … }` 에 넣어요. Tailwind 와 함께 쓰는 법은 Storybook **Guides › Tailwind 와 함께 쓰기**. 0.x 에서 올리시나요? [마이그레이션 가이드](https://github.com/Dev-BestOW/BestOW-UI/blob/main/docs/MIGRATION-1.0.md)를 보세요. ### 다크 모드 ```tsx const [mode, setMode] = useState('system'); // 'light' | 'dark' | 'system' …; ``` Modal·Tooltip·Select·Toast 처럼 portal 로 띄우는 요소도 테마를 자동으로 따라가요. `'system'` 은 OS 설정을 따르고, 서버 렌더링에서도 CSS 미디어쿼리로 그려 첫 화면이 깜빡이지 않아요. 사용자가 고른 모드를 쿠키로 저장해 서버에서 읽는 방법은 [Storybook 시작하기 › 다크 모드](https://dev-bestow.github.io/BestOW-UI/)에 있어요. ### 브랜드 테마 ```tsx import { ThemeProvider, createTheme, createIntentColors } from '@bestow-ui/react'; const brand = createTheme( createIntentColors('primary', '#7c3aed'), // 라이트·다크 색 세트, 글자 대비 4.5:1 보장 { common: { radius: { md: '6px' } } }, ); …; ``` 입력 칸·체크 컨트롤(Checkbox·Radio·Switch) 테두리는 기본으로 배경과 3:1 이상이에요(2.0). 색만 바꾸려면 `color.border.input`·`inputHover`·`control` 토큰을 지정해요. 모든 토큰은 이름이 고정된 CSS 변수라 CSS 로도 덮어쓸 수 있어요. ```css :root { --bestow-color-primary-solid: #7c3aed; } ``` ### 문구 바꾸기 기본 문구(닫기 버튼, "결과가 없어요", 달력 이름 등)는 한국어예요. `LocaleProvider` 로 한 번에 바꿔요. ```tsx import { LocaleProvider, en } from '@bestow-ui/react'; … ``` ### 폼 라이브러리 react-hook-form 과 함께 쓸 수 있어요. 입력창은 `register` 를 그대로 펼치고, 값을 고르는 컴포넌트는 `Controller` 로 연결해요. 제출에 실패하면 `ErrorSummary` 가 포커스를 받아 고칠 곳을 알려요. 자세한 방법은 [화면 패턴](https://dev-bestow.github.io/BestOW-UI/?path=/docs/guides-%ED%99%94%EB%A9%B4-%ED%8C%A8%ED%84%B4--docs) 가이드에 있어요. ### 컴포넌트 | 분류 | 컴포넌트 | |---|---| | 레이아웃 | `Stack`(구간별 값 `{ base, md }`), `Grid`, `Container`, `Divider`, `Card`, `VisuallyHidden` — 구간 `media`(sm 640 · md 768 · lg 1024 · xl 1280px) | | 글자 | `Typography` | | 동작 | `Button`, `IconButton`, `Link` | | 폼 | `Input`, `TextField`, `PasswordInput`(보기 버튼), `SearchInput`(지우기·Enter 검색), `FormField`(직접 만든 입력에 레이블·오류 연결), `Textarea`, `Select`, `NativeSelect`, `Combobox`, `MultiCombobox`(여러 개, 칸 안 칩), `NumberInput`, `FileUpload`, `Checkbox`, `CheckboxGroup`, `RadioGroup`, `Switch`, `Slider` | | 피드백 | `Alert`, `ErrorSummary`, `Progress`, `ToastProvider` + `useToast`, `Spinner`, `Skeleton`, `Badge`, `Tag` + `TagGroup`(고른 필터·필터 칩), `EmptyState` | | 오버레이 | `Modal`, `Drawer`, `Popover`, `Tooltip`, `DropdownMenu` | | 탐색 | `Tabs`, `SegmentedControl`, `Accordion`, `Pagination`, `Breadcrumb`, `SkipLink`, `Stepper` | | 표시 | `Avatar`, `Table`(+ `TableEmpty`, `TableSkeleton`, `useTableSelection`), `DescriptionList` | | 한국 서비스 키트 | `AgreementGroup`(약관 동의), `VerificationCodeInput`(인증번호), `PhoneNumberInput`, `BusinessNumberInput`, `CorporateNumberInput`(법인등록번호), `CardNumberInput`, `AccountNumberInput`(+ 은행 목록 `KOREAN_BANKS`), `ResidentNumberInput`(주민·외국인등록번호, 뒷자리 가림), `AmountInput`, `AddressInput`, `DatePicker`, `DateRangePicker`(+ `dateRangePresets`), `Calendar` (+ `formatPhoneNumber`, `formatBusinessNumber`, `isValidBusinessNumber`, `formatKoreanAmount`, `formatKoreanDate`, 주민번호 `formatResidentNumber`·`isValidResidentNumber`·`parseResidentNumber`, 마스킹 `maskName`·`maskPhoneNumber`·`maskResidentNumber`·`maskCardNumber`·`maskDigits`, `parseISODate`, `toISODate` — 서식 함수는 React 없이도 쓰는 [`@bestow-ui/korean-utils`](https://www.npmjs.com/package/@bestow-ui/korean-utils) 에서 다시 내보내요) | | 문구 | `LocaleProvider`, `en`(영어 문구 묶음) | | 테마 | `ThemeProvider`, `createTheme`, `createIntentColors`, `vars`, `useTheme`, `usePortalProps` | | 도움 함수 | `focusField` | 어떤 컴포넌트를 골라야 할지는 [언제 무엇을 쓰나](https://dev-bestow.github.io/BestOW-UI/?path=/docs/guides-%EC%96%B8%EC%A0%9C-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%93%B0%EB%82%98--docs) 가이드를 보세요. ### 지원 범위 Chrome·Edge·Firefox·Safari 최근 2개 메이저 버전(모바일 포함), React 18·19, Next.js App Router 를 CI 로 확인해요. **실서비스(프로덕션)에 써도 돼요** — 세 브라우저 엔진·모바일 E2E, React 18·19 CI, 스크린리더 수동 점검(VoiceOver·NVDA), 실제 화면 파일럿, 지원·보안 정책까지 갖췄어요. 버전 정책(semver·deprecated 절차)과 배포 주기는 [SUPPORT.md](https://github.com/Dev-BestOW/BestOW-UI/blob/main/SUPPORT.md), 보안 문제는 공개 이슈 대신 [SECURITY.md](https://github.com/Dev-BestOW/BestOW-UI/blob/main/SECURITY.md) 의 방법으로 알려 주세요. ### 링크 - [Storybook](https://dev-bestow.github.io/BestOW-UI/) — 문서, 디자인 토큰, 모든 컴포넌트 - [llms.txt](https://dev-bestow.github.io/BestOW-UI/llms.txt) · [llms-full.txt](https://dev-bestow.github.io/BestOW-UI/llms-full.txt) — Cursor·Claude 같은 AI 코딩 도구에 넣는 문서(모든 컴포넌트의 props 정의 포함) - [GitHub](https://github.com/Dev-BestOW/BestOW-UI) — 소스, [이슈](https://github.com/Dev-BestOW/BestOW-UI/issues), [기여 방법](https://github.com/Dev-BestOW/BestOW-UI/blob/main/CONTRIBUTING.md) - [변경 내역](https://github.com/Dev-BestOW/BestOW-UI/blob/main/packages/react/CHANGELOG.md) · [0.x 에서 올리기 (MIGRATION-1.0)](https://github.com/Dev-BestOW/BestOW-UI/blob/main/docs/MIGRATION-1.0.md) ### 라이선스 [MIT](https://github.com/Dev-BestOW/BestOW-UI/blob/main/LICENSE) © Dev-BestOW ## README — @bestow-ui/korean-utils > Formatting, validation and masking helpers for Korean services — phone numbers, business, corporate and resident registration numbers, card and bank account numbers, KRW amounts in Korean, dates. Zero dependencies. Docs are in Korean. **한국 서비스에 늘 필요한 서식·검증 함수를 모았어요.** 전화번호 하이픈, 사업자·법인등록번호 검증 공식, 주민등록번호 생년월일·성별 읽기, 카드번호(Luhn)·계좌번호와 은행 목록(금융결제원 코드), "12만 5,000원" 금액 읽기, 한국어 날짜, 카카오 우편번호 참고항목, 화면에 보여 줄 때의 마스킹(`홍*동`, `010-****-5678`)까지. - **의존성 0, 프레임워크 무관** — React·Vue·Svelte·Node 서버 어디서나 써요. 전체 약 2 kB(은행 목록 포함), 함수 하나만 쓰면 그만큼만 번들에 들어가요(`maskName` 하나 99 B, 카드번호 서식·검증 260 B, minify + brotli). - **입력 중인 값도 자연스럽게** — `formatPhoneNumber('0101234')` → `'010-123-4'` 처럼 덜 입력한 값도 나눠서, 입력창의 onChange 에 그대로 쓸 수 있어요. - **하이픈이 섞여도 됨** — 숫자가 아닌 글자는 무시해요. - [BestOW UI](https://github.com/Dev-BestOW/BestOW-UI) 디자인 시스템의 `PhoneNumberInput`·`BusinessNumberInput`·`CardNumberInput`·`AmountInput` 등이 쓰는 함수 그대로예요. ### 설치 ```bash npm install @bestow-ui/korean-utils ``` ### 사용하기 ```ts import { formatPhoneNumber, formatBusinessNumber, isValidBusinessNumber, formatKoreanAmount, formatKoreanDate, } from '@bestow-ui/korean-utils'; formatPhoneNumber('01012345678'); // '010-1234-5678' formatPhoneNumber('021234567'); // '02-123-4567' formatPhoneNumber('15881234'); // '1588-1234' formatBusinessNumber('1248100998'); // '124-81-00998' isValidBusinessNumber('124-81-00998'); // true (검증 공식) formatKoreanAmount(125000); // '12만 5,000원' formatKoreanAmount(150000000); // '1억 5,000만원' formatKoreanAmount(20000, '포인트'); // '2만포인트' formatKoreanDate(new Date(2026, 9, 15)); // '2026년 10월 15일 목요일' formatKoreanDate(new Date(2026, 9, 15), false); // '2026년 10월 15일' formatResidentNumber('9001011234567'); // '900101-1234567' isValidResidentNumber('900101-1234567'); // true (생년월일·뒷자리 첫 숫자) parseResidentNumber('900101-1'); // { birthDate: '1990-01-01', sex: 'male', foreigner: false } maskName('홍길동'); // '홍*동' maskPhoneNumber('01012345678'); // '010-****-5678' maskResidentNumber('9001011234567'); // '900101-1******' maskCardNumber('1234-5678-9012-3456'); // '1234-****-****-3456' maskDigits('110-123-456789', { end: 4 }); // '***-***-**6789' (계좌번호처럼 형식이 제각각인 번호) ``` ### 함수 | 함수 | 하는 일 | |---|---| | `formatPhoneNumber(input)` | 하이픈 넣기. 서울(02)·대표번호(15xx·16xx·18xx)·휴대폰·지역번호·070 을 구분하고, 최대 자릿수를 넘으면 자른다 | | `phoneNumberDigits(input)` | 국내 번호 숫자로 바꿈(`+82` → `0`, 숫자만 남기고 최대 자릿수로 자름) | | `phoneNumberMaxDigits(digits)` | 그 번호의 최대 자릿수(02 → 10, 대표번호 → 8, 050 → 12, 나머지 → 11). 입력창 `maxLength` 계산에 | | `formatBusinessNumber(input)` | 사업자등록번호 3-2-5 하이픈 | | `isValidBusinessNumber(input)` | 사업자등록번호 검증 공식(가중치 1,3,7,1,3,7,1,3,5) 확인. **실제로 등록된 번호인지는 국세청 조회가 필요해요** | | `formatCorporateNumber(input)` | 법인등록번호 6-7 하이픈 | | `isValidCorporateNumber(input)` | 법인등록번호 검증 공식(앞 12자리에 가중치 1,2 를 번갈아 곱한 합) 확인. **실제로 등기된 법인인지는 등기소 조회가 필요해요** | | `formatCardNumber(input)` | 카드번호 4-4-4-4 하이픈(아메리칸 익스프레스 15자리는 4-6-5) | | `isValidCardNumber(input)` | 자릿수(15·16)와 검사 공식(Luhn)으로 오타 확인. **쓸 수 있는 카드인지는 결제사가 확인해요** | | `cardNumberLength(input)` | 그 카드번호의 자릿수(34·37 로 시작하면 15, 나머지 16). 입력창 최대 자릿수에 | | `KOREAN_BANKS` | 국내 은행 26곳 `{ code, name }` — 금융결제원 표준 코드 3자리(예: `'090'` 카카오뱅크). 많이 쓰는 은행부터. 합병·이름 변경이 있으면 갱신해요(2026-11 기준) | | `accountNumberDigits(input)` | 계좌번호를 숫자로(하이픈·공백 제거, 최대 16자리). 은행·상품마다 하이픈 위치가 달라 서식은 넣지 않아요 | | `isValidAccountNumber(input)` | 계좌번호 자릿수(10~16)만 확인. **은행마다 검증 공식이 공개돼 있지 않아요** — 실제 계좌·예금주는 예금주 조회 API 로 | | `formatKoreanAmount(amount, unit = '원')` | 만·억·조 단위로 읽기 쉽게. 음수는 `-` 를 붙인다 | | `formatKoreanDate(date, withWeekday = true)` | `2026년 10월 15일 목요일`. 스크린리더용 날짜 이름에도 좋아요 | | `KOREAN_WEEKDAYS` | `['일', '월', '화', '수', '목', '금', '토']` | | `formatResidentNumber(input)` | 주민·외국인등록번호 6-7 하이픈 | | `parseResidentNumber(input)` | 앞 7자리로 생년월일(`YYYY-MM-DD`)·성별·외국인 여부. 없는 날짜면 `null`. 앞 7자리만 받는 화면에도 | | `isValidResidentNumber(input)` | 13자리 + 생년월일·뒷자리 첫 숫자 확인. **마지막 검증 숫자는 보지 않아요** — 2020년 10월 이후 번호는 뒤 6자리가 임의 번호라 예전 공식으로는 올바른 번호도 틀렸다고 나와요. 실제 번호인지는 본인확인 기관 조회가 필요해요 | | `maskName(name)` | 가운데 글자 가리기(`홍*동`, `남**수`, 두 글자는 `이*`, 한 글자는 `*`) | | `maskPhoneNumber(input)` | 가운데 자리 가리기(`010-****-5678`). 대표번호는 그대로, 형식을 모르는 번호는 모두 가림 | | `maskResidentNumber(input)` | 뒤 6자리 가리기(`900101-1******`) | | `maskCardNumber(input)` | 앞 4·뒤 4자리만 남기기. 13자리 미만이면 모두 가림 | | `maskDigits(input, { start, end })` | 숫자 중 앞 `start`·뒤 `end` 개만 남기고 가리기(구분 글자는 그대로). 남길 자리가 전체를 덮으면 모두 가림 | | `composeExtraAddress(data)` | [카카오(다음) 우편번호 서비스](https://postcode.map.kakao.com/guide) 결과로 도로명 주소 참고항목(법정동, 공동주택 건물명)을 만든다 — 예: `' (역삼동, 래미안)'` | #### 입력창에서 쓰기 ```tsx const [phone, setPhone] = useState(''); setPhone(formatPhoneNumber(e.target.value).replace(/\D/g, ''))} />; ``` 숫자만 저장하고 보여 줄 때 서식을 입혀요. 이렇게 하면 중간에 글자를 지울 때 커서가 맨 끝으로 튀는데, BestOW UI 의 [`PhoneNumberInput`](https://dev-bestow.github.io/BestOW-UI/?path=/docs/korean-kit-phonenumberinput--docs) 은 커서 위치와 하이픈 뒤 Backspace 까지 처리해 두었어요. > 마스킹은 **화면 표시용**이에요. 저장·전송·로그에는 가린 값이 아니라 꼭 필요한 값만 남기세요. > 주민등록번호는 법령에 근거가 있을 때만 수집할 수 있어요(개인정보 보호법 제24조의2). ### React 컴포넌트가 필요하면 [`@bestow-ui/react`](https://www.npmjs.com/package/@bestow-ui/react) 에 이 함수들을 쓰는 입력 컴포넌트가 있어요 — 전화번호·사업자등록번호·주민등록번호·금액·주소·인증번호·약관 동의·날짜. 접근성(KWCAG 2.2)을 CI 로 검사하고, 대표색 하나로 브랜드 테마를 만들 수 있어요. → [Storybook › Korean Kit](https://dev-bestow.github.io/BestOW-UI/?path=/docs/korean-kit-agreementgroup--docs) `@bestow-ui/react` 1.5 까지 쓰던 `import { formatPhoneNumber } from '@bestow-ui/react'` 도 그대로 동작해요(같은 함수를 다시 내보내요). ### 라이선스 [MIT](https://github.com/Dev-BestOW/BestOW-UI/blob/main/LICENSE) © Dev-BestOW ## 언제 무엇을 쓰나 ### 하나 고르기 선택지가 적고 한눈에 보여야 하면 RadioGroup·SegmentedControl, 많으면 Select(가볍게는 NativeSelect), 아주 많아 찾아야 하면 Combobox. - RadioGroup: 폼 안에서 2~5개 중 하나. 선택지마다 설명이 필요할 때. 피할 때: 고르자마자 화면이 바뀌는 보기 전환 (→ SegmentedControl). - SegmentedControl: 2~5개 짧은 이름 중 하나를 고르면 바로 반영(기간 일/주/월, 목록·격자 보기). 피할 때: 아래 내용(패널)이 통째로 바뀜 (→ Tabs). 폼 제출 값 (→ RadioGroup). - Select: 6개 이상이거나 자리가 좁을 때. 목록이 길지 않아 훑어보면 찾을 수 있을 때. 피할 때: 20개 넘게 길어 찾기 어려움 (→ Combobox). - NativeSelect: Select 와 같은 경우인데 번들을 줄이고 싶거나, 모바일 사용자가 많아 운영체제 선택 창이 나을 때. react-hook-form register 를 그대로. 피할 때: 목록 항목에 아이콘·설명을 넣거나 모양을 꾸며야 함 (→ Select). - Combobox: 선택지가 많아 글자를 입력해 걸러야 할 때(도시·은행·상품). 피할 때: 선택지가 몇 개뿐 (→ Select·RadioGroup). - DropdownMenu (라디오 항목): 정렬·간격처럼 화면 보기 설정을 메뉴 안에서. 피할 때: 폼에 제출하는 값 (→ Select·RadioGroup). ### 여러 개 고르기 · 켜고 끄기 폼에서 여러 개면 CheckboxGroup(하나뿐이면 Checkbox), 선택지가 많아 찾아야 하면 MultiCombobox, 약관은 AgreementGroup, 표의 행은 useTableSelection. 바로 적용되는 설정은 Switch. - CheckboxGroup: 폼에서 여러 선택지 중 여러 개(관심 분야·받을 알림). 묶음 레이블·오류·"하나 이상" 필수 표시. 피할 때: 선택지가 많아(10개 넘게) 한눈에 안 들어옴 (→ MultiCombobox). - MultiCombobox: 많은 선택지에서 글자를 입력해 찾아 여러 개(업무 지역·담당자·태그). 고른 값은 칸 안의 칩, 최대 개수(maxSelected). 피할 때: 선택지가 몇 개뿐이라 모두 보여 줄 수 있음 (→ CheckboxGroup). - Checkbox: 제출할 때 함께 적용되는 동의·옵션 하나. - AgreementGroup: 전체 동의 + 필수/선택 약관(보기 펼침 포함). - Switch: 누르는 즉시 적용되는 켜기/끄기(알림 받기). 피할 때: "저장" 을 눌러야 적용되는 폼 값 (→ Checkbox). - Table + useTableSelection: 목록의 행을 골라 일괄 작업. ### 글자 입력 한 줄은 TextField, 여러 줄은 Textarea. 비밀번호·검색처럼 버튼이 붙는 칸은 전용 컴포넌트를 쓴다. - TextField: 이름·이메일처럼 한 줄 글자. 레이블·도움말·오류 연결이 들어 있다. - Textarea: 메모·문의처럼 여러 줄(글자 수 표시·자동 높이). - PasswordInput: 로그인·회원가입 비밀번호. 보기 버튼과 비밀번호 관리자 자동 완성. 피할 때: 인증번호(문자로 받는 숫자) (→ VerificationCodeInput). - SearchInput: 목록을 거르는 검색어, 검색 페이지의 검색창. 지우기 버튼·Escape·Enter 검색. 피할 때: 정해진 목록에서 하나를 골라야 함 (→ Combobox). ### 숫자 · 금액 · 날짜 정확한 값은 입력창, 대략적인 값·범위는 Slider. 서식(쉼표·하이픈)은 컴포넌트가 맡는다. - NumberInput: 수량·인원처럼 작은 범위의 정수·소수(−/+, ↑/↓). - AmountInput: 금액. 천 단위 쉼표와 "12만 5,000원" 읽기. - Slider: 대략적인 값이나 범위(음량·가격대)를 눈으로 고를 때. 피할 때: 정확한 값이 중요 (→ NumberInput, 또는 함께 둔다). - DateRangePicker: 조회·예약 기간(시작일~종료일). 오늘·1주일·1개월 같은 빠른 선택. - DatePicker: 날짜 하나. 숫자 입력(YYYY-MM-DD)과 달력 둘 다. 피할 때: 생년월일처럼 먼 날짜 — 숫자 입력이 더 빠르다는 것을 안내. - 시간 (전용 컴포넌트 없음): 정해진 시간대(예약·배송)는 RadioGroup·Select, 아무 시각은 FormField + (min·max·step). 화면 패턴 "시간 고르기" 참고. ### 위에 띄우기 · 알리기 결정이 필요하면 Modal, 옆에서 길게 보면 Drawer, 짧은 추가 정보는 Popover, 결과 알림은 Toast, 페이지에 남겨야 하면 Alert. - Modal: 흐름을 멈추고 확인·짧은 입력이 필요할 때(삭제 확인, 초대). 피할 때: 긴 내용·참고용 상세 (→ Drawer). - Drawer: 목록에서 상세 보기·필터처럼 옆에서 길게 보는 내용. - Popover: 버튼 옆의 짧은 추가 정보·작은 폼(공유 설정). 바깥을 누르면 닫힘. - Tooltip: 아이콘 버튼 이름 같은 짧은 보조 설명. 피할 때: 꼭 알아야 할 정보·링크·버튼 (터치 기기에서 보기 어렵다). - DropdownMenu: 버튼 하나에 모은 동작 목록(편집·복제·삭제). 피할 때: 페이지 이동 목록 (→ 링크 목록). - Toast: 동작 결과를 잠깐 알림(저장했어요). 피할 때: 되돌리기처럼 놓치면 안 되는 동작만 두기 (사라진다). - Alert: 페이지에 남아 있어야 하는 안내·경고. 일반 안내는 color="info"(브랜드 색과 따로 파랑), 브랜드를 드러내는 소개는 primary. - ErrorSummary: 폼 제출에 실패했을 때 고칠 곳 모음(포커스·링크). ### 배치 · 페이지 틀 한 줄(가로·세로)로 늘어놓으면 Stack, 같은 폭 칸을 여러 열로 깔면 Grid, 페이지 본문의 최대 폭·좌우 여백은 Container. - Stack: 버튼 줄·폼 칸처럼 한 방향으로 늘어놓음. 좁은 화면은 세로, 넓으면 가로 — direction={{ base: "column", md: "row" }}. 피할 때: 칸 폭이 같아야 하는 카드 목록 (→ Grid). - Grid: 카드·상품처럼 같은 폭 칸을 여러 열로. 열 수를 구간별로(columns) 또는 칸 최소 폭만(minItemWidth). 피할 때: 행·열 머리글이 있는 데이터 (→ Table). - Container: 페이지 본문을 가운데에 두고 최대 폭(sm~xl)과 좌우 여백(좁은 화면 16px · md 이상 24px)을 맞춤. ### 화면 나누기 · 이동 같은 페이지에서 내용을 바꾸면 Tabs, 세로로 접으면 Accordion, 진행 위치는 Stepper, 다른 페이지로 가면 Link. - Tabs: 선택에 따라 아래 패널이 바뀜. 탭이 많거나 이름이 길면 세로. 피할 때: 값만 고르고 패널이 없음 (→ SegmentedControl). - Accordion: 자주 묻는 질문처럼 여러 섹션을 세로로 펼쳐 봄. - Stepper: 가입·결제처럼 단계가 정해진 흐름의 현재 위치 표시. 피할 때: 단계 간 자유 이동 (→ Tabs). - Pagination: 긴 목록을 페이지로 나눔. - Link · Button: 다른 페이지·주소로 가면 Link, 이 화면에서 동작하면 Button. 페이지 이동이 주요 동작이면 Button asChild 로 링크를 감싼다. ### 기다림 · 상태 표시 얼마나 남았는지 알면 Progress, 모르면 Spinner, 내용 자리를 미리 보여 주면 Skeleton. 상태 라벨은 Badge, 분류·필터는 Tag, 비어 있으면 EmptyState. - Progress: 업로드처럼 진행률을 알 때(모르면 value 없이). - Spinner: 짧은 대기·버튼 안 로딩(Button loading). - Skeleton · TableSkeleton: 불러오는 동안 내용 모양을 미리(aria-busy 와 함께). - Badge: 짧은 상태 라벨(결제 완료·배송 중). - Tag: 분류·키워드, 고른 필터를 보여 주고 지우기(onRemove + TagGroup), 바로 켜고 끄는 필터 칩(selected). 피할 때: 상태 라벨 (→ Badge). 폼에 제출하는 여러 선택 (→ CheckboxGroup). - EmptyState: 보여 줄 것이 없을 때(검색 결과 없음, 아직 만든 것 없음) — 무엇을 하면 되는지와 행동 버튼. 표 안은 TableEmpty 에 넣는다. ## 키보드·스크린리더 동작 - Button — 키보드: Enter·Space 로 실행. / 스크린리더: button. loading 이면 aria-busy·aria-disabled — 포커스는 유지되고(위치를 잃지 않음) 클릭·제출만 막힌다. 스피너는 장식으로 숨김. asChild 로 링크가 되면 역할은 link 이고, disabled 는 aria-disabled + Tab 제외 + 클릭 차단으로 나타낸다. / 사용하는 쪽: 아이콘만 있는 버튼은 IconButton 을 쓴다. 페이지를 이동하면 asChild 로 링크를 감싼다(링크는 되도록 비활성으로 두지 않는다). - IconButton — 키보드: Enter·Space 로 실행. / 스크린리더: button. aria-label 이 타입으로 필수라 이름 없는 버튼을 만들 수 없다. 아이콘은 숨김. / 사용하는 쪽: 이름은 동작으로 ("닫기", "검색"). 같은 화면의 같은 아이콘은 같은 이름. - Link — 키보드: Enter 로 이동. / 스크린리더: link. external 이면 새 창 안내("(새 창)")를 이름에 덧붙이고 rel="noopener noreferrer". as/asChild 로 라우터 링크를 써도 같은 스타일·속성. / 사용하는 쪽: 링크 글자만으로 목적지를 알 수 있게 ("여기" 대신 "환불 정책"). - TextField · Textarea · Input — 키보드: 기본 입력 동작. / 스크린리더: