ARC

개요

리바운드 비즈의 고객 페이지(채권자와 채무자가 실제로 쓰는 화면)를 만들 때 필요한 색·글자·컴포넌트 규칙이에요. 여기 적힌 값은 전부 rebound-biz 저장소에서 지금 돌고 있는 값이고, 새 화면은 이 값을 그대로 가져다 쓰면 됩니다.

이 문서를 쓰는 곳

제품저장소적용
리바운드 비즈GroundBasic/rebound-biz기준. 이 문서에 적힌 모든 값의 출처
채무자 페이지rebound-biz /debtor같은 토큰, 같은 컴포넌트
신규 고객 페이지—이 문서대로 시작
소개 페이지GroundBasic/rebound-info마케팅 톤은 별도, 브랜드 색만 공유

네 가지 원칙

원칙뜻
데이터가 주인공화면의 대부분은 무채색이에요. 색은 상태를 가리킬 때만 씁니다. 표 한가득 알록달록한 배지는 읽는 속도를 떨어뜨려요.
채운 색은 하나한 화면에서 #FF5900으로 채운 버튼은 하나. 나머지 행동은 흰 버튼이나 고스트로 내립니다. 선택 상태(고른 카드 · 칩 · 체크)는 파랑, 오렌지는 주 행동에만.
선보다 여백카드는 테두리 없이 흰 배경 + 아주 옅은 그림자. 선은 표의 행 사이와 영역 경계에만.
숫자는 줄을 맞춘다금액·건수·날짜는 고정폭 숫자(tabular-nums), 금액 열은 오른쪽 정렬.

문서의 구성

섹션무엇이 있나
Foundations팔레트 원본(Primitive)과 역할별 토큰(Semantic), 글자 스케일, 여백·모서리·그림자, 아이콘
Components버튼·배지·입력·카드·표·모달·내비. 디자인 탭은 스펙과 규칙, 코드 탭은 실제 클래스 정의
Patterns한 화면을 구성하는 순서, 빈 화면·로딩·오류, 화면에 쓰는 말
Resources로고 파일, Tailwind 설정과 CSS 변수

설치와 사용

토큰 파일 두 개와 폰트 한 줄이면 준비가 끝나요. 새 저장소든, 이미 있는 페이지든 같습니다.

1. 설치

npm i -D tailwindcss postcss autoprefixer npx tailwindcss init -p

2. 토큰 가져오기

rebound-biz에서 두 파일을 그대로 복사합니다. 값은 Resources · 토큰에도 전문이 있어요.

파일가져올 부분
tailwind.config.jstheme.extend의 colors · fontFamily · boxShadow · borderRadius
src/index.css@layer base(바탕·스크롤바·모바일 입력) + @layer components(버튼·입력·표·배지)

3. 폰트

@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css'); body { font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; }

4. 화면 만들기 전 확인

확인기준
색을 골랐나요hex가 아니라 Semantic 토큰의 역할로. 팔레트에 없는 색은 만들지 않아요
주 행동은 하나인가요채운 오렌지 버튼은 화면에 하나
숫자 열은 오른쪽인가요금액·비율은 오른쪽 정렬 + 고정폭 숫자
빈 화면이 있나요데이터가 없을 때 무엇을 보여줄지 먼저 정하기
모바일에서 열리나요640px 아래에서 입력 글자 16px, 표는 가로 스크롤
색은 이 문서에서만 늘려요. 컴포넌트 안에 새 hex를 하드코딩하지 말고, 필요한 색이 없으면 여기에 추가한 뒤 쓰세요. 지금도 P3 디스플레이 렌더링 차이를 맞추려고 hex를 다시 덮어쓰는 규칙이 index.css 아래쪽에 있어요.

Foundations

모든 화면의 바탕이 되는 가장 작은 단위들이에요. 색·글자·여백·아이콘처럼 컴포넌트를 만들기 전에 먼저 맞춰야 하는 기준을 모았습니다.

Base Material

Colors

색은 역할로 고릅니다. 화면을 만들 때는 hex가 아니라 Semantic 이름을 쓰고, 그 값의 출처인 팔레트는 Atomic에 있어요. 같은 오렌지라도 주 버튼에 쓸 때와 상태 표시에 쓸 때 이름이 다릅니다.

Typography

리바운드의 정보 위계와 읽기 흐름을 일관되게 만드는 타입 시스템입니다. Pretendard 한 벌, 굵기는 Bold와 Regular, 자간은 전부 -2.5%. 크기는 데스크톱·태블릿·모바일 세 단계로 줄어듭니다.

Font

한글과 라틴, 숫자를 한 서체로 맞출 수 있어서 Pretendard 하나만 씁니다.

Pretendard
리바운드의 모든 경험을 하나의 흐름으로. Rebound 0123456789
Regular 400Bold 700

Type scale

다섯 계열을 실제 크기로. 각 계열의 L을 데스크톱 크기로 보여주고, 아래 한 줄이 그 스타일의 값입니다.

Specification

크기는 데스크톱 / 태블릿 / 모바일 순서. 이름은 계열과 크기만으로 부릅니다.

Rules

상황규칙
숫자금액·건수·날짜는 tabular-nums. 표에서 자릿수가 흔들리지 않게.
금액12,340,000원처럼 세 자리 쉼표, 단위는 붙여서. 축약(1.2천만)은 쓰지 않아요.
숫자 글꼴금액 · KPI · 표의 숫자는 Inter 500 tabular(.num), 강조는 700. 문장 안의 숫자는 Pretendard 그대로. 단위(원 · %)는 Pretendard로 뒤에 작게(12~14 Label/Alternative) 붙여요.
날짜2026.09.15, 기간은 2026.09.15 ~ 10.14, 남은 기간은 D-7.
줄바꿈한 줄로 끝나야 하는 셀은 truncate, 문장은 break-keep으로 단어를 자르지 않게.
모바일 입력iOS Safari는 16px 미만 입력에 포커스하면 화면을 확대해요. 640px 아래에서는 입력 글자를 16px로 강제합니다.

Layout

화면 규격과 구간, 여백 단위, 컬럼 그리드. 리바운드의 모든 화면은 이 네 가지 위에 놓입니다.

Artboard size

해상도별로 모든 화면을 그리지 않고 아래 대표 규격만 설계합니다. 그 사이 크기는 그리드가 알아서 대응해요.

환경너비높이최대 너비
Web desktop1440px960px1280px
Web tablet768px1024px—
Web mobile390px844px—

Breakpoint

네 구간. 데스크톱부터는 본문 폭을 1280px에서 멈추고 가운데에 놓습니다.

명칭대응 환경너비최대 너비
Mobile모바일360 ~ 639px—
Tablet태블릿640 ~ 1023px—
Desktop데스크톱1024 ~ 1439px1280px (패딩 포함)
Wide데스크톱 대형1440px ~1280px (패딩 포함)

Spacing

1 unit = 4px. 여백은 4의 배수로 잡고, 시각 보정이 필요할 때만 2px 단위로 움직입니다.

Grid

화면 너비에 맞게 늘어나는 컬럼 그리드. 컬럼은 비율이고 거터와 마진만 고정값입니다. 컬럼은 자유롭게 묶어서 쓸 수 있어요.

Mobile

4단 컬럼, 거터 16px, 마진 16px.

Tablet

8단 컬럼, 거터 24px, 마진 24px.

Desktop

12단 컬럼, 거터 24px, 마진 24px. 고객 페이지에서는 사이드바 256px 오른쪽 본문이 이 그리드 위에 놓입니다.

Radius

크기에 비례합니다. 작은 것은 6, 조작 요소는 8, 카드와 모달은 12, 동그라미는 999.

6rounded-md툴팁, 작은 칩
8rounded-lg버튼, 입력, 내비 항목
12rounded-xl · 2xl카드, 리스트 박스, 모달
999rounded-full카운트 배지, 아바타, 스텝

Elevation

그림자로 표면 사이의 거리를 표현합니다. 리바운드는 그림자를 거의 보이지 않을 만큼 옅게 쓰고, 떠 있는 정도만 네 단계로 나눕니다.

Level

바닥에 붙은 것부터 화면 위에 떠 있는 모달까지. 단계가 올라갈수록 그림자가 아래로 길어지고 조금 더 번집니다.

None바닥에 붙은 요소. 표, 구분선, 입력칸
Card카드, KPI 카드
Medium드롭다운, 팝오버, 툴팁
Modal모달, 다이얼로그

Layer

컴포넌트가 놓이는 층. 위 레벨은 아래 레벨을 항상 덮고, 같은 레벨끼리는 겹치지 않게 배치합니다.

Level 3
모달다이얼로그토스트
Level 2
드롭다운팝오버툴팁날짜 선택
Level 1
카드KPI 카드안내 배너사이드바
Level 0
표리스트입력칸탭배지
레벨그림자z-index컴포넌트
Level 0None0표, 리스트, 입력칸, 탭, 배지. 바닥에 붙어 있는 것
Level 1Card10카드, KPI 카드, 안내 배너, 사이드바
Level 2Medium20드롭다운, 팝오버, 툴팁, 날짜 선택. 열려 있는 동안만 떠 있는 것
Level 3Modal40 · 토스트 50모달, 다이얼로그, 토스트. 화면 전체 위에 놓이는 것

Style

명칭토큰값
Cardshadow-card0 -2px 8px -2px rgba(16,24,40,.02)
0 6px 20px -6px rgba(16,24,40,.04)
0 2px 8px -2px rgba(16,24,40,.02)
Mediumshadow-md0 4px 8px -2px rgba(16,24,40,.08)
0 2px 4px -2px rgba(16,24,40,.06)
Modalshadow-xl0 20px 25px -5px rgba(16,24,40,.1)
0 8px 10px -6px rgba(16,24,40,.1)

Focus

키보드로 이동할 때만 3px 오렌지 링을 씁니다. 입력칸은 포커스에서 그림자 대신 테두리 색만 Information 파랑으로 바뀌어요.

Focus ringshadow-focus · 0 0 0 3px rgba(255,89,0,.25)
Input focusborder-color: primary. 그림자 없음

Iconography

아이콘은 lucide 한 벌만 씁니다. 선 굵기 2, 끝은 둥글게, 기본 크기 16. 아래는 고객 페이지에서 실제로 쓰고 있는 아이콘이에요.

이름으로 찾고, 아이콘을 누르면 컴포넌트 코드가 복사됩니다.

Size

네 크기만 씁니다. 표 안과 12~13px 글자 옆은 14, 버튼·내비·리스트는 16, 모달 닫기와 섹션 헤더는 18, 빈 화면과 강조는 20.

Color

기본은 Label Assistive, 마우스를 올리면 Label Title. 내비에서는 회색이었다가 활성 시 Primary. 아이콘만 있는 버튼은 28×28 둥근 박스 안에 가운데로.

기본#8E8E8E
호버#1D1D1D
활성 내비#FF5900
아이콘 버튼28×28 · rounded-lg

Components

화면을 만드는 조각들이에요. 스물여덟 개가 같은 문법을 씁니다. 회색 바탕에 흰 면, 오렌지는 행동과 진행에만, 파랑은 입력과 선택에만. 각 페이지는 미리보기 · Anatomy · Variants · States · Behavior · Usage · Specification · 코드 순서로 읽어요.

코드베이스에 ARC 컴포넌트가 없으면 브라우저 기본 요소(<select> · <input type=date> · window.confirm · 기본 <dialog>)로 때우지 않고, 여기 스펙(치수 · 색 · 동작)대로 먼저 만든 뒤 그걸 써요.

Actions

Data Display

Inputs

Feedback

Loading

Navigation

Button

행동을 실행하는 컴포넌트. 기본은 회색 Neutral이고, 오렌지 Primary는 화면에서 가장 중요한 한 곳에만 포인트로 씁니다. 흰 카드 안에서 항목을 더하거나 파일을 받는 보조 동작은 Line(흰 면 + 1px 선), 배경 없는 Ghost, 위험한 일에만 Danger.

Anatomy

컨테이너, 앞 아이콘(선택), 라벨. 아이콘은 라벨 앞에만 오고 간격은 8.

  1. 1컨테이너 · 높이 36 · 좌우 16 · radius 8 · Fill/Normal
  2. 2앞 아이콘 · 16 · 라벨 색과 같음 · 라벨과 8
  3. 3라벨 · Label M 13 · 600 (Primary 700) · Label/Title

Variants

여섯이면 충분합니다. 고민되면 Neutral, 화면의 방향을 정하는 한 곳만 Primary. Line은 흰 카드 안의 보조 동작 전용, Link는 도움말 옆 작은 행동 전용.

Neutral기본. 특별한 일이 아니면 전부 이것. Fill/Normal 채움, Label/Title 글자, 테두리 없음
Primary화면에서 가장 중요한 행동 하나에만. 오렌지가 포인트로 남아야 눈에 띕니다
Line흰 면 + Line/Normal 1px, Label/Title 600, 호버 Background/Alternative. 흰 카드 안에서 목록에 항목을 더하거나 파일을 받는 보조 동작(채권 추가 · 파일 추가 · 템플릿 내려받기). 회색 페이지 위엔 Neutral. 앞 아이콘 15 허용
Ghost취소, 닫기, 표 안의 작은 행동. 배경 없이 글자만
Link도움말 줄 옆에 붙는 아주 작은 행동(계좌 다시 입력하기). 배경·높이 없이 글자 13 Label/Body, 호버하면 Label/Title + 밑줄. 회색 채움이 생기면 도움말 옆에서 버튼처럼 튀어요
Danger삭제, 해지, 되돌릴 수 없는 일. 확인 모달 안에서만

Sizes

S는 표 안과 카드 머리, M이 기본, L은 온보딩·빈 화면의 단 하나의 CTA. 폭은 자유지만 높이는 고정.

S높이 32 · Label S · 좌우 12 · radius 8
M높이 36 · Label M · 좌우 16 · radius 8
L높이 48 · Label L · 좌우 24 · radius 12

Layout

글자만, 앞 아이콘 + 글자, 아이콘만. 아이콘은 뜻을 보태는 경우에만 쓰고 L 크기의 CTA에는 넣지 않습니다.

Text기본
Icon + Text아이콘 16, 간격 8
Icon only28×28 · radius 8 · aria-label 필수

States

색을 새로 정의하지 않습니다. 호버는 투명도, 눌림은 크기, 비활성은 투명도로만 표현해요.

Default
Hoveropacity .9
Pressedscale .98 · 200ms
Disabledopacity .4
Loading스피너는 아이콘 자리, 라벨은 진행형
Default
HoverFill/Strong
Pressed
Disabled
Default배경 없음
HoverFill/Normal 배경이 나타남
Pressed
Disabled

Hierarchy

Primary 하나가 화면의 방향을 정하고, 나머지는 Neutral과 Ghost로 물러납니다.

이렇게주 행동은 오른쪽 끝에 하나, 왼쪽으로 갈수록 약하게
이건 피해요Primary가 둘이면 어느 쪽이 주 행동인지 모릅니다
이건 피해요검정 버튼은 쓰지 않아요. 중요한 것은 오렌지, 나머지는 회색 Neutral
이건 피해요L 크기 CTA 안에는 아이콘을 넣지 않아요

Placement

데스크톱은 가로로 오른쪽 정렬, 간격 spacing-8. 모바일은 세로로 쌓고 폭을 채우며 주 행동이 위.

Desktop오른쪽 정렬 · 8px
Mobile세로 · 폭 채움 · 주 행동 위

Label

무슨 일이 일어나는지 동사로. 두 어절 안에서 끝내고 느낌표와 영문은 쓰지 않습니다.

이렇게

  • 위임 신청
  • 계좌 등록
  • 엑셀 내려받기
  • 동의하고 계속

이건 피해요

  • 확인(무엇을 확인하는지 모름)
  • 제출하기 / ~하기 남발
  • OK, Submit 같은 영문
  • 느낌표 붙이기

Specification

Variant배경글자호버비활성
NeutralFill/Normal #F0F0F0Label/TitleFill/Strong #E4E4E4opacity .4
PrimaryPrimary/Normal #FF5900Static/Whiteopacity .9opacity .4
LineBackground/Normal + Line/Normal 1pxLabel/Title 600Background/Alternative #F7F7F7opacity .4
Ghost없음Label/BodyFill/Normal 배경 · Label/Titleopacity .4
DangerSystem/Danger #EB003BStatic/Whiteopacity .9opacity .4
Link없음 · 높이 없음Label/Body 13 500Label/Title + 밑줄(offset 3)opacity .4
Size높이좌우 여백글자Radius아이콘
S32spacing-12Label S · Bold814
M36spacing-16Label M · Bold816
L48spacing-24Label L · Bold12없음
Icon only28——816

Props

Prop값기본설명
variantneutral · primary · line · ghost · link · dangerneutral종류. 기본은 회색, 화면의 주 행동 하나만 primary, 흰 카드 안 보조 동작(추가 · 내려받기)은 line
sizesm · md · lgmd크기. lg는 CTA 전용, 아이콘 불가
iconReactNode—앞 아이콘. lucide 16
loadingbooleanfalse스피너 표시 + 비활성. 라벨은 호출하는 쪽에서 진행형으로
disabledbooleanfalseopacity .4, 클릭 불가
fullbooleanfalse폭 채움. 모바일 세로 배치에서
typebutton · submitbutton폼 안에서는 명시. 기본값 submit이면 폼이 날아가요

쓰기

<Button icon={<Download size={16} />}>엑셀 내려받기</Button> {/* 기본 = neutral */} <Button variant="primary" onClick={submit}>위임 신청</Button> {/* 화면에 하나 */} <Button variant="line" icon={<Download size={15} />}>템플릿 내려받기</Button> {/* 흰 카드 안 보조 동작 */} <Button variant="ghost" onClick={close}>취소</Button> <Button variant="danger" onClick={remove}>삭제</Button> {/* 진행 중: 라벨을 진행형으로 */} <Button variant="primary" loading={saving}>{saving ? '신청 중' : '위임 신청'}</Button> {/* 온보딩·빈 화면의 단 하나의 CTA */} <Button variant="primary" size="lg" full>채권 등록하기</Button>

클래스

Tailwind로 쓸 때의 원형. src/index.css의 @layer components.

.btn-primary { @apply inline-flex items-center gap-2 h-9 px-4 rounded-lg text-[13px] font-bold; @apply bg-primary text-white hover:opacity-90; @apply active:scale-[0.98] transition-all duration-200 disabled:opacity-40 disabled:cursor-not-allowed; } .btn-neutral { @apply inline-flex items-center gap-2 h-9 px-4 rounded-lg text-[13px] font-semibold; @apply bg-[#F0F0F0] text-[#1D1D1D] hover:bg-[#E4E4E4]; @apply active:scale-[0.98] transition-all duration-200 disabled:opacity-40 disabled:cursor-not-allowed; } .btn-ghost { @apply inline-flex items-center gap-2 h-9 px-2.5 rounded-lg text-[13px] font-medium; @apply text-[#555555] hover:bg-[#F0F0F0] hover:text-[#1D1D1D]; @apply active:scale-[0.98] transition-all duration-200; } .btn-danger { @apply inline-flex items-center gap-2 h-9 px-4 rounded-lg text-[13px] font-bold; @apply bg-[#EB003B] text-white hover:opacity-90; @apply active:scale-[0.98] transition-all duration-200 disabled:opacity-40; } .btn-sm { @apply h-8 px-3 text-[12px]; } .btn-lg { @apply h-12 px-6 text-[15px] rounded-xl; } .btn-icon { @apply inline-flex items-center justify-center w-7 h-7 rounded-lg text-[#8E8E8E] hover:bg-[#F8F8F8] hover:text-[#1D1D1D]; }

주의

항목설명
btn-outline → Line옛 btn-outline(회색 글자 · 아무 데나)은 폐기. 테두리 버튼은 Line 변형 하나로 다시 정의했어요 — Label/Title 600 · Line/Normal 1px · 호버 Background/Alternative, 흰 카드 안 보조 동작(채권 추가 · 파일 추가 · 템플릿 내려받기)에만. 회색 페이지 위에서는 선이 묻히니 Neutral.
검정 버튼bg-text-primary 버튼이 남아 있는 화면은 Primary로 교체 대상.
a 태그이동은 <Link className="btn-neutral">로. 버튼처럼 보여도 링크는 링크로.

Icon button

글자 없이 아이콘만 있는 버튼. 더보기, 닫기, 새로 고침처럼 뜻이 뻔한 행동에만 쓰고, 이름은 aria-label과 Tooltip으로 줍니다.

더보기 새로 고침 엑셀로 내려받기 알림 3개 닫기

Anatomy

정사각 히트 영역 안에 아이콘 하나. 배경은 호버할 때만 보이고, 알림처럼 개수가 있으면 오른쪽 위에 배지.

  1. 1히트 영역 · 32 (S 28 · L 40) · radius 8 · 배경 없음 → 호버 Fill/Normal
  2. 2아이콘 · 18 (S 16 · L 20) · Label/Body · lucide 2px
  3. 3배지 · Count 16 · Primary · 오른쪽 위 -4 · 개수가 있을 때만

Variants

셋. 대부분은 배경 없는 Ghost. 눌러야 한다는 게 보여야 하면 Neutral, 화면의 유일한 행동이면 Primary.

Ghost기본. 표 행, 카드 머리, 모달 닫기
NeutralFill/Normal 채움. 도구 줄에서 버튼처럼 보여야 할 때
Primary모바일 새로 만들기(FAB)처럼 화면의 주 행동이 아이콘 하나일 때만

Sizes

S · 28아이콘 16 · 표 행 안
M · 32아이콘 18 · 기본
L · 40아이콘 20 · 헤더, 모바일

States

Default배경 없음
HoverFill/Normal
Pressed · OpenFill/Strong · 메뉴 열린 동안
Disabledopacity .4

Behavior

동작규칙
이름aria-label 필수. 마우스를 올리면 같은 이름이 Tooltip으로
아이콘뜻이 한 가지로 읽히는 것만. 더보기 ⋯, 닫기 ×, 새로 고침, 내려받기, 편집, 삭제. 애매하면 글자 버튼
메뉴더보기는 Menu를 열어요. 열린 동안 Pressed 상태 유지
위험삭제 아이콘 버튼은 바로 지우지 않고 Alert로 한 번 물어요
간격나란히 놓을 때 사이 4. 히트 영역이 붙어도 아이콘 사이는 16 이상

Usage

이렇게뜻이 뻔한 아이콘, 같은 크기, 사이 4
← 이게 뭘까요
이건 피해요뜻이 애매한 아이콘. "문자 보내기" 글자 버튼으로
이건 피해요글자 버튼 사이에 아이콘 버튼 섞기. 한 줄은 한 종류로
이건 피해요Primary 아이콘 버튼 여럿. Primary는 화면에 하나

Specification

부분크기아이콘색여백
GhostS 28 · M 32 · L 40 · radius 8 (L 10)16 · 18 · 20Label/Body · 호버 Fill/Normal · 눌림 Fill/Strong사이 4
Neutral같음같음 · Label/TitleFill/Normal · 호버 Fill/Strong—
Primary같음Static/WhitePrimary · 호버 90%—
배지16 · radius 8Caption 10 700Primary · 99+오른쪽 위 -4

Props

Prop값기본설명
iconLucideIcon—크기는 size가 정해요
labelstring—필수. aria-label과 Tooltip에 같이 쓰여요
variantghost · neutral · primaryghost—
sizesm · md · lgmd28 · 32 · 40
countnumber—0이면 숨김, 99 넘으면 99+
pressedbooleanfalse메뉴가 열린 동안 true. aria-expanded로 나가요

쓰기

<IconButton icon={MoreHorizontal} label="더보기" onClick={openMenu} pressed={menuOpen} /> <IconButton icon={Bell} label="알림" count={unread} size="lg" /> <IconButton icon={Download} label="엑셀로 내려받기" variant="neutral" onClick={download} /> <IconButton icon={X} label="닫기" onClick={onClose} />

클래스

.icon-btn { @apply relative inline-flex items-center justify-center w-8 h-8 rounded-lg text-label-body hover:bg-fill active:bg-fill-strong disabled:opacity-40 [&_svg]:w-[18px] [&_svg]:h-[18px]; } .icon-btn-sm { @apply w-7 h-7 [&_svg]:w-4 [&_svg]:h-4; } .icon-btn-lg { @apply w-10 h-10 rounded-[10px] [&_svg]:w-5 [&_svg]:h-5; } .icon-btn-neutral{ @apply bg-fill text-label-title hover:bg-fill-strong; } .icon-btn-primary{ @apply bg-primary text-white hover:opacity-90; } .icon-btn[aria-expanded="true"] { @apply bg-fill-strong; } .icon-btn-count { @apply absolute -top-1 -right-1 min-w-4 h-4 px-1 rounded-full bg-primary text-white text-[10px] font-bold flex items-center justify-center; }

주의

항목설명
p-1 rounded-lg 조각모달 닫기, 헤더 아이콘들이 각자 p-1 hover:bg-surface로 그려져 있어요. IconButton으로.
Button 안 아이콘글자 있는 버튼에 아이콘을 넣는 건 Button의 icon prop. 이 컴포넌트는 글자가 없을 때만.

Badge

상태나 개수를 짧게 표시하는 컴포넌트. 배경을 채우지 않고 색 점 하나와 글자로만 말해서, 표 안에 수십 개가 늘어서도 시끄럽지 않아요. 누를 수 없습니다.

정상 상환 연체 주의 이탈 검토 중 신규 종료 12

Anatomy

점과 라벨. 점이 뜻을 나르고 라벨이 이름을 붙입니다. 개수 배지는 컨테이너와 숫자만.

정상 상환 12
  1. 1점 · 6 원 · 톤 색 · 라벨과 6
  2. 2라벨 · Label S 12 600 · Label/Body
  3. 3개수 배지 · 높이 20 · radius 10 · Primary · Caption 11 700 tabular

Variants

넷. 표와 목록의 상태는 점(Status)이 기본이고, 카드나 제목 옆에서 눈에 띄어야 하면 옅게 채운 Tinted, 사진이나 어두운 면 위처럼 대비가 필요하면 Solid, 개수는 Count.

정상 상환
Status기본. 상태 이름 앞에 톤 색 점. 표, 카드 머리, 리스트 행. 가장 조용해요
추천NEWD-3종료
Tinted톤 10% 채움 + 톤 글자 · 22 · radius 6. 카드 제목 옆, 플랜 이름 옆처럼 한 화면에 셋 이하
연체진행 중종료
Solid톤 채움 + 흰 글자. 사진·차트·어두운 면 위처럼 Tinted가 묻힐 때만
채무자 관리 12
Count미확인 개수. 내비와 탭, 아이콘 버튼에서만. 99를 넘으면 99+

Tone

점 색은 System 컬러에서 옵니다. 뜻이 다르면 톤이 다르고, 톤이 같으면 뜻이 같아야 해요.

정상 상환Success정상, 완료, 진행 중
연체 주의Warning주의, 확인 필요
이탈Danger연체, 이탈, 해지
검토 중Information검토 중, 안내, 대기
신규Brand신규, 추천처럼 눈에 띄어야 하는 것. 한 화면에 하나
종료Neutral끝난 것. 글자까지 회색으로 물러남

Placement

표에서는 상태 열에 하나, 카드에서는 제목 오른쪽 끝에 하나. 한 대상에 배지는 하나가 원칙, 많아도 둘.

채무자상태잔액
김리바정상 상환12,340,000원
이바운연체 주의3,200,000원
박채권종료0원
표상태 열 · 왼쪽 정렬
주식회사 리바운드검토 중
카드제목 줄 오른쪽 끝

Usage

정상 상환
이렇게상태 이름 그대로, 한 대상에 하나
정상 상환연체 주의
이건 피해요배경을 채운 알약 배지. 표 한가득 늘어서면 읽는 속도가 떨어져요
정상 상환신규검토 중
이건 피해요한 대상에 셋. 가장 중요한 상태 하나만
검토 중 →
이건 피해요누르게 만들기. 행동이 필요하면 Button, 선택이면 Chip

Label

상태 이름을 그대로, 2~5자. 숫자와 글자를 한 배지에 섞지 않습니다.

이렇게

  • 정상 상환
  • 연체 주의
  • 검토 중
  • 종료

이건 피해요

  • 연체 3회(숫자 섞기)
  • 진행중입니다(문장)
  • ACTIVE(영문)
  • 정상!

Specification

Variant크기글색여백
Status점 6Label S 12 600 · Label/Body점 = 톤 색점과 6
Tinted높이 22 (S 18) · radius 6 (S 5)12 600 (S 11)톤 10% 채움 + 톤 글자 · Neutral은 Fill/Normal + Label/Body좌우 8 (S 6) · 배지 사이 6
Solid같음12 600톤 채움 + Static/White · Warning만 Label/Title같음
Count높이 20 · 최소 폭 22 · radius 99Caption 11 700 tabularPrimary + Static/White좌우 6
Variant글자점 · 배경간격모양
StatusLabel S 12 · SemiBold · Label/Body점 6 · 톤 색점과 글자 6배경 없음 · 줄바꿈 없음
Status · NeutralLabel/Assistive점 Label/Disable6배경 없음
Count11 · Bold · Static/White · tabularPrimary/Normal좌우 6높이 20 · 폭 22+ · r999
Tone점 색토큰
Success#008A1ESystem/Success
Warning#FFB724System/Warning
Danger#EB003BSystem/Danger
Information#2768FFSystem/Information
Brand#FF5900Primary/Normal
Neutral#C6C6C6Label/Disable

Props

Prop값기본설명
tonesuccess · warning · danger · info · brand · neutralneutral점 색. 뜻에 맞는 톤 하나
childrenstring—상태 이름. 2~5자
Count · countnumber—개수. 99를 넘으면 99+로 표시

쓰기

<Badge tone="success">정상 상환</Badge> <Badge tone="warning">연체 주의</Badge> <Badge tone="danger">이탈</Badge> <Badge tone="neutral">종료</Badge> {/* 상태값과 배지를 한 곳에서 묶어두면 화면마다 다르게 부르지 않아요 */} const STATUS = { ACTIVE: { tone: 'success', label: '정상 상환' }, OVERDUE: { tone: 'warning', label: '연체 주의' }, EXIT: { tone: 'danger', label: '이탈' }, DONE: { tone: 'neutral', label: '종료' }, }; <Badge tone={STATUS[s].tone}>{STATUS[s].label}</Badge> {/* 개수 */} <CountBadge count={unread} />

클래스

지금 rebound-biz에 있는 이름과의 대응. 새 화면은 톤 이름으로 쓰고, 옛 이름은 교체 대상이에요.

.badge { @apply inline-flex items-center gap-1.5 text-xs font-semibold text-[#555555] whitespace-nowrap; @apply before:content-[''] before:inline-block before:w-1.5 before:h-1.5 before:rounded-full before:flex-shrink-0; } .badge-success { @apply badge before:bg-[#008A1E]; } /* 옛 badge-active */ .badge-warning { @apply badge before:bg-[#FFB724]; } /* badge-warn */ .badge-danger { @apply badge before:bg-[#EB003B]; } /* badge-exit */ .badge-info { @apply badge before:bg-[#2768FF]; } .badge-brand { @apply badge before:bg-primary; } /* badge-new */ .badge-neutral { @apply badge text-[#8E8E8E] before:bg-[#C6C6C6]; } /* badge-done */ .count-badge { @apply inline-flex items-center justify-center min-w-[22px] h-5 px-1.5 rounded-full; @apply bg-primary text-white text-[11px] font-bold tabular-nums; }

주의

항목설명
Warning 색옛 badge-warn은 오렌지 점이었어요. 오렌지는 행동 색이라 상태에 쓰지 않고, System/Warning 노랑으로 바꿉니다.
누르는 배지배지에 onClick을 달지 않아요. 필터로 쓰려면 Chip.

Board row

공지, 문의, 알림처럼 글 목록의 한 줄. 제목과 한 줄 설명, 오른쪽에 날짜나 상태. 눌러서 상세로 가고, 읽으면 제목이 옅어집니다. 숫자 열이 많으면 Table.

공지사항
9월 정기 점검 안내 (9/28 02:00~04:00)점검 중에는 로그인과 상환 조회가 잠시 멈춰요.9.17
위임 계약서 양식이 바뀌었어요9월 1일 이후 새 계약부터 적용돼요.9.02
자동이체 출금일 변경 방법설정 > 출금 계좌에서 바꿀 수 있어요.8.25

Anatomy

왼쪽에 제목과 설명, 오른쪽에 메타와 chevron. 안 읽은 글은 제목 앞에 오렌지 점.

9월 정기 점검 안내점검 중에는 상환 조회가 잠시 멈춰요.9.171
  1. 1행 · 높이 72 · 16 20 · 아래 Line/Alternative · 호버 Background/Alternative
  2. 2안 읽음 점 · 6 · Primary · 제목 앞 · 읽으면 사라짐
  3. 3제목 · Body M 15 600 · Label/Title · 한 줄 말줄임 · 읽으면 500 Label/Body
  4. 4설명 · Body S 13 · Label/Alternative · 한 줄 말줄임 · 위 2
  5. 5메타 · Caption 12 · Label/Alternative · 날짜 또는 Badge
  6. 6chevron · 16 · Line/Strong · 행 전체가 링크

Variants

넷. 공지는 Default, 문의처럼 상태가 있으면 With badge, 알림처럼 종류가 다르면 With icon, 제목만 있으면 Compact.

위임 계약서 양식이 바뀌었어요9월 1일 이후 새 계약부터 적용돼요.9.02
Default제목 + 설명 + 날짜. 공지, 도움말
상환 일정을 바꿀 수 있나요?1:1 문의 · 9.15답변 대기
With badge오른쪽에 Status 배지. 문의, 신청 내역
김리바 님이 3,200,000원을 입금했어요10분 전
With icon왼쪽 원 36에 종류 아이콘. 알림 목록
개인정보 처리방침
이용약관
Compact제목만, 높이 52. 설정 메뉴, 문서 목록

States

9월 정기 점검 안내점검 중에는 조회가 멈춰요.9.17
UnreadPrimary 점 · 제목 600 Label/Title
9월 정기 점검 안내점검 중에는 조회가 멈춰요.9.17
HoverBackground/Alternative
9월 정기 점검 안내점검 중에는 조회가 멈춰요.9.17
Read점 없음 · 제목 500 Label/Body
자동이체 안내내용 없음삭제됨
No linkchevron 없음 · 누를 수 없어요

Behavior

동작규칙
이동행 전체가 링크. 누르면 상세로, 돌아오면 그 행이 읽음으로. 본문이 몇 줄이면 행 아래로 펼쳐도 돼요(미리보기처럼)
읽음상세를 열면 읽음. 목록에서 점만 지우고 순서는 바꾸지 않아요
개수한 카드에 5~10행. 넘으면 아래에 "더 보기" Ghost 버튼, 무한 스크롤은 쓰지 않아요
빈 상태행이 없으면 카드 안에 한 줄 "아직 알림이 없어요". Empty state
날짜오늘은 "10분 전", 올해는 "9.17", 지난해는 "2025.12.3"

Usage

위임 계약서 양식이 바뀌었어요9월 1일 이후 새 계약부터 적용돼요.9.02
이렇게제목은 문장, 설명은 한 줄, 날짜는 짧게
[공지] [중요] 위임 계약서 양식 변경 안내의 건2026년 9월 1일 이후 신규 체결되는 위임 계약에 대하여 개정된 양식이 적용됨을 안내드립니다.2026.09.02 14:32
이건 피해요말머리, 격식체, 초 단위 날짜. 잘려요
김리바원금 8,400,000 · 상환률 62% · 연체 0정상
이건 피해요숫자가 셋 넘는 데이터. Table로
9월 정기 점검 안내점검 중에는 조회가 멈춰요.
이건 피해요행 안에 버튼 여럿. 행 전체가 링크, 행동은 상세에서

Specification

부분크기글색여백
행높이 72 · Compact 52—Background/Normal · 아래 Line/Alternative · 호버 Background/Alternative16 20 · 요소 사이 14
제목한 줄 말줄임Body M 15 600 · 읽음 500Label/Title · 읽음 Label/Body—
설명한 줄 말줄임Body S 13Label/Alternative위 2
안 읽음 점6—Primary제목과 6
아이콘 원36 · radius 10 · 아이콘 16—Fill/Normal · Label/Body글과 14
메타 · chevronchevron 16Caption 12Label/Alternative · chevron Line/Strong사이 8
카드radius 12—Card Default안쪽 0

Props

Prop값기본설명
title · descriptionstring—description 없으면 Compact
metastring · ReactNode—날짜 문자열 또는 Badge
iconLucideIcon—왼쪽 원
unreadbooleanfalse점 + 제목 600
to · onClickstring · fn—없으면 chevron도 없고 누를 수 없어요

쓰기

<Card padding="none"> {notices.map(n => ( <BoardRow key={n.id} title={n.title} description={n.summary} meta={fmtDate(n.createdAt)} unread={!n.readAt} to={`/notices/${n.id}`} /> ))} {hasMore && <Button variant="ghost" full onClick={loadMore}>더 보기</Button>} </Card> {/* 알림 */} <BoardRow icon={Bell} title="김리바 님이 3,200,000원을 입금했어요" description={fromNow(a.at)} to={`/payments/${a.id}`} />

클래스

.board-row { @apply flex items-center gap-3.5 px-5 py-4 border-b border-line-alt last:border-0 hover:bg-bg-alt cursor-pointer; } .board-row-icon { @apply w-9 h-9 rounded-[10px] bg-fill text-label-body flex items-center justify-center shrink-0 [&_svg]:w-4 [&_svg]:h-4; } .board-row-title{ @apply block text-[15px] font-semibold text-label-title truncate; } .board-row-desc { @apply block mt-0.5 text-[13px] text-label-alt truncate; } .board-row.is-read .board-row-title { @apply font-medium text-label-body; } .board-row-dot { @apply inline-block w-1.5 h-1.5 rounded-full bg-primary mr-1.5 align-middle; } .board-row-meta { @apply flex items-center gap-2 text-xs text-label-alt whitespace-nowrap shrink-0; } .board-row-chev { @apply w-4 h-4 text-line-strong shrink-0; } .board-row-compact { @apply py-3.5; }

주의

항목설명
NoticeCard공지를 카드 하나씩 그리던 NoticeCard는 BoardRow 목록으로. 카드 안에 행으로 쌓아요.
Table과 구분비교할 숫자 열이 둘 이상이면 Table. 글이 주인공이면 BoardRow.
a vs buttonto가 있으면 a(NavLink), onClick만 있으면 button. div에 onClick 금지.

Form

입력칸은 흰 바탕에 얇은 테두리. 포커스하면 테두리만 Information 파랑으로 바뀌고 그림자는 생기지 않아요. 라벨은 위에, 도움말은 아래에.

등록된 채권 정보와 같아야 해요

매월 25일

10자리 숫자만 입력해 주세요

안내 문자 받기

Anatomy

라벨, 필드, 도움말. 셋이 한 묶음이고 위에서 아래로 읽힙니다. 도움말 자리에 오류가 대신 들어가요.

등록된 채권 정보와 같아야 해요

2
  1. 1라벨 · Label S 13 600 · Label/Title · 아래 6. 필수 항목은 뒤에 빨간 별표(*) Danger 600, 선택 항목은 표시 없음
  2. 2필드 · 높이 40 · Body S 14 · Line/Normal 1px · radius 8. 플레이스홀더는 예시 값
  3. 3도움말 · Caption 12 · Label/Alternative · 위 6. 오류일 땐 Danger 글자와 테두리

Variants

넷. 글을 받으면 Text, 여러 줄이면 Textarea, 정해진 것 중 하나면 Select, 켜고 끄거나 고르면 Checkbox와 Radio.

Text한 줄 글, 숫자, 전화번호. 형식이 있으면 입력 중 자동으로 맞춰요. 자세한 건 Text field
Textarea여러 줄. 3줄에서 시작해 글에 맞춰 8줄까지 자동으로. 자세한 건 Textarea
매월 25일
Select정해진 것 중 하나. 5개 이하면 Radio를 먼저 생각해요
안내 문자 받기
매월 10일매월 25일
Checkbox · Radio16 정사각과 원. 켜지면 Information 파랑 채움. 자세한 건 Checkbox, Radio

Select

브라우저 기본 목록은 쓰지 않고 우리 메뉴를 그립니다. 닫혀 있을 땐 Text 필드와 같고, 열리면 아래로 6 떨어진 메뉴. 고른 항목은 오른쪽에 체크.

은행을 골라 주세요
Empty플레이스홀더 · chevron Label/Alternative
국민은행
ClosedText 필드와 같은 모양
Open메뉴 radius 12 · Elevation Dropdown · 항목 40 · 여섯 개 넘으면 스크롤

Sizes

둘. 기본은 40이고, 로그인과 온보딩처럼 필드가 화면의 주인공일 때만 48.

M · 40기본 · Body S 14 · 좌우 14
L · 48로그인, 온보딩 · Body M 15 · 좌우 16 · radius 10

States

여섯. 포커스는 테두리 색만 Information으로 바뀌고, 오류는 테두리와 문구가 함께 Danger로.

DefaultLine/Normal · 플레이스홀더 Label/Alternative
FocusInformation 테두리 1 · 그림자 없음
FilledLabel/Title

10자리 숫자만 입력해 주세요

ErrorDanger 테두리 · 도움말 자리에 오류
DisabledBackground/Alternative 채움 · Label/Disable
Read-only테두리 없이 채움만. 복사는 되게

Layout

한 열. 필드 사이 20, 라벨은 항상 위. 시작일과 종료일처럼 짝인 짧은 값만 나란히 둡니다. 버튼은 맨 아래 오른쪽.

표시(*)는 필수예요

기본 폼폭 최대 480 · 필드 사이 20 · 짝인 값만 2열 · 필수는 라벨 뒤 별표(*) · 발에 취소와 주 행동
010
붙는 컨트롤하나의 값이면 라벨 하나 아래 나란히. 간격 8

Validation

오류는 무엇을 어떻게 고칠지 말합니다. "유효하지 않은 값입니다" 대신 "10자리 숫자만 입력해 주세요".

상황시점표시
형식 오류필드를 떠날 때 (blur)테두리 Danger + 도움말 자리에 오류 문구. 입력 중에는 지우지 않아요
빈 필수 값제출할 때빨간 선 + 아래 문구 + 좌우 흔들림 .35s. 첫 오류 필드로 스크롤하고 포커스. 상단 요약 배너는 쓰지 않아요
서버 오류응답 후관련 필드가 있으면 그 필드에, 없으면 폼 위에 한 줄
도움말과 오류—같은 자리를 씁니다. 오류가 있으면 도움말은 사라져요
필수 · 선택—필수 항목은 라벨 뒤에 빨간 별표(*), 선택 항목은 아무 표시도 하지 않아요. 글자로 (필수) · (선택)을 붙이지 않고, '선택' 알약으로 바꿔 달지도 않아요. 필수와 선택이 섞인 폼이면 맨 위에 "표시(*)는 필수예요" 한 줄을 둘 수 있어요
자동 형식입력 중전화번호와 사업자번호는 하이픈을 자동으로. 금액은 천 단위 콤마

Usage

등록된 채권 정보와 같아야 해요

이렇게라벨은 위에 남고, 플레이스홀더는 예시
이건 피해요플레이스홀더를 라벨로. 입력하면 무엇을 묻는지 사라져요

하이픈 없이 입력해 주세요

10자리 숫자만 입력해 주세요

이건 피해요도움말과 오류를 동시에. 한 자리에 하나만
이건 피해요포커스에 글로우. 테두리 색만 바뀌어요

Specification

부분크기글색 · 선여백
라벨—Label S 13 600Label/Title · 필수 별표(*) Danger 600 · 라벨과 2아래 6
Text · Select M높이 40 · radius 8Body S 14Background/Normal · Line/Normal 1px좌우 14 · Select 오른쪽 36
Text L높이 48 · radius 10Body M 15같음좌우 16
Select 메뉴폭 = 필드 · radius 12 · 항목 40Body S 14 · 선택 600 + 체크 14Background/Normal · Elevation Dropdown · 호버 Fill/Normal위 6 · 안쪽 4 · 항목 좌우 12
Select 자리 · 순서body 포털(fixed) · 아래 6—아래 공간이 모자라면 위로. 카드 overflow·transform에 가려지지 않게선택지는 가나다순 — 영문 머리글자(KB · NH · IBK · SC)는 빼고 한글 이름으로. 자주 쓰는 것 먼저 같은 임의 순서 없음
Textarea최소 96 · radius 8Body S 14 · 행간 1.6같음10 14
Checkbox · Radio16 · radius 4 / 원Body S 14 라벨Line/Strong 1px → 켜지면 Information 채움라벨과 8
Focus——Information 1px · 그림자 없음—
Error—Caption 12Danger 테두리와 글자위 6
Disabled · Read-only—Label/Disable · Label/BodyBackground/Alternative 채움 · 선 Line/Alternative · 없음—
도움말—Caption 12Label/Alternative위 6
필드 사이20——짝 필드 12 · 붙는 컨트롤 8

Props

Prop값기본설명
labelstring—항상 있어야 해요. 숨기려면 hideLabel로 시각만 숨김
requiredbooleanfalse라벨 뒤에 빨간 별표(*). 선택 항목은 표시 없음
help · errorstring—같은 자리. error가 있으면 help 대신 보여요
sizemd · lgmd40 · 48
disabled · readOnlybooleanfalse회색 채움. readOnly는 복사 가능
options · placeholder{ value, label }[] · string—SelectField. 여섯 개 넘으면 메뉴 안 스크롤
formatphone · biz · money—입력 중 하이픈, 콤마 자동

쓰기

<TextField label="채무자 이름" required placeholder="예) 김리바" help="등록된 채권 정보와 같아야 해요" value={name} onChange={setName} /> <TextField label="사업자등록번호" format="biz" value={bizNo} onChange={setBizNo} error={touched && !isBiz(bizNo) ? '10자리 숫자만 입력해 주세요' : undefined} /> <SelectField label="상환 주기" value={cycle} onChange={setCycle} options={[{ value: 25, label: '매월 25일' }, { value: 'last', label: '매월 말일' }]} /> <TextareaField label="메모" rows={3} /> <Checkbox checked={sms} onChange={setSms}>안내 문자 받기</Checkbox> <RadioGroup value={day} onChange={setDay} options={[{ value: 10, label: '매월 10일' }, { value: 25, label: '매월 25일' }]} />

클래스

.field-label { @apply block text-[13px] font-semibold text-label-title mb-1.5; } .field-opt { @apply ml-1 font-medium text-label-alt; } .input { @apply w-full h-10 px-3.5 text-sm text-label-title bg-white border border-line rounded-lg placeholder:text-label-alt focus:outline-none focus:border-info transition-[border-color]; } .input-lg { @apply h-12 px-4 text-[15px] rounded-[10px]; } textarea.input { @apply h-auto min-h-24 py-2.5 leading-relaxed resize-y; } .select { @apply input flex items-center justify-between pr-3 cursor-pointer; } /* 트리거. 네이티브 select 아님 */ .select-menu { @apply mt-1.5 p-1 bg-white rounded-xl shadow-dropdown max-h-[264px] overflow-y-auto; } .select-item { @apply flex items-center justify-between h-10 px-3 rounded-lg text-sm text-label-title hover:bg-fill; } .select-item.is-selected { @apply font-semibold; } .input-error { @apply border-danger focus:border-danger; } .input:disabled { @apply bg-bg-alt text-label-disable border-line-alt; } .input:read-only { @apply bg-bg-alt border-transparent; } .field-help { @apply mt-1.5 text-xs text-label-alt; } .field-error { @apply mt-1.5 text-xs text-danger; } .checkbox { @apply h-4 w-4 rounded border border-line-strong text-info focus:ring-0; } .radio { @apply h-4 w-4 border border-line-strong text-info focus:ring-0; }

주의

항목설명
라벨 클래스 세 가지지금 제품에 13/bold, 13/semibold, sm/medium 라벨이 섞여 있어요. .field-label 하나로.
모바일 16pxiOS 확대 방지용 16px 강제는 유지. 그래서 모바일 필드는 L 사이즈처럼 보여요.
focus ringTailwind 기본 ring과 브라우저 outline은 끕니다. 테두리 색만 Information으로. 체크박스·라디오·스위치와 같은 파랑이라 '입력 계열'이 한 색으로 묶여요.
네이티브 select브라우저와 OS마다 다르게 열려요. role="listbox" 메뉴로 직접 그리고, 640 미만에서는 Bottom sheet로 열어요.
accent-color네이티브 checkbox, radio는 accent-color를 Information #2768FF로. 오렌지가 기본으로 들어가지 않게.

Search field

목록에서 찾을 때 쓰는 입력칸. 흰 면에 Text field와 같은 1px 테두리, 검색 아이콘, 글자가 있으면 지우기 버튼. 입력하는 동안 결과가 따라오고, 결과가 없으면 표가 빈 상태로 말해줘요.

채무자 이름, 사업자번호
김리

Anatomy

왼쪽 아이콘, 글, 오른쪽 지우기. 라벨은 없고 플레이스홀더가 무엇을 찾을 수 있는지 말해요.

김리1
  1. 1컨테이너 · 높이 40 · radius 8 · 흰 면 · Line/Normal 1px 테두리(Text field와 같음) · 포커스하면 Information 테두리
  2. 2검색 아이콘 · 16 · Label/Alternative
  3. 3글 · Body S 14 · Label/Title · 플레이스홀더는 무엇을 찾을 수 있는지
  4. 4지우기 · 원 18 Line/Strong + X 10 · 글자가 있을 때만

Variants

넷. 표 위 도구 줄에 두는 게 기본이고, 조건이 많으면 옆에 필터 버튼, 좁은 자리엔 Compact. 결과가 없으면 표가 말해요.

채무자 검색
Default표 도구 줄 왼쪽. 폭 240~320
채무자 검색
With filter조건이 셋 넘으면 필터 버튼을 옆에. 적용 중이면 개수 배지
검색
Compact높이 32. 카드 머리, 메뉴 안
홍길동
채무자원금상태
'홍길동'으로 찾은 결과가 없어요.
Empty result표 자리에 검색어를 넣은 문장과 지우기 버튼

States

채무자 검색
Default흰 면 · Line/Normal 1px · 플레이스홀더
채무자 검색
Focus흰 배경 · Information 테두리
김리
Filled지우기 나타남
채무자 검색
Disabledopacity .4

Behavior

동작규칙
검색입력을 멈추고 300ms 뒤 자동으로. Enter는 바로. 검색 버튼은 없어요
지우기X를 누르거나 ESC. 비우면 전체 목록으로 돌아가고 포커스는 남아요
결과 없음표 자리에 "'김리'로 찾은 결과가 없어요" + 지우기 버튼. 검색어를 문장에 넣어요
범위플레이스홀더에 적은 항목만. "채무자 이름, 사업자번호"면 그 둘로만 찾아요
유지검색어는 URL 쿼리(?q=)에. 새로 고쳐도, 돌아와도 남아요

Usage

채무자 이름, 사업자번호
이렇게무엇으로 찾을 수 있는지 플레이스홀더에
검색어 입력
이건 피해요검색 버튼. 입력하면 바로 찾아요
채무자 검색
이건 피해요라벨 붙은 Text 필드로 검색. 검색은 이 컴포넌트로

Specification

부분크기글색여백
컨테이너높이 40 (S 32) · radius 8 · 폭 240~320Body S 14 (S 13)Background/Normal · Line/Normal 1px → 포커스 Information 1px좌우 12 · 안쪽 간격 8
아이콘16 (S 14)—Label/Alternative—
지우기원 18 · X 10—Line/Strong · 호버 Label/Alternative—
시간———debounce 300ms

Props

Prop값기본설명
value · onChangestring · fn—onChange는 300ms debounce 뒤. onSubmit은 Enter
placeholderstring'검색'무엇으로 찾는지
sizemd · smmd40 · 32
syncQuerystring—'q'처럼 주면 URL 쿼리와 동기화

쓰기

<SearchField placeholder="채무자 이름, 사업자번호" value={q} onChange={setQ} syncQuery="q" /> {/* 표 도구 줄 */} <div className="flex items-center gap-2 mb-4"> <SearchField className="w-[280px]" … /> <Button variant="neutral" leading={ListFilter}>필터{active > 0 && <Badge variant="count">{active}</Badge>}</Button> </div>

클래스

.search { @apply flex items-center gap-2 h-10 px-3 rounded-lg bg-fill border border-transparent text-sm text-label-title focus-within:bg-white focus-within:border-info transition-colors; } .search input { @apply flex-1 min-w-0 bg-transparent outline-none placeholder:text-label-alt; } .search-icon { @apply w-4 h-4 text-label-alt shrink-0; } .search-clear { @apply w-[18px] h-[18px] rounded-full bg-line-strong text-white flex items-center justify-center hover:bg-label-alt; } .search-sm { @apply h-8 px-2.5 text-[13px]; }

주의

항목설명
type="search"브라우저 기본 지우기(X)가 따로 생겨요. appearance none으로 끄고 우리 지우기만.
roleinput에 role="searchbox", 감싸는 form에 role="search". 결과 수는 aria-live로.

Radio

여럿 중 하나만 고르는 동그란 칸. 둘에서 다섯 개를 한눈에 보여줄 때 쓰고, 그보다 많으면 Select. 켜고 끄는 거면 Checkbox나 Switch. 고른 색은 Information 파랑.

Anatomy

16 동그라미와 안의 점, 오른쪽에 라벨. 설명이 있으면 라벨 아래. 라벨을 눌러도 골라져요.

  1. 1원 · 16 · Line/Strong 1 → 고르면 Information 링 5
  2. 2점 · 6 · Static/White · 링 안쪽
  3. 3라벨 · Body S 14 · Label/Title
  4. 4설명 · Caption 12.5 · Label/Alternative · 위 2

Variants

넷. 세로 목록이 기본, 짧은 값 셋 이하면 가로, 설명이 붙으면 With description, 큼직하게 보여야 하면 Card.

Vertical기본. 사이 12, 라벨 하나 아래 묶어요
Horizontal짧은 값 셋 이하. 사이 24
With description차이를 설명해야 할 때. 한 줄씩
Card이름 + 한 줄 설명이 있는 택1. Choice card

Sizes

M · 16기본 · 링 5 · 라벨 14
L · 20모바일 · 링 6 · 라벨 15

States

UnselectedLine/Strong 1
SelectedInformation 링
Disabledopacity .4

하나를 골라 주세요

ErrorDanger 테두리 · 아래 문구

Behavior

동작규칙
고르기하나를 고르면 다른 건 풀려요. 고른 걸 다시 눌러도 풀리지 않아요
기본값대부분 하나가 미리 골라져 있어요. 꼭 사용자가 정해야 하면 비워 두고 제출 때 Error
개수2~5개. 여섯이 넘으면 Select, 둘인데 켜고 끄는 뜻이면 Switch나 Checkbox
키보드그룹에 Tab으로 들어가고 ↑↓로 옮겨요. 옮기는 순간 골라져요
라벨원, 라벨, 설명 어디를 눌러도. 그룹 위엔 무엇을 고르는지 라벨 하나

Usage

이렇게그룹 라벨 하나, 세로 목록, 기본값 하나
이건 피해요여섯 개 넘는 목록. Select
이건 피해요켜기/끄기를 Radio로. Switch
이건 피해요오렌지 라디오. 선택 계열은 Information

Specification

부분크기글색여백
원 M16 · 선 1 · 고르면 링 5—Line/Strong → Information · Error Danger라벨과 8
원 L20 · 링 6라벨 15같음라벨과 10
라벨 · 설명—Body S 14 · Caption 12.5Label/Title · Label/Alternative설명 위 2
그룹—그룹 라벨 Label S 13 600—세로 12 · 가로 24 · 라벨 아래 8

Props

Prop값기본설명
value · onChangeT · fn—RadioGroup에
options{ value, label, description?, disabled? }[]—2~5개
labelstring—그룹 라벨. 없으면 aria-label
directionvertical · horizontalverticalhorizontal은 셋 이하
sizesm · md · lgmdsm은 칸 16 · 라벨 12.5 Label/Body — '모든 채권에 적용'처럼 제목 옆에 붙는 보조 옵션 · md 16 · lg 20
errorstring—아무것도 안 골랐을 때

쓰기

<RadioGroup label="상환 주기" value={day} onChange={setDay} options={[ { value: 10, label: '매월 10일', description: '급여일 직후. 가장 많이 골라요' }, { value: 25, label: '매월 25일' }, { value: 'last', label: '매월 말일' }, ]} /> <RadioGroup label="사업자 유형" direction="horizontal" value={type} onChange={setType} options={[{ value: 'individual', label: '개인' }, { value: 'sole', label: '개인사업자' }, { value: 'corp', label: '법인' }]} />

클래스

.radio { @apply peer sr-only; } .radio-dot { @apply inline-block w-4 h-4 rounded-full border border-line-strong bg-white shrink-0 peer-checked:border-[5px] peer-checked:border-info peer-focus-visible:ring-2 peer-focus-visible:ring-info/30 peer-disabled:opacity-40; } .radio-lg .radio-dot { @apply w-5 h-5 peer-checked:border-[6px]; } .radio-group { @apply flex flex-col gap-3; } .radio-group-h { @apply flex-row gap-6; } .radio-label { @apply text-sm text-label-title; } .radio-desc { @apply mt-0.5 text-[12.5px] text-label-alt; } .radio-error .radio-dot { @apply border-danger; }

주의

항목설명
role그룹은 role="radiogroup" + aria-labelledby. 항목은 진짜 input type=radio를 sr-only로 두고 점을 그려요.
플랜 선택회수 전략(강경/완화)처럼 이름 + 한 줄 설명이 있는 택1은 Choice card(Radio 의미)로. 버튼 여러 개로 만들지 않아요.

Choice card

이름 있는 선택지 몇 개 중 하나를 고르는 카드. 둘에서 넷을 나란히 놓고, 제목 아래 한 줄 설명으로 차이를 말해요. 고르면 Information 테두리와 오른쪽에 파란 체크. 짧은 값이면 Radio, 여럿을 고르면 Checkbox.

Anatomy

흰 카드 한 장. 왼쪽에 제목과 알약, 그 아래 설명. 오른쪽 끝에 고름 표시 원. 카드 어디를 눌러도 골라져요.

완화장기 · 저부담월 상환액을 낮춰 끝까지 갚게 해요1
  1. 1카드 · Background/Normal · Line/Normal 1 · radius 12 · 안쪽 12 16 → 고르면 Information 테두리 + 4% 틴트
  2. 2제목 · 15 500 · Label/Title
  3. 3알약 · 있어도 없어도 · Badge Tinted neutral · 제목 오른쪽 8
  4. 4설명 · 13 · Label/Body · 한 줄 · 제목 아래 2
  5. 5고름 표시 · 18 원 · 선 1.5 Label/Disable → 고르면 Information 채움 + 흰 체크 11

Variants

넷. 둘이면 2열이 기본, 셋이나 넷이면 그만큼 열을 늘려요. 제목만으로 뜻이 분명하면 설명을 빼고, 지금 고를 수 없는 선택지는 흐리게 두고 이유를 설명에 적어요.

2열기본. 둘 중 하나, 카드 사이 8. 그룹 라벨 하나를 위에
3열셋이면 3열, 넷이면 4열. 다섯부터는 Radio 세로 목록이나 Select
설명 없음제목만으로 뜻이 분명하고 고르는 게 화면의 중심일 때. 폼 안의 짧은 값이면 Radio
비활성고를 수 없는 선택지. opacity .4, 이유는 설명에. 숨기지 않아요

States

선택은 파랑(Information). 오렌지 테두리로 선택을 표시하지 않아요. 비활성은 흐리게.

개인사업자번호 없음
DefaultBackground/Normal · Line/Normal 1
개인사업자번호 없음
HoverBackground/Alternative
개인사업자번호 없음
SelectedInformation 테두리 · 4% 틴트 · 채운 체크
개인사업자번호 없음
Disabledopacity .4

Behavior

동작규칙
고르기카드 어디를 눌러도. 하나를 고르면 다른 건 풀려요. 고른 걸 다시 눌러도 풀리지 않아요(Radio와 같아요)
기본값대부분 하나가 미리 골라져 있어요. 꼭 사용자가 정해야 하면 비워 두고 제출 때 그룹 아래 Error 문구
개수2~4개. 다섯부터는 Radio 세로 목록이나 Select. 둘인데 켜고 끄는 뜻이면 Switch
키보드Tab으로 그룹에 들어가고 Space·Enter로 골라요. ←→로 옮기기는 선택 사항(옮기는 순간 골라져요)
전환배경·테두리 150ms. 체크는 바로
모바일640 미만에서는 세로로 쌓여요. 카드 높이는 그대로

Usage

이렇게이름 + 한 줄 설명이 있는 택1. 그룹 라벨 하나, 기본값 하나
개인사업자번호 없음
사업자개인사업자 · 법인
이건 피해요오렌지 테두리로 선택 표시. 선택 계열은 Information
문자바로 도착해요
이메일기록이 남아요
이건 피해요여럿 고르기에 택1 카드. 둘 이상 고르면 Checkbox
국민은행
신한은행
우리은행
하나은행
농협
카카오뱅크
이건 피해요다섯 넘는 선택지를 카드로. Radio 세로 목록이나 Select

Specification

부분크기글색여백
카드열 균등 · 높이는 내용 · radius 12 · 선 1—Background/Normal · Line/Normal #E4E4E4 → 고르면 Information #2768FF 테두리 + Information 4% 채움 · 호버 Background/Alternative #F7F7F7안쪽 12 16 · 카드 사이 8
제목 · 알약알약 2215 600 · 알약 Badge Tinted 12Label/Title · 알약 Fill/Normal + Label/Body알약은 제목 오른쪽 8
설명—13 · 한 줄(keep-all)Label/Body제목 아래 2
고름 표시18 원 · 선 1.5 · 체크 11 (stroke 3.5)—Label/Disable #C6C6C6 → Information 채움 + Static/White 체크글과 12 · 세로 가운데
그룹2~4열 grid · 640 미만 1열그룹 라벨 Label S 13 600—라벨 아래 6 · 카드 사이 8
Disabled——opacity .4—

Props

Prop값기본설명
selectedboolean—고른 카드 하나만 true. 그룹 값은 부모가 들어요
onSelect() => void—누르면 호출. 이미 고른 카드를 눌러도 호출돼요(값은 그대로)
titleReactNode—15 600 Label/Title. 명사 2~4자
badgeReactNode—제목 오른쪽 알약. <KpiPill>로
descriptionReactNode—13 Label/Body. 한 줄로 차이를 말해요
disabledbooleanfalseopacity .4. 이유는 description에

쓰기

{/* 회수 전략 · 알약 + 설명 */} <div className="grid grid-cols-2 gap-2" role="radiogroup" aria-label="회수 전략"> {STRATEGIES.map(o => ( <ChoiceCard key={o.v} name="strategy" selected={strategy === o.v} onSelect={() => setStrategy(o.v)} title={o.label} badge={<KpiPill>{o.tag}</KpiPill>} description={o.desc} /> ))} </div> {/* 채무자 유형 · 설명만 */} <div className="grid grid-cols-2 gap-2" role="radiogroup" aria-label="채무자 유형"> <ChoiceCard selected={kind === 'INDIVIDUAL'} onSelect={() => setKind('INDIVIDUAL')} title="개인" description="사업자등록번호 없음" /> <ChoiceCard selected={kind === 'BUSINESS'} onSelect={() => setKind('BUSINESS')} title="사업자" description="개인사업자 · 법인" /> </div>

클래스

/* src/components/arc/ChoiceCard.tsx — button role="radio" aria-checked */ .choice { @apply flex w-full items-center justify-between gap-3 rounded-lg border border-line bg-white px-4 py-3 text-left transition-colors duration-150 hover:bg-fill-alt; } .choice[aria-checked="true"] { @apply border-info bg-info/[0.04]; } .choice:disabled { @apply cursor-not-allowed opacity-40; } .choice-title { @apply flex items-center gap-2 text-[15px] font-semibold text-label-title; } .choice-desc { @apply mt-0.5 block text-[13px] text-label-body; } .choice-mark { @apply h-[18px] w-[18px] shrink-0 rounded-full border-[1.5px] border-label-disable; } .choice[aria-checked="true"] .choice-mark { @apply flex items-center justify-center border-0 bg-info; } /* 안에 lucide Check 11 · stroke 3.5 · 흰색 */

주의

항목설명
role그룹은 role="radiogroup" + aria-label, 카드는 button role="radio" aria-checked. div onClick으로 만들면 키보드로 못 골라요.
어디에rebound-biz src/components/arc/ChoiceCard.tsx. 채무자 유형(DebtRegistration)과 회수 전략(ConfigurePlans)이 같이 써요. 화면마다 다시 그리지 않아요.
Card Selectable과회색 채움 .card.pick은 그리드에서 고르는 카드용이에요. 폼 안에서 이름 + 설명으로 하나를 고르는 건 Choice card.

Switch

켜고 끄는 스위치. 누르는 순간 바로 적용되는 설정에만 씁니다. 저장 버튼을 눌러야 반영되면 Checkbox. 켜진 색은 Checkbox와 같은 Information 파랑.

상환 알림 문자입금일 하루 전에 채무자에게 보내요
연체 알림3일 연체되면 나에게 알려요

Anatomy

트랙과 손잡이. 켜지면 트랙이 파랗게 되고 손잡이가 오른쪽으로. 라벨은 왼쪽, 설명은 그 아래.

상환 알림 문자입금일 하루 전에 보내요
  1. 1트랙 · 44×24 · radius 12 · 꺼짐 Line/Strong · 켜짐 Information
  2. 2손잡이 · 20 · Static/White · 그림자 살짝 · 왼쪽 ↔ 오른쪽 200ms
  3. 3라벨 · Body S 14 600 · Label/Title · 누르면 스위치도 바뀜
  4. 4설명 · Caption 12.5 · Label/Alternative · 있어도 없어도

Variants

셋. 설정 화면의 행은 List row, 폼 안에 한 줄로 두면 Inline, 스위치만 놓으면 Bare.

상환 알림 문자입금일 하루 전
List row설정 화면. 카드 안에 행으로, 행 사이 Line/Alternative
종료된 건 포함
Inline표 도구 줄, 폼 안. S 36×20에 라벨 500
Bare표 셀 안처럼 라벨이 머리글에 이미 있을 때. aria-label 필수

States

OffLine/Strong
OnInformation
Off · Disabledopacity .4
On · Disabledopacity .4 · 이유는 툴팁

Behavior

동작규칙
적용누르는 순간 저장. 서버 응답을 기다리는 동안 손잡이는 이미 넘어가 있고, 실패하면 되돌리며 Toast
라벨라벨과 설명을 눌러도 바뀜. 라벨은 켜졌을 때의 상태를 말해요("상환 알림 문자", "문자 보내기 켜기" 아님)
확인끄면 되돌리기 어려운 것(자동이체 해지)은 Alert로 한 번 물어요
키보드Space로 토글. role="switch", aria-checked
Checkbox와저장 버튼이 있는 폼 안에서는 Switch를 쓰지 않아요. 여러 개를 고르는 것도 Checkbox

Usage

상환 알림 문자입금일 하루 전에 보내요
이렇게바로 적용되는 설정. 라벨은 상태의 이름
안내 문자 받기
이건 피해요저장 버튼이 있는 폼 안의 Switch. Checkbox로
문자 알림을 켤까요?
이건 피해요질문형 라벨. 상태의 이름으로
알림
이건 피해요오렌지 스위치. 선택 계열은 Information

Specification

부분크기글색여백
트랙 M44×24 · radius 12—꺼짐 Line/Strong #C6C6C6 · 켜짐 Information #2768FF라벨과 16
트랙 S36×20 · radius 10—같음라벨과 12
손잡이20 (S 16) · 안쪽 2—Static/White · 그림자 0 1 2 15%이동 200ms
라벨 · 설명—Body S 14 600 · Caption 12.5Label/Title · Label/Alternative행 위아래 14

Props

Prop값기본설명
checked · onChangeboolean · fn—onChange 안에서 바로 저장. 실패 시 되돌리기
label · descriptionstring—없으면 aria-label 필수
sizemd · smmd44×24 · 36×20
disabledbooleanfalse이유는 Tooltip으로

쓰기

<Switch label="상환 알림 문자" description="입금일 하루 전에 채무자에게 보내요" checked={sms} onChange={async v => { setSms(v); try { await save({ sms: v }); } catch { setSms(!v); toast('저장하지 못했어요'); } }} /> {/* 표 도구 줄 */} <Switch size="sm" label="종료된 건 포함" checked={showClosed} onChange={setShowClosed} />

클래스

.switch { @apply relative inline-block w-11 h-6 rounded-full bg-line-strong transition-colors; } .switch::after { @apply content-[''] absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white shadow-sm transition-[left] duration-200; } .switch[aria-checked="true"] { @apply bg-info; } .switch[aria-checked="true"]::after { @apply left-[22px]; } .switch-sm { @apply w-9 h-5; } .switch-sm::after { @apply w-4 h-4; } .switch-sm[aria-checked="true"]::after { @apply left-[18px]; } .switch:disabled { @apply opacity-40 cursor-not-allowed; } .switch-row { @apply flex items-center justify-between gap-4 py-3.5 border-b border-line-alt last:border-0; }

주의

항목설명
rolebutton role="switch" aria-checked. input type=checkbox로 만들면 스크린리더가 체크박스라고 읽어요.
Settings 화면지금 Settings의 알림 설정은 Checkbox + 저장 버튼이에요. 바로 저장되는 항목은 Switch row로 바꿔요.

Textarea

여러 줄 글을 받는 입력칸. 예상되는 글 양에 맞춰 시작 높이를 정해 두고, 넘치면 최대 줄까지 자동으로 늘어난 뒤 스크롤. 손잡이로 늘리는 건 없어요. 글자 수 제한이 있으면 오른쪽 아래에 세어 줍니다.

58 / 200

Anatomy

라벨, 필드, 오른쪽 아래 글자 수. 도움말이 있으면 글자 수와 같은 줄 왼쪽에.

2
채무자에게 보이지 않아요36 / 200
  1. 1라벨 · Label S 13 600 · Text 필드와 같음
  2. 2필드 · 시작 3줄(96) → 최대 8줄 자동 · 10 14 · Body S 14 · 행간 1.6 · radius 8 · 손잡이 없음
  3. 3도움말 · Caption 12 · Label/Alternative · 왼쪽
  4. 4글자 수 · Caption 12 tabular · 오른쪽 · 넘으면 Danger

Variants

글 양으로 셋을 고릅니다. 한두 문장이면 Short, 보통 메모는 Default, 사유나 긴 설명은 Long. 모두 글에 맞춰 최대 줄까지 자동으로 늘어나요. 제한이 있으면 Counter를 붙여요.

Short2줄 시작 → 4줄. 문자, 한 줄 메모
Default3줄 시작 → 8줄. 대부분의 메모
Long6줄 시작 → 12줄. 사유, 동의 내용
0 / 500
Counter제한이 있을 때만. 넘으면 숫자와 테두리 Danger

States

DefaultLine/Normal
FocusInformation 테두리 · 그림자 없음
212 / 200
Error제한 초과 · Danger 테두리와 숫자
DisabledBackground/Alternative 채움
Read-only테두리 없이 채움만 · 복사 가능

Behavior

동작규칙
Enter줄바꿈. 제출은 버튼으로만. ⌘/Ctrl+Enter로 제출은 허용
크기시작 높이는 예상 글 양으로 정해 두고, 글이 넘치면 한 줄씩 늘어나요. 최대 줄을 넘으면 안에서 스크롤. 손잡이로 늘리게 두지 않아요
한 줄 시작'간략히 적어 주세요' 같은 짧은 메모는 Text field 높이(한 줄)에서 시작해 글에 맞춰 최대 8줄까지. 최대 줄 전까지는 스크롤 막대 없음
글자 수입력하는 동안 실시간. 제한을 넘어도 입력은 막지 않고 Danger로 알려요. 제출 때 막아요
붙여넣기서식은 버리고 글만. 줄바꿈은 유지
모바일16px 강제(확대 방지). 키보드가 올라오면 필드가 보이도록 스크롤

Usage

0 / 200
이렇게라벨 위, 예시 플레이스홀더, 제한이 있으면 글자 수
이건 피해요한 줄 값에 Textarea. 이름, 번호, 금액은 Text
이건 피해요손잡이로 늘리는 필드. 글에 맞춰 자동으로
9
이건 피해요제한 없는데 글자 수. Counter는 제한이 있을 때만

Specification

부분크기글색 · 선여백
필드Short 64→4줄 · Default 96→8줄 · Long 144→12줄 · radius 8Body S 14 · 행간 1.6 (한 줄 22.4)Background/Normal · Line/Normal 1px · 손잡이 없음10 14
글자 수—Caption 12 tabularLabel/Alternative · 초과 Danger위 6 · 오른쪽
Focus · Error · Disabled——Text 필드와 같음—

Props

Prop값기본설명
rows · maxRowsnumber3 · 8시작 줄과 최대 줄. Short 2·4, Long 6·12
maxLengthnumber—주면 글자 수가 보여요. 입력은 막지 않아요
label · help · errorstring—TextField와 같음

쓰기

<TextareaField label="상담 메모" placeholder="예) 25일 입금 약속, 다음 주 재확인" value={memo} onChange={setMemo} maxLength={200} help="채무자에게 보이지 않아요" /> <TextareaField label="채무자에게 보내는 말" rows={2} maxRows={4} value={msg} onChange={setMsg} /> <TextareaField label="분쟁 사유" rows={6} maxRows={12} maxLength={500} value={reason} onChange={setReason} />

클래스

textarea.input { @apply h-auto py-2.5 leading-relaxed resize-none overflow-hidden; } /* 높이는 JS: rows로 시작, 입력마다 scrollHeight, maxRows 넘으면 overflow-auto */ .textarea-count { @apply mt-1.5 flex justify-end text-xs tabular-nums text-label-alt; } .textarea-count.is-over { @apply text-danger; }

주의

항목설명
maxlength 속성HTML maxlength는 입력을 뚝 끊어요. 쓰지 말고 Counter로 알려요.
resizeresize: none. 사용자가 손잡이로 늘리지 않고 글에 맞춰 자동으로. 높이는 rows·maxRows로 정해요.

Time picker

시각을 고르는 필드. 닫혀 있을 땐 시계 아이콘이 붙은 Select, 열리면 시와 분 두 열. 직접 타이핑해도 되고, 분은 5 단위.

09:30
09:00
~
18:00

Anatomy

트리거는 Select와 같고 앞에 시계 아이콘. 메뉴는 시 열과 분 열, 고른 값은 가운데에 진하게.

  1. 1트리거 · Text 필드와 같음 · 포커스 Information 테두리 · 값은 HH:MM tabular
  2. 2시계 아이콘 · 16 · Label/Alternative
  3. 3시 열 · 00~23 · 행 36 · 고른 값이 가운데 오게 스크롤
  4. 4고른 값 · Fill/Normal 채움 · Label/Title 600
  5. 5메뉴 · 폭 = 필드 · 높이 180 · radius 12 · Elevation Dropdown · 분 열은 5 단위

Variants

넷. 시각 하나면 Time, 시작과 끝이면 Range, 날짜와 함께면 Date + time. 모바일에서는 같은 두 열이 Bottom sheet로.

09:30
Time기본. 24시간 HH:MM
09:00
~
18:00
Range시작과 끝. 끝은 시작보다 뒤, 어긋나면 Error
2026.09.25
09:30
Date + time라벨 하나 아래 날짜 필드와 나란히. 간격 8

발송 시각

Bottom sheet640 미만. 같은 두 열, 확인 버튼으로 닫아요

States

시각 선택
Empty플레이스홀더
09:30
FilledHH:MM tabular
09:3
Typing직접 입력 · 콜론 자동
25:70

00:00~23:59 사이로

Error범위 밖 · Danger

Behavior

동작규칙
열기클릭하거나 포커스. 고른 값이 두 열 가운데에 오도록 스크롤해 둬요
고르기시를 고르면 분 열로 포커스, 분을 고르면 닫혀요. 둘 다 골라야 값이 바뀜
직접 입력숫자만 치면 콜론이 자동으로. 0930 → 09:30. 범위 밖이면 Error, 필드를 떠날 때 가장 가까운 5분으로
키보드↑↓로 열 안 이동, ←→로 열 바꾸기, Enter 확정, ESC 닫기
Range시작을 고르면 끝 필드가 열리고, 끝은 시작 이후만 고를 수 있어요
모바일640 미만에서 Bottom sheet에 같은 두 열. OS 기본 피커는 쓰지 않아요

Usage

09:30

평일 09:00~18:00에만 보내요

이렇게24시간 표기, 고를 수 있는 범위는 도움말에
오전 9시 30분
이건 피해요오전/오후 표기. 24시간 HH:MM으로
09시
30분
이건 피해요Select 두 개로 시와 분. 필드 하나에 두 열

Specification

부분크기글색여백
트리거높이 40 · radius 8Body S 14 tabularText 필드와 같음 · 아이콘 Label/Alternative좌우 14 · 아이콘과 8
메뉴폭 = 필드 · 높이 180 · radius 12—Background/Normal · Elevation Dropdown위 6 · 안쪽 4 · 열 사이 4
행36 · radius 8Body S 14 tabular · 고른 값 600Label/Body · 고른 값 Fill/Normal + Label/Title · 호버 Fill/Normal—
Range필드 둘 + ~~ Label/Alternative—사이 8

Props

Prop값기본설명
value · onChange'HH:MM' · fn—24시간 문자열
step1 · 5 · 10 · 15 · 305분 단위
min · max'HH:MM'—고를 수 있는 범위. 밖은 흐리게
rangebooleanfalsetrue면 [start, end] 값
label · help · errorstring—TextField와 같음

쓰기

<TimeField label="발송 시각" value={time} onChange={setTime} min="09:00" max="18:00" help="평일 09:00~18:00에만 보내요" /> <TimeField label="연락 가능 시간" range value={[from, to]} onChange={([f, t]) => { setFrom(f); setTo(t); }} /> {/* 날짜와 함께 */} <Field label="예약 발송"> <div className="grid grid-cols-[1.3fr_1fr] gap-2"><DateField … /><TimeField … /></div> </Field>

클래스

.time-trigger { @apply input flex items-center gap-2 tabular-nums cursor-pointer [&_svg]:w-4 [&_svg]:h-4 [&_svg]:text-label-alt; } .time-menu { @apply mt-1.5 flex gap-1 p-1 h-[180px] bg-white rounded-xl shadow-dropdown; } .time-col { @apply flex-1 overflow-y-auto snap-y scrollbar-none; } .time-item { @apply h-9 flex items-center justify-center rounded-lg text-sm tabular-nums text-label-body hover:bg-fill snap-center; } .time-item.is-selected { @apply bg-fill font-semibold text-label-title; } .time-item:disabled { @apply opacity-40; }

주의

항목설명
input type="time"브라우저마다 다르게 그려요. 트리거는 text로 두고 메뉴를 직접 그려요.
Select 둘시·분을 Select 둘로 나눈 화면이 있어요. TimeField 하나로.
시간대값은 KST 기준 문자열만. Date 객체로 넘기지 않아요.

Text field

한 줄 글을 받는 기본 입력칸. 라벨 위, 메시지 아래, 필드 안에는 앞 아이콘이나 단위, 지우기, 상태 아이콘이 들어갈 수 있어요. 형식이 있으면 입력하는 동안 맞춰 주고, 맞는지 틀린지는 필드를 떠날 때 말합니다.

rebound, admin은 이미 있는 아이디예요
8자 이상영문과 숫자 섞기특수문자 1개 이상
원

Anatomy

라벨, 필드, 메시지. 필드 안은 왼쪽에 아이콘이나 앞 단위, 가운데 글, 오른쪽 자리엔 지우기 · 상태 아이콘 · 뒤 단위 중 하나만.

master@groundbasic.com2
사용할 수 있는 이메일이에요
  1. 1라벨 · Label S 13 600 · 아래 6 · 필수 항목은 빨간 별표(*), 선택 항목은 표시 없음
  2. 2필드 · 높이 40 · Line/Normal 1px · radius 8 · 좌우 14 · 안쪽 요소 사이 8
  3. 3앞 아이콘 또는 앞 단위(₩) · 16 · Label/Alternative
  4. 4글 · Body S 14 · Label/Title · 플레이스홀더 Label/Alternative
  5. 5오른쪽 자리 · 하나만 · 포커스 중엔 지우기(원 18), 떠나면 상태 아이콘(채운 원 16 · 확인 중 스피너 · 통과 Success · 오류 Danger), 단위가 있으면 뒤 단위
  6. 6메시지 · Caption 12 · 위 6 · 도움말 Label/Alternative → 통과 Success · 오류 Danger · 앞에 채운 원 14. 한 자리에 하나

Variants

일곱. 대부분 Default고, 값에 단위가 있으면 Affix, 뜻이 아이콘으로 빨리 읽히면 Leading icon, 자주 지우면 Clearable, 필드 옆 행동이 붙으면 Action, 비밀번호는 Password, 인증 번호 6자리는 OTP.

Default라벨 + 필드. 플레이스홀더는 예시 값
원
%
Affix앞·뒤 단위. 글자와 8, Label/Alternative. 값은 오른쪽 정렬하지 않아요
Leading icon16 · Label/Alternative. 이메일, 전화, 검색처럼 뜻이 뻔한 것만
Clearable글자가 있을 때 지우기. 검색·필터처럼 자주 비우는 필드
Action필드 오른쪽에 Line 버튼(필드와 같은 선 · 높이). 회색 채움은 필드 옆에서 혼자 튀어요. 라벨 하나 아래, 간격 8
Password눈 아이콘으로 보기·숨기기. 붙여넣기 막지 않아요
OTP6칸 44×48(가운데 정렬 시 56) · 숫자만 · 한 자리 치면 다음 칸, Backspace는 이전 칸 · 6자리 붙여넣기 · 다 채우면 자동 제출 · 틀리면 6칸 Danger + 문구

Sizes

M · 40기본 · 14 · 좌우 14
L · 48로그인 · 온보딩 · 15 · 좌우 16 · radius 10

States

일곱. 포커스는 테두리만 파랗게, 통과는 초록 체크와 메시지, 오류는 빨간 테두리와 메시지. 확인 중엔 스피너.

DefaultLine/Normal
FocusInformation 테두리 1 · 그림자 없음
확인하는 중
Checking서버 확인 중 · 스피너 S
사용할 수 있어요
SuccessSuccess 체크 + 메시지
이미 쓰고 있는 아이디예요
ErrorDanger 테두리 · X · 메시지
DisabledBackground/Alternative · Label/Disable
Read-only테두리 없이 채움만 · 복사 가능

Field feedback

틀렸다고만 하지 말고 어떻게 고치면 되는지. 입력 중엔 빨갛게 하지 않고, 필드를 떠날 때 판정해요. 서버 확인은 입력을 멈춘 뒤 500ms. 직접 써 보세요.

영문 소문자, 숫자, 밑줄 4~20자
중복 확인형식은 떠날 때 · 중복은 500ms 뒤 서버 · 확인 중 스피너 → 체크/X. rebound, admin이 이미 있는 아이디
8자 이상영문과 숫자 섞기특수문자 1개 이상
규칙 체크리스트도움말 자리에 규칙. 타이핑하면서 채운 항목만 초록 체크. 빨간색은 제출 때만
일치 확인떠날 때 비교. 다르면 "서로 달라요", 같으면 체크. 앞 필드를 고치면 다시 판정
상황시점표시문구
형식 오류떠날 때(blur)Danger 테두리 + X + 메시지"영문 소문자, 숫자, 밑줄 4~20자" 처럼 고치는 법
서버 확인멈춘 뒤 500ms스피너 S + "확인하는 중" → 결과"사용할 수 있어요" · "이미 쓰고 있는 아이디예요. 다른 아이디를 골라 주세요"
규칙 여러 개입력 중 실시간체크리스트 · 채우면 Success규칙 하나에 한 줄, 셋까지. 못 채운 건 제출 때만 Danger
두 값 비교둘째 필드 떠날 때체크 또는 Danger"비밀번호가 서로 달라요"
고친 뒤다시 입력하는 순간오류 표시 지움떠날 때 다시 판정. 타이핑 중엔 조용히

Behavior

동작규칙
형식 맞춤전화번호 하이픈, 금액 콤마, 사업자번호 하이픈은 입력하는 동안 자동. 저장값은 숫자만
오른쪽 자리한 번에 하나. 포커스 중 글자가 있으면 지우기, 떠나면 상태 아이콘. 지우기를 누르면 비우고 포커스 유지. 뒤 단위가 있는 필드엔 지우기를 두지 않아요
비밀번호눈 아이콘으로 보기·숨기기. 붙여넣기와 자동 완성을 막지 않아요
단위₩처럼 앞, 원·%처럼 뒤. 값은 왼쪽 정렬 그대로
Enter폼 안이면 제출. 필드 하나만 있는 검색은 Search field
모바일16px 강제(확대 방지), inputmode로 숫자 키패드

Usage

이미 쓰고 있는 아이디예요. 다른 아이디를 골라 주세요
이렇게무엇이 왜 안 되고, 어떻게 하면 되는지
유효하지 않은 값입니다
이건 피해요"유효하지 않은 값". 고칠 방법이 없어요
8자 이상 입력하세요
이건 피해요타이핑 중에 빨간색. 다 치기 전엔 판정하지 않아요
원
이건 피해요콤마 없는 금액, 오른쪽 정렬. 8,400,000으로 자동, 왼쪽 정렬

Specification

부분크기글색 · 선여백
필드 M높이 40 · radius 8Body S 14Background/Normal · Line/Normal 1px좌우 14 · 안쪽 8
필드 L높이 48 · radius 10Body M 15같음좌우 16
앞 아이콘 · 단위16단위 14Label/Alternative글과 8
지우기원 18 · X 10—Line/Strong · 호버 Label/Alternative—
상태 아이콘채운 원 16 (메시지 앞 14) · 스피너 S 16—Success #008A1E · Danger #EB003B · 글리프 Static/White—
눈아이콘 16 · 히트 28—Label/Alternative → 호버 Label/Body오른쪽 -6
메시지—Caption 12Label/Alternative · Success · Danger위 6 · 아이콘 14와 6
규칙 목록체크 원 14Caption 12Line/Strong → Success 채움위 8 · 줄 사이 4
Focus · Error · Disabled · Read-only——Information 1px · Danger 1px · Background/Alternative + Label/Disable · Background/Alternative 선 없음—

Props

Prop값기본설명
label · helpstring—라벨 필수. help는 메시지 자리의 기본 글
status · messageidle · checking · ok · error · stringidle상태 아이콘과 메시지 색이 함께. message가 help를 대신해요
prefix · suffixstring—₩ · 원 · %
leadingIconLucideIcon—16
clearablebooleanfalse글자 있을 때 지우기
typetext · password · email · teltextpassword면 눈 아이콘이 붙어요
formatphone · biz · money—입력 중 자동. onChange엔 숫자만
rules{ label, test }[]—체크리스트. 셋까지
action{ label, onClick, disabled? }—오른쪽 Neutral 버튼
sizemd · lgmd40 · 48

쓰기

{/* 아이디 중복 확인: 훅이 500ms 디바운스와 상태를 관리 */} const idCheck = useAsyncCheck(id, checkIdAvailable, { delay: 500, validate: v => /^[a-z0-9_]{4,20}$/.test(v) || '영문 소문자, 숫자, 밑줄 4~20자' }); <TextField label="아이디" value={id} onChange={setId} status={idCheck.status} message={idCheck.message} help="영문 소문자, 숫자, 밑줄 4~20자" /> {/* 비밀번호 규칙 */} <TextField label="비밀번호" type="password" value={pw} onChange={setPw} rules={[ { label: '8자 이상', test: v => v.length >= 8 }, { label: '영문과 숫자 섞기', test: v => /[a-z]/i.test(v) && /\\d/.test(v) }, { label: '특수문자 1개 이상', test: v => /[^a-z0-9]/i.test(v) }, ]} /> {/* 두 값 비교: 둘째 필드 blur 때 */} <TextField label="비밀번호 확인" type="password" value={pw2} onChange={setPw2} onBlur={() => setPw2Status(pw2 === pw ? 'ok' : 'error')} status={pw2Status} message={pw2Status === 'error' ? '비밀번호가 서로 달라요' : undefined} /> <TextField label="원금" inputMode="numeric" format="money" suffix="원" value={amount} onChange={setAmount} /> <TextField label="휴대전화" format="phone" action={{ label: '인증번호', onClick: sendCode, disabled: !isPhone(phone) }} />

클래스

.field { @apply flex items-center gap-2 h-10 px-3.5 rounded-lg bg-white border border-line text-sm text-label-title focus-within:border-info transition-[border-color]; } .field input { @apply flex-1 min-w-0 bg-transparent outline-none placeholder:text-label-alt; } .field-lg { @apply h-12 px-4 text-[15px] rounded-[10px]; } .field-affix, .field-icon { @apply text-label-alt shrink-0 [&_svg]:w-4 [&_svg]:h-4; } .field-clear { @apply w-[18px] h-[18px] rounded-full bg-line-strong text-white flex items-center justify-center hover:bg-label-alt; } .field-eye { @apply -mr-1.5 p-1.5 rounded-md text-label-alt hover:text-label-body; } .field-status { @apply shrink-0 [&_svg]:w-4 [&_svg]:h-4; } .field-status.is-ok { @apply text-success; } .field-status.is-error { @apply text-danger; } .field.is-error { @apply border-danger focus-within:border-danger; } .field:has(input:disabled) { @apply bg-bg-alt text-label-disable border-line-alt; } .field:has(input:read-only) { @apply bg-bg-alt border-transparent; } .field-msg { @apply mt-1.5 flex items-center gap-1.5 text-xs text-label-alt [&_svg]:w-3.5 [&_svg]:h-3.5; } .field-msg.is-ok { @apply text-success; } .field-msg.is-error { @apply text-danger; } .field-rules { @apply mt-2 flex flex-col gap-1 text-xs text-label-alt; } .field-rule i { @apply inline-flex w-3.5 h-3.5 rounded-full border border-line-strong items-center justify-center; } .field-rule.is-ok { @apply text-success; } .field-rule.is-ok i { @apply bg-success border-success text-white; }

주의

항목설명
.input 클래스지금 제품의 .input은 input 요소 하나예요. 아이콘·단위·상태를 넣으려면 감싸는 .field가 필요해서 TextField 컴포넌트로 옮겨요.
회원가입 폼아이디 중복은 제출 때가 아니라 입력 중 확인으로. 비밀번호 규칙은 문장이 아니라 체크리스트로.
aria메시지에 id를 주고 input의 aria-describedby로 연결. 오류일 때 aria-invalid.

Checkbox

여러 개를 고르거나 하나를 켜고 끄는 네모 칸. 저장 버튼과 함께 쓰는 폼 안의 선택이에요. 바로 적용되면 Switch, 여럿 중 하나만 고르면 Radio. 켜진 색은 Information 파랑.

Anatomy

16 네모 칸과 체크, 오른쪽에 라벨. 설명이 있으면 라벨 아래. 라벨을 눌러도 바뀌어요.

  1. 1칸 · 16 · radius 4 · Line/Strong 1 → 켜지면 Information 채움
  2. 2체크 · 10 · Static/White · 선 3
  3. 3라벨 · Body S 14 · Label/Title · 필수면 뒤에 빨간 별표(*) Danger 600, 선택은 표시 없음
  4. 4설명 · Caption 12.5 · Label/Alternative · 위 2

Variants

다섯. 하나면 Single, 설명이 붙으면 With description, 필수 동의는 Agreement box, 여러 항목은 Group, 표 머리글처럼 전체 선택은 Indeterminate.

Single필터, 옵션 하나. 라벨은 켜진 상태의 이름
With description동의 항목처럼 설명이 필요할 때. 두 줄까지
Agreement box필수 동의. 회색 블록으로 감싸 해야 할 일로 보이게
Group세로로, 사이 12. 라벨 하나 아래 묶어요. 가로 배열은 셋 이하일 때만
채무자원금
김리바8,400,000
이바운3,200,000
Indeterminate일부만 골라졌을 때 머리글. 누르면 전체 선택 → 전체 해제

Sizes

M · 16기본 · 라벨 14
L · 20모바일 · 동의 화면 · 라벨 15

States

UncheckedLine/Strong 1
CheckedInformation
IndeterminateInformation · 가로선
Disabledopacity .4
동의해야 계속할 수 있어요
Error박스 전체 Danger 틴트 + 테두리 · 0.35초 흔들림

Behavior

동작규칙
토글칸, 라벨, 설명 어디를 눌러도. Space로도
전체 선택모두 켜져 있으면 체크, 일부면 Indeterminate, 없으면 빈 칸. Indeterminate를 누르면 전체 선택
필수라벨 뒤 빨간 별표(*), 선택은 표시 없음. 필수 동의는 Agreement box(회색 블록)로 감싸요. 제출할 때 안 켜져 있으면 박스 전체가 Error(연한 Danger 배경 + Danger 테두리 + 칸 Danger)가 되고 문구가 붙고 0.35초 좌우로 흔들려요. 그 박스로 스크롤·포커스. 켜는 순간 원래대로. 제출 버튼을 비활성으로 두지 않아요
저장폼의 저장 버튼으로 반영. 누르는 즉시 적용되어야 하면 Switch
Radio와여럿 중 하나만 고르면 Radio. 체크박스 여러 개를 배타적으로 만들지 않아요

Usage

이렇게세로 목록, 필수만 별표(*)
이건 피해요오렌지 체크. 선택 계열은 Information
이건 피해요하나만 고르는 걸 체크박스로. Radio
이건 피해요라벨 없는 칸. 표 안이라도 머리글에 이름이 있어야

Specification

부분크기글색여백
칸 M16 · radius 4 · 선 1—Line/Strong → 켜짐 Information · Error Danger라벨과 8
Agreement boxradius 8 · 선 1(투명)안쪽 14 16Fill/Alternative #F7F7F7 → Error Danger 5% 배경 + Danger 선문구 위 8 · 칸 뒤 24
칸 L20 · radius 5라벨 15같음라벨과 10
체크 · 가로선10 (L 14) · 선 3—Static/White—
라벨 · 설명—Body S 14 · Caption 12.5Label/Title · Label/Alternative설명 위 2
S (보조 옵션)16 · radius 4 · 선 1Caption 12.5 · Label/Body같음라벨과 8 · 제목 옆이나 컨트롤 아래 12
필수 별표(*)—라벨과 같은 크기 600Danger라벨과 2
Group———항목 사이 12 · 가로 배열 24

Props

Prop값기본설명
checked · onChangeboolean · fn——
indeterminatebooleanfalse일부 선택. checked보다 우선
label · descriptionReactNode · string—label 필수. 숨기면 aria-label
requiredbooleanfalse라벨 뒤에 빨간 별표(*). 선택은 표시 없음
sizemd · lgmd16 · 20
errorstring—Danger 테두리 + 아래 문구

쓰기

<Checkbox checked={agree.terms} onChange={v => set('terms', v)} required label="서비스 이용약관 동의" /> <Checkbox checked={agree.marketing} onChange={v => set('marketing', v)} label="마케팅 정보 수신 동의" description="상환 팁과 새 기능을 문자로 보내드려요" /> {/* 표 전체 선택 */} <Checkbox aria-label="전체 선택" checked={allSelected} indeterminate={someSelected} onChange={toggleAll} />

클래스

.checkbox { @apply peer sr-only; } /* 진짜 input은 숨기고 */ .checkbox-box { @apply inline-flex items-center justify-center w-4 h-4 rounded border border-line-strong bg-white shrink-0 peer-checked:bg-info peer-checked:border-info peer-indeterminate:bg-info peer-indeterminate:border-info peer-focus-visible:ring-2 peer-focus-visible:ring-info/30 peer-disabled:opacity-40; } .checkbox-box svg { @apply w-2.5 h-2.5 text-white opacity-0 peer-checked:opacity-100; } .checkbox-lg .checkbox-box { @apply w-5 h-5 rounded-[5px] [&_svg]:w-3.5 [&_svg]:h-3.5; } .checkbox-label { @apply text-sm text-label-title; } .checkbox-desc { @apply mt-0.5 text-[12.5px] text-label-alt; } .checkbox-req { @apply ml-1 text-[13px] font-medium text-danger; } .checkbox-opt { @apply ml-1 text-[13px] font-medium text-label-alt; } .checkbox-error .checkbox-box { @apply border-danger; }

주의

항목설명
네이티브 체크박스지금 동의 화면은 input type=checkbox 기본 모양(accent 오렌지)이에요. sr-only로 숨기고 box를 그려요.
label for라벨과 input을 label 요소로 감싸요. div onClick으로 만들면 키보드로 못 켜요.
IndeterminateHTML 속성이 아니라 JS 프로퍼티(el.indeterminate = true). ref로 넣어요.

Date picker

날짜를 고르는 필드. 닫혀 있을 땐 달력 아이콘이 붙은 필드, 열리면 한 달 달력. 머리의 연월을 누르면 달과 해를 한 번에 옮길 수 있어요. 오늘은 점, 고른 날은 오렌지. 기간이면 시작과 끝을 순서대로 눌러요. 직접 타이핑해도 됩니다.

2026.09.25
날짜 선택

Anatomy

트리거는 Select와 같고 앞에 달력 아이콘. 달력은 월 이동 머리, 요일 줄, 날짜 격자, 아래에 빠른 선택.

2026.09.25
2026년 9월
일월화수목금토
303112345678910111213141516174181920212223242552627282930123
  1. 1트리거 · Text 필드와 같음 · 달력 아이콘 16 · 값 YYYY.MM.DD tabular
  2. 2머리 · 연월 14 600 · 양쪽 이전·다음 달 아이콘 버튼 S
  3. 3요일 · Caption 11 · Label/Alternative · 일요일 시작
  4. 4오늘 · 아래 점 4 Primary · 다른 달 날짜는 Line/Strong
  5. 5고른 날 · 34 정사각 · Primary 채움 · radius 8 · Static/White 700
  6. 6빠른 선택 · Ghost S 버튼 · 위 Line/Alternative · 오늘, 이번 달 말처럼 자주 고르는 날

Variants

넷. 날짜 하나면 Single, 시작과 끝이면 Range, 달만 고르면 Month, 자주 고르는 날이 정해져 있으면 Presets.

2026.09.25
Single기본. 한 달 달력, 누르면 바로 닫혀요
2026.09.01
~
2026.09.30
Range시작을 누르고 끝을 누르면 사이가 옅게 칠해져요. 끝은 시작 이후만
2026
1월2월3월4월5월6월7월8월9월10월11월12월
Month정산 월처럼 달만 고를 때. 4×3 격자, 값 YYYY.MM
2026년 9월
14151617181920
Presets아래에 Ghost 버튼 셋까지. 조회 기간 필드에 잘 맞아요

States

날짜 칸의 상태. 오늘은 점으로만 표시하고, 고를 수 없는 날은 흐리게.

17
DefaultLabel/Title
17
HoverFill/Normal
17
Today아래 점 Primary
17
SelectedPrimary · 700
17
In rangePrimary 10%
17
DisabledLine/Strong · 취소선 없음

Behavior

동작규칙
열기클릭하거나 포커스. 값이 있으면 그 달, 없으면 이번 달을 보여요
고르기Single은 누르면 바로 닫혀요. Range는 시작 → 끝 두 번, 끝을 시작보다 앞에 누르면 새 시작으로
직접 입력숫자만 치면 점이 자동으로. 20260925 → 2026.09.25. 없는 날이면 Error
달·해 건너뛰기머리의 연월을 누르면 월 고르기(4×3 · 화살표는 해마다), 거기서 연도를 누르면 연도 고르기(4×3 · 화살표는 12년씩). 고르면 한 단계씩 되돌아와 날짜 고르기로 끝나요. 몇 해 전을 화살표로만 넘기게 두지 않아요. min·max 밖의 달·해는 Disabled, 오늘이 든 달·해에는 날짜와 같은 오렌지 점
범위min·max 밖은 Disabled. 과거를 못 고르는 필드는 오늘 이전을 흐리게, 숨기지 않아요
키보드←→ 하루, ↑↓ 한 주, PageUp·Down 한 달, Enter 확정, ESC 닫기
모바일640 미만에서 Bottom sheet에 같은 달력. OS 기본 피커는 쓰지 않아요
좁은 화면호버가 없으므로 눌러서 열고 바깥을 누르면 닫혀요. 아이콘 누르는 자리는 28, 말풍선은 화면 좌우에서 12를 띄우고 위가 모자라면 트리거 아래로 뒤집어요. 밀어 넣은 만큼 꼬리만 트리거 쪽으로 옮겨 어디서 나온 말풍선인지 보이게 해요
자리달력은 body에 포털로(fixed) 트리거 아래 6. 오른쪽이 모자라면 오른쪽 끝을 맞추고, 아래가 모자라면 위로 열어요. 값은 ISO 'YYYY-MM-DD'로 들고 화면엔 'YYYY.MM.DD'

Usage

2026.09.25

오늘 이후만 고를 수 있어요

이렇게YYYY.MM.DD, 고를 수 있는 범위는 도움말에
09/25/2026
이건 피해요월/일/년 표기. 항상 연.월.일
2026년
9월
25일
이건 피해요Select 셋으로 년·월·일. 필드 하나에 달력
14151617181920
이건 피해요오늘을 채움으로, 고른 날을 다른 색으로. 오늘은 점, 고른 날만 Primary

Specification

부분크기글색여백
트리거높이 40 · radius 8Body S 14 tabularText 필드와 같음 · 아이콘 Label/Alternative아이콘과 8
달력폭 280 · radius 12—Background/Normal · Elevation Dropdown위 6 · 안쪽 12
머리아이콘 버튼 S 28연월 14 600Label/Title아래 10
요일—Caption 11Label/Alternative아래 4
날짜 칸34 · radius 813 tabular · 고른 날 700Label/Title · 호버 Fill/Normal · 고른 날 Primary · 범위 Primary 10% · 다른 달·Disabled Line/Strong행 사이 2
월·연 칸4열 · 높이 40 · radius 813 tabular · 고른 것 700Label/Title · 호버 Fill/Normal · 고른 것 Primary 흰 글자 · 범위 밖 Label/Disable칸 사이 4
오늘점 4—Primary아래 4
빠른 선택Ghost S—위 Line/Alternative위 8 · 사이 6

Props

Prop값기본설명
value · onChange'YYYY.MM.DD' · fn—문자열. Date 객체는 넘기지 않아요
modedate · range · monthdaterange면 [start, end]
min · max'YYYY.MM.DD'—밖은 Disabled
presets{ label, value }[]—셋까지. 아래 Ghost 버튼
label · help · errorstring—TextField와 같음

쓰기

<DateField label="계약 시작일" value={start} onChange={setStart} min={today} help="오늘 이후만 고를 수 있어요" /> <DateField label="조회 기간" mode="range" value={[from, to]} onChange={([f, t]) => { setFrom(f); setTo(t); }} presets={[{ label: '오늘', value: [today, today] }, { label: '이번 달', value: [monthStart, monthEnd] }]} /> <DateField label="정산 월" mode="month" value={ym} onChange={setYm} />

클래스

.date-trigger { @apply input flex items-center gap-2 tabular-nums cursor-pointer [&_svg]:w-4 [&_svg]:h-4 [&_svg]:text-label-alt; } .calendar { @apply mt-1.5 w-[280px] p-3 bg-white rounded-xl shadow-dropdown; } .calendar-head{ @apply flex items-center justify-between px-1 pb-2.5 text-sm font-semibold text-label-title; } .calendar-week{ @apply grid grid-cols-7 mb-1 text-center text-[11px] text-label-alt; } .calendar-grid{ @apply grid grid-cols-7 gap-y-0.5; } .calendar-day { @apply relative h-[34px] flex items-center justify-center rounded-lg text-[13px] tabular-nums text-label-title hover:bg-fill; } .calendar-day.is-out, .calendar-day:disabled { @apply text-line-strong hover:bg-transparent; } .calendar-day.is-today::after { @apply content-[''] absolute bottom-1 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-primary; } .calendar-day.is-selected { @apply bg-primary text-white font-bold; } .calendar-day.is-in-range { @apply bg-primary/10 rounded-none; } .calendar-foot{ @apply flex gap-1.5 mt-2 pt-2.5 border-t border-line-alt; }

주의

항목설명
input type="date"브라우저마다 다르고 형식을 못 정해요. 트리거는 text, 달력은 직접.
MonthCalendar상환 일정 화면의 큰 달력은 이 컴포넌트가 아니에요. 날짜 칸 스타일(오늘 점, 고른 날 Primary)만 맞춰요.
시간대값은 KST 날짜 문자열. toISOString()으로 하루 밀리는 실수 주의.

Card

콘텐츠를 한 덩어리로 묶는 흰 면. 회색 페이지 위에 흰 카드를 놓아 색 대비만으로 구분하고, 테두리와 그림자는 쓰지 않습니다. 라운드는 12 하나.

계좌 인증

완료
1원 인증으로 확인된 출금 계좌예요.
회수 예정액12,340,000원전월 대비 +8.2%

Anatomy

컨테이너 안에 머리, 몸통, 발. 머리는 제목과 배지나 작은 행동, 발은 버튼이 오른쪽 정렬. 몸통만 있어도 카드예요.

주식회사 리바운드

검토 중2
위임 계약 검토가 진행 중이에요. 보통 1~2일 걸립니다.3
4
1
  1. 1컨테이너 · Background/Normal · radius 12 · 안쪽 여백 spacing-24
  2. 2머리 · Title S 16 Bold + 배지 또는 Ghost 버튼 · 아래 16
  3. 3몸통 · Body M · Label/Body
  4. 4발 · 버튼 오른쪽 정렬 · 간격 8 · 위 20

Variants

넷. 대부분은 Default이고, 지표는 KPI, 고르는 것은 Selectable, 흰 배경 위에 놓일 때만 Elevated.

채권 정보

채무자 3명 · 12,340,000원
Default흰 면, 테두리와 그림자 없음. 회색 페이지 위에서만 성립
연체7건확인 필요
KPI지표 하나. 라벨 12 · 값 26 Bold tabular · 델타 배지. 안쪽 여백 20
10일
매월
25일
매월
Selectable여럿 중 하나를 고르는 카드. 안 고른 건 회색 채움, 고르면 파란(Information) 테두리. 이름 + 한 줄 설명이 있는 폼 안 택1은 Choice card

안내

흰 배경 위에선 그림자로 띄워요.
Elevated흰 페이지 위에 카드가 놓일 때만. Elevation Card 그림자

Padding

안쪽 여백은 셋. 지표와 선택 카드는 20, 기본은 24, 온보딩과 로그인처럼 카드가 화면의 주인공이면 32.

20
S · 20KPI, 선택 카드
24
M · 24기본
32
L · 32온보딩, 로그인, 빈 화면

States

Selectable만 상태가 있습니다. 나머지 카드는 누를 수 없어요. 선택 상태는 파랑(Information)이에요 — 오렌지는 주 행동에만 쓰고, 오렌지 테두리로 선택을 표시하지 않아요.

10일
매월
DefaultFill/Normal
10일
매월
HoverFill/Strong
10일
매월
SelectedInformation 테두리 1 · Information 4% 채움
10일
매월
Disabledopacity .4

Composition

카드 안에서 나눌 때는 가로선, 묶을 때는 회색 블록. 카드 안에 카드를 넣지 않습니다.

출금 계좌

국민은행 123456-78-901234
예금주 홍다희
가로선같은 카드 안에서 항목을 나눌 때. Line/Alternative

신청 안내

서류 확인 후 1~2일 안에 결과를 알려드려요.
회색 블록안내, 스텝, 요약처럼 안에서 한 번 더 묶을 때. Background/Alternative

Placement

회색 페이지 위에 카드 사이 간격 16. 지표는 3열, 그 아래 본문 카드는 폭을 채웁니다.

회수 예정액12,340,000원
관리 중인 채무자148명
연체7건

최근 상환

표가 들어가는 자리

Usage

카드
회색 블록
이렇게안에서 한 번 더 묶을 때는 회색 블록
카드
카드 안의 카드
이건 피해요카드 안에 또 카드. 이중 여백이 생기고 면이 쪼개져요
테두리 + 그림자
이건 피해요테두리와 그림자를 같이. 회색 페이지 위에선 둘 다 필요 없어요
흰 위의 흰 카드
이건 피해요흰 페이지 위에 Default 카드. 보이지 않아요. 페이지를 회색으로 바꾸거나 Elevated로

Specification

Variant배경테두리 · 그림자Radius안쪽 여백
DefaultBackground/Normal없음1224 (S 20 · L 32)
KPIBackground/Normal없음1220 · 세로 간격 8
SelectableFill/Normal → 선택 시 Information 4%선택 시 Information/Normal 1px1220 · 택1은 Choice card
ElevatedBackground/NormalElevation Card1224
회색 블록Background/Alternative #F7F7F7없음1220

Props

Prop값기본설명
variantdefault · kpi · selectable · elevateddefault종류
paddingsm · md · lgmd20 · 24 · 32
title / actionReactNode—머리. action엔 배지나 Ghost 버튼
footerReactNode—발. 버튼은 오른쪽 정렬
selected · onSelectboolean · fn—selectable에서만

쓰기

<Card title="계좌 인증" action={<Badge tone="success">완료</Badge>} footer={<><Button variant="ghost">변경</Button><Button>내역 보기</Button></>}> 1원 인증으로 확인된 출금 계좌예요. </Card> <KpiCard label="회수 예정액" value={12340000} unit="원" delta={<Badge tone="success">전월 대비 +8.2%</Badge>} /> {/* 고르는 카드 */} {DAYS.map(d => ( <Card key={d} variant="selectable" padding="sm" selected={day === d} onSelect={() => setDay(d)}> <b>{d}일</b> 매월 </Card> ))} {/* 카드 안에서 한 번 더 묶을 때 */} <Card title="신청 안내"> <div className="card-block">서류 확인 후 1~2일 안에 결과를 알려드려요.</div> </Card>

클래스

.card { @apply bg-white rounded-xl p-6; } .card-sm { @apply p-5; } .card-lg { @apply p-8; } .card-elev { @apply shadow-card; } .card-pick { @apply bg-[#F0F0F0] p-5 border border-transparent cursor-pointer hover:bg-[#E4E4E4] transition-colors; } .card-pick.on{ @apply bg-info/[0.04] border-info; } .card-block { @apply bg-[#F7F7F7] rounded-xl p-5; } .kpi-card { @apply bg-white rounded-xl p-5 flex flex-col gap-2; }

주의

항목설명
rounded-2xlTailwind 설정에서 2xl을 12px로 덮어썼어요. xl과 같은 값이라 어느 쪽을 써도 12. 새 코드는 xl로.
border border-border회색 페이지 위 카드에 테두리를 두른 화면이 남아 있어요. 교체 대상.
선택 카드 링선택 표시는 Information 1px 테두리 + 4% 틴트만. 오렌지 테두리, 글로우, ring은 쓰지 않아요.

Table

채권자 화면의 중심. 세로선 없이 가로선만, 머리글은 회색 띠, 마지막 행의 아래선은 지웁니다. 모든 열은 왼쪽 정렬이고 숫자는 tabular로 자릿수를 맞춥니다.

채무자원금상환률상태최근 입금
김리바8,400,00062%정상 상환2026.09.10
이바운3,200,00018%연체 주의2026.08.25
박그라12,000,000100%종료2026.07.30

Anatomy

흰 컨테이너 안에 머리글 행과 본문 행, 그 사이의 가로선. 열 정렬은 아래 Alignment에서.

채무자원금상태
김리바8,400,000정상 상환
이바운3,200,000연체 주의
1 2 3
  1. 1컨테이너 · Card Default · radius 12
  2. 2머리글 행 · 44 · Fill/Normal · Label S 600
  3. 3본문 행 · 48 · Body M · 숫자는 tabular 600
  4. 4가로선 · Line/Alternative · 마지막 행은 없음

Variants

넷. 기본 표에 필요한 것만 더합니다. 행을 누르면 이동하는 표는 오른쪽 끝에 화살표를, 여러 개 고르는 표는 왼쪽에 체크박스를, 합이 중요하면 맨 아래 합계 행을.

채무자원금상태
김리바8,400,000정상 상환
이바운3,200,000연체 주의
Default머리글 + 본문. 누를 수 없는 정보 표
채무자원금상태
김리바8,400,000정상 상환
이바운3,200,000연체 주의
Row link행 전체가 상세로 가는 링크. 호버에 Background/Alternative, 끝에 chevron
채무자원금상태
김리바8,400,000정상 상환
이바운3,200,000연체 주의
Selectable여럿을 골라 한 번에 처리할 때. 체크는 Information 파랑, 고른 행은 Information 6% 채움
채무자원금회수
김리바8,400,0005,200,000
이바운3,200,000580,000
합계11,600,0005,780,000
Summary합계 행. Bold, 위에 Line/Normal

Head

머리글은 셋 중 하나. 제목만 있는 Text, 눌러서 정렬하는 Order, 조건을 고르는 Filter. 아이콘은 글자 오른쪽에 4 띄우고, 쓰는 중인 열만 진하게.

Text
채무자최근 입금
Order
원금상환률
Filter
상태담당자
종류표시동작
Text제목만기본. 대부분의 열
Orderchevrons-up-down 14 Line/Strong → 정렬 중 chevron-up · down Label/Title누를 때마다 오름 → 내림 → 해제. 한 번에 한 열만 정렬
Filterlist-filter 14 Line/Strong → 적용 중 Primary누르면 조건 메뉴(Select 메뉴와 같은 모양). 적용 중이면 아이콘만 Primary

Density

행 높이는 둘. 기본은 48이고, 한 화면에 많이 보여야 하는 관리 목록만 40.

채무자원금
김리바8,400,000
이바운3,200,000
M · 48기본 · 머리글 44 · Body M 15
채무자원금
김리바8,400,000
이바운3,200,000
S · 40많은 목록 · 머리글 38 · Body S 14

States

행의 상태. 호버는 Row link와 Selectable에만 있고, 정보 표의 행은 아무 반응이 없어요.

김리바840,000
DefaultBackground/Normal
김리바840,000
HoverBackground/Alternative
김리바840,000
SelectedInformation 6%
김리바840,000
Disabledopacity .4 · 종료된 건

Alignment

모든 열이 왼쪽. 열마다 정렬이 다르면 머리글과 본문이 어긋나 보여요. 숫자는 tabular 숫자로 자릿수를 세로로 맞추고, 단위는 머리글에 씁니다.

열정렬스타일예
이름 · 글왼쪽Body M · Label/Title 500김리바 · 주식회사 리바운드
금액 · 비율 · 건수왼쪽tabular · Label/Title 600 · 단위는 머리글에8,400,000 · 62%
날짜왼쪽tabular · Label/Alternative · 142026.09.10
상태왼쪽Badge Status정상 상환 · 연체 주의
행동오른쪽 끝Ghost 버튼 하나 또는 chevron. 유일한 예외상세 · ›

Empty · Pagination

결과가 없으면 표 자리에 한 문장과 되돌리는 버튼. 20건이 넘으면 아래에 페이지네이션, 무한 스크롤은 쓰지 않아요.

1 / 12 · 148건
Pagination32 정사각 Ghost, 현재 페이지만 Primary. 규칙은 Pagination 페이지에
채무자원금상태
조건에 맞는 채무자가 없어요.
Empty머리글은 남기고 본문 자리에 안내. 위아래 44

Responsive

좁은 화면에서는 열을 숨기지 않고 표를 카드 목록으로 바꿉니다. 한 행이 한 카드, 이름과 금액이 첫 줄.

채무자원금상태
김리바8,400,000정상 상환
이바운3,200,000연체 주의
박그라12,000,000종료
김리바8,400,000
정상 상환2026.09.10
이바운3,200,000
연체 주의2026.08.25
박그라12,000,000
종료2026.07.30

Usage

채무자원금상태
김리바8,400,000정상 상환
이바운3,200,000연체 주의
이렇게모든 열 왼쪽, 머리글은 회색 띠. 세로선 없이 가로선만
채무자원금상태
김리바8,400,000정상 상환
이바운3,200,000연체 주의
이건 피해요열마다 다른 정렬. 머리글과 본문이 어긋나 보여요
채무자원금상태
김리바8,400,000정상 상환
이바운3,200,000연체 주의
이건 피해요세로선. 가로선과 여백만으로 충분해요
채무자원금상태
김리바8,400,000정상 상환
이바운3,200,000연체 주의
박그라12,000,000종료
이건 피해요줄무늬 배경. 호버와 선택 색이 묻혀요

Specification

부분높이글배경 · 선여백
머리글 th44 (S 38)Label S 13 600 · Label/AlternativeFill/Normal · 아래 Line/Normal좌우 20 (S 16)
본문 td48 (S 40)Body M 15 · Label/BodyBackground/Normal · 아래 Line/Alternative좌우 20 (S 16)
숫자 열—tabular · Label/Title 600 · 왼쪽——
비교표 · 보조 줄—보조 줄 Caption 12 Label/Alternative플랜 비교처럼 모든 열이 짧은 값이면 전부 가운데 정렬 허용보조 정보(연이율 · 이자)는 폭이 모자라면 두 열에 나눠 담아요. 같은 사실을 두 번 적지 않아요 — 연이율은 기간 칸 아래, 이자 금액은 총액 칸 아래 한 번씩이고, 플랜 이름에 든 말(거치)을 보조 줄에 되풀이하지 않아요
Hover · Selected——Background/Alternative · Information 6%—
합계 행48Bold · Label/TitleBackground/Alternative · 위 Line/Normal좌우 20
Empty—Body S 14 · Label/Alternative · 가운데—위아래 44
Pagination32Label S 13 · 현재 PrimaryGhost · radius 8간격 4 · 위아래 14
감싸는 면——Card Default · radius 12 · overflow hidden0

Props

Prop값기본설명
columns{ key, title, width?, numeric?, render? }[]—열 정의. numeric이면 tabular 숫자와 600
columns[].sortable · filterboolean · { options }—머리글 Order · Filter. sort와 onSortChange, filters와 onFilterChange를 함께
rows · rowKeyT[] · keyof T—데이터와 키
densitymd · smmd행 48 · 40
onRowClick(row) => void—주면 Row link. 호버와 chevron이 붙어요
selectable · selected · onSelectboolean · Key[] · fn—체크박스 열
summaryPartial<T>—합계 행
emptyReactNode'데이터가 없어요.'결과 없을 때 본문 자리
pagination{ page, limit, total, onChange }—20건 초과 시. 표 아래에 붙어요

쓰기

<Table rows={debtors} rowKey="id" columns={[ { key: 'name', title: '채무자' }, { key: 'principal', title: '원금', numeric: true, render: r => won(r.principal) }, { key: 'rate', title: '상환률', numeric: true, render: r => `${r.rate}%` }, { key: 'status', title: '상태', render: r => <Badge tone={TONE[r.status]}>{LABEL[r.status]}</Badge> }, { key: 'lastPaid', title: '최근 입금', render: r => <Date value={r.lastPaid} /> }, ]} onRowClick={r => go(`/debtors/${r.id}`)} empty={<>조건에 맞는 채무자가 없어요.<Button variant="neutral" size="sm" onClick={reset}>필터 초기화</Button></>} pagination={{ page, limit: 20, total, onChange: setPage }} /> {/* 좁은 화면 */} <div className="hidden sm:block"><Table … /></div> <div className="sm:hidden">{debtors.map(d => <RowCard key={d.id} … />)}</div>

클래스

.table-wrapper { @apply bg-white rounded-xl overflow-hidden; } .th { @apply h-11 px-5 text-left text-[13px] font-semibold text-label-alt bg-fill border-b border-line whitespace-nowrap; } .td { @apply h-12 px-5 text-left text-[15px] text-label-body border-b border-line-alt whitespace-nowrap; } .th-sort { @apply cursor-pointer select-none [&_svg]:ml-1 [&_svg]:inline [&_svg]:text-line-strong; } .th-sort.is-active { @apply text-label-title [&_svg]:text-label-title; } .th-filter.is-active svg { @apply text-primary; } .td-num { @apply tabular-nums font-semibold text-label-title; } .td-name { @apply font-medium text-label-title; } .td-date { @apply tabular-nums text-sm text-label-alt; } .tr-link { @apply cursor-pointer hover:bg-bg-alt transition-colors; } .tr-selected { @apply bg-info/[.06]; } .row-check { @apply h-4 w-4 rounded border border-line-strong accent-info; } .tr-sum { @apply bg-bg-alt font-bold text-label-title border-t border-line; } .table-sm .th{ @apply h-[38px] px-4 text-xs; } .table-sm .td { @apply h-10 px-4 text-sm; } .table-wrapper tbody tr:last-child td { border-bottom: 0; }

주의

항목설명
text-center지금 제품의 .th .td는 전부 가운데 정렬이에요. 새 코드는 전부 왼쪽. 기존 화면은 페이지 단위로 바꿔요.
border-y.table-wrapper의 위아래 테두리는 빼고, 카드처럼 radius 12에 overflow hidden으로. 회색 페이지 위에서 흰 면으로 구분돼요.
Pagination 버튼테두리 버튼에서 Ghost로. 현재 페이지만 Primary, 나머지는 배경 없음.
colSpan 빈 행빈 상태는 <td colSpan>로 표 안에 넣어요. 표 밖에 따로 카드 만들지 않기.

Modal

흐름을 끊어야 할 때만 띄웁니다. 입력이 있으면 Dialog, 문서를 읽어야 하면 Sheet, 모바일에선 Bottom sheet. 확인만 받으면 Alert, 성공 알림은 Toast.

계좌 등록

상환금을 받을 계좌를 등록해 주세요

국민은행

Anatomy

어두운 스크림 위에 흰 컨테이너. 안은 머리, 몸통, 발이고 닫기는 머리 오른쪽 끝.

계좌 등록

상환금을 받을 계좌를 등록해 주세요

3
국민은행
4
5
2
1
  1. 1스크림 · Static/Black 40% · 누르면 닫힘
  2. 2컨테이너 · Background/Normal · radius 12 · Elevation Modal
  3. 3머리 · Title M 18 800 + 설명 Body S · 닫기 X 18
  4. 4몸통 · 필드 사이 16
  5. 5발 · 버튼 오른쪽 · 취소 Neutral 다음 주 행동

Variants

셋. 무엇을 담는지에 따라 폭과 구조가 정해집니다. 모바일에서는 Dialog와 Sheet가 아래에서 올라오는 Bottom sheet가 돼요.

계좌 등록

상환금을 받을 계좌예요

국민은행
Dialog짧은 입력. 필드 넷 이하, 넘으면 페이지로
위임 계약서
Sheet문서와 동의서. 머리와 발은 고정, 몸통만 스크롤. 높이 최대 80vh

정렬

최근 등록순
금액 높은순
Bottom sheet모바일. 아래에서 올라오고 위 모서리만 16. 손잡이 바 있음

Sizes

폭은 둘로 고정. 내용에 맞춰 늘리지 않고 둘 중 하나를 고릅니다. 확인만 받는 Alert는 360. 좌우 여백은 항상 16 이상.

Dialog

M · 448Dialog · 기본

Sheet

L · 560Sheet · 문서

Footer

버튼은 오른쪽, 둘까지. 취소가 왼쪽, 주 행동이 오른쪽. Sheet는 폭을 채우는 버튼 하나. 되돌릴 수 없는 결정은 Alert에서.

기본Neutral + Primary
SheetPrimary 하나 · 폭 100% · 위 Line/Normal
단일답이 하나면 파랑(Information) 글자 버튼 하나 · 안내만 할 때는 Alert Acknowledge

Behavior

동작규칙
열기가운데에서 96% → 100%로 150ms. 스크림은 같은 시간에 페이드. 첫 필드에 포커스
닫기ESC, 스크림 클릭, X, 취소. 넷 다 같은 결과. 입력 중이면 "작성 중인 내용이 사라져요" Alert 한 번
뒤 화면스크롤 잠금. 포커스는 모달 안에서만 돌아요. 닫히면 열었던 버튼으로 돌아감
겹침한 번에 하나. 모달 위에 뜰 수 있는 건 Alert만
긴 내용Sheet로. 머리와 발 고정, 몸통 스크롤, 최대 80vh. 그래도 길면 페이지로
모바일640 미만에서 Bottom sheet. 좌우 여백 0, 위 모서리 16

Usage

계좌 등록

상환금을 받을 계좌예요

국민은행
이렇게제목은 무슨 일이 일어나는지, 버튼은 그 행동의 이름

채무자 등록

이건 피해요필드 넷 넘는 폼. 페이지로 옮겨요

저장되었습니다

이건 피해요성공 알림을 모달로. 토스트 한 줄로 충분해요

계좌 등록

정말 닫을까요?

이건 피해요모달 위에 모달. 지금 모달의 내용을 바꿔서 물어요

Specification

부분크기글색 · 그림자여백
스크림화면 전체—Static/Black 40%좌우 16
컨테이너448 · 560 · radius 12—Background/Normal · Elevation Modal—
머리—Title M 18 800 · 설명 Body S 14 Label/Alternative—24 24 8 · 설명 위 4
닫기아이콘 18 · 히트 28—Label/Alternative → 호버 Label/Title + Fill/Normal머리 오른쪽 끝
몸통—Body S 14—16 24 0 · 항목 사이 16
발버튼 M 36——20 24 24 · 버튼 사이 8
Sheet 머리 · 발—Title S 16 700아래 · 위 Line/Normal16 24 · 발 버튼 full
Bottom sheet폭 100% · 위 radius 16—손잡이 36×4 Line/Normal손잡이 위 8

Props

Prop값기본설명
open · onCloseboolean · fn—ESC, 스크림, X가 모두 onClose를 불러요
variantdialog · sheetdialog폭과 구조. 모바일에선 자동으로 bottom sheet. 확인만 받으면 AlertDialog
title · descriptionstring—title은 필수. aria-labelledby로 연결
footerReactNode—버튼 둘까지. 취소가 먼저
dirtybooleanfalsetrue면 닫을 때 한 번 더 물어요
closeOnScrimbooleantrue결제처럼 중간에 끊으면 안 될 때만 false

쓰기

<Modal open={open} onClose={close} title="계좌 등록" description="상환금을 받을 계좌를 등록해 주세요" footer={<><Button variant="neutral" onClick={close}>취소</Button><Button onClick={submit}>등록</Button></>}> <SelectField label="은행" … /> <TextField label="계좌번호" placeholder="- 없이 숫자만" … /> </Modal> {/* 문서 */} <Modal variant="sheet" open={doc} onClose={…} title="위임 계약서" footer={<Button full onClick={agree}>동의합니다</Button>}> <article className="consent-document" dangerouslySetInnerHTML={{ __html }} /> </Modal>

클래스

.modal-scrim { @apply fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4; } .modal { @apply w-full bg-white rounded-xl shadow-modal; } .modal-md { @apply max-w-[448px]; } .modal-lg { @apply max-w-[560px] max-h-[80vh] flex flex-col; } .modal-head { @apply flex items-start justify-between px-6 pt-6 pb-2; } .modal-title { @apply text-lg font-extrabold tracking-tight text-label-title; } .modal-desc { @apply mt-1 text-sm text-label-alt; } .modal-close { @apply -m-1 p-1 rounded-lg text-label-alt hover:text-label-title hover:bg-fill; } .modal-body { @apply px-6 pt-4 space-y-4; } .modal-foot { @apply flex justify-end gap-2 px-6 pt-5 pb-6; } .sheet-head { @apply flex items-center justify-between px-6 py-4 border-b border-line; } .sheet-body { @apply flex-1 overflow-y-auto overscroll-contain px-6 py-4; } .sheet-foot { @apply px-6 py-4 border-t border-line; } @media (max-width: 639px) { .modal-scrim { @apply items-end px-0; } .modal { @apply max-w-none rounded-b-none rounded-t-2xl; } }

주의

항목설명
모달마다 다른 뼈대지금은 모달 컴포넌트가 여덟 개 각자 div를 짜요. Modal 하나로 모으고 내용만 넘겨요.
확인만 받을 때필드 없이 묻기만 하면 Modal이 아니라 AlertDialog. 폭 360, 스크림 클릭으로 닫히지 않아요.
btn-outline취소 버튼의 테두리 스타일은 Neutral(회색 채움)로.
shadow-xlTailwind 기본 그림자 대신 Elevation의 shadow-modal 토큰.
ESC · 스크롤 잠금useEffect로 매번 쓰던 코드는 Modal 안으로. 페이지에서는 신경 쓰지 않아요.

Alert

지금 결정을 받아야 할 때 띄우는 작은 대화상자. 제목은 질문, 버튼은 그 행동의 이름. 버튼 모양은 둘 — 되돌릴 수 없는 실행은 채움형(취소 Neutral + Danger), 그 밖의 확인·알림은 글자형(취소 회색 글자 + 파랑 글자). 닫기 X 없음, 스크림 클릭으로 닫히지 않음. 알리기만 할 거면 Toast.

다음 단계로 갈까요?

입력한 내용은 저장돼요. 다음 단계에서 서류를 올려요.

채권을 삭제할까요?

삭제하면 상환 이력도 함께 지워져요. 되돌릴 수 없어요.

글자형보통의 확인 · 알림 — 취소 회색 글자 + 파랑 글자
채움형되돌릴 수 없는 실행 — 취소 Neutral + Danger

Anatomy

스크림 위 폭 360의 흰 컨테이너. 제목과 설명, 그 아래 오른쪽에 버튼 둘. 닫기 X는 없고 버튼으로만 답합니다. 버튼 모양은 둘 중 하나 — 되돌릴 수 없는 실행은 채움형(취소 Neutral + Danger), 그 밖의 확인·알림은 글자형(취소 회색 글자 + 파랑 글자). 그림은 채움형.

채권을 삭제할까요?

삭제하면 상환 이력도 함께 지워져요.

2
1
  1. 1스크림 · Static/Black 40% · 눌러도 닫히지 않아요
  2. 2컨테이너 · 폭 360 · radius 12 · Elevation Modal · 안쪽 24
  3. 3제목 · Title S 16 700 · 질문형 · 한 줄
  4. 4설명 · Body S 14 · Label/Alternative · 두 줄까지 · 위 4
  5. 5취소 · 항상 왼쪽 · 라벨은 "취소" · 글자형은 회색 글자(Ghost, Label/Body) · 채움형은 Neutral
  6. 6주 행동 · 행동의 이름 · 글자형은 Information 파랑 글자 700 · 채움형(되돌릴 수 없는 실행)은 Danger 채움 · 답이 하나면 파랑 글자 하나

Variants

넷. 주 행동이 무엇인지에 따라 버튼 모양이 정해져요 — 보통의 확인·알림은 글자형, 되돌릴 수 없는 실행은 채움형. 답이 하나면 버튼도 하나. 라벨이 길거나 모바일이면 세로로 쌓아요.

변경 사항을 저장할까요?

저장하지 않으면 입력한 내용이 사라져요.

Confirm글자형. 계속할지 묻기 — 취소 회색 글자 + 파랑 글자

결제 수단이 만료됐어요

다음 자동이체 전에 카드를 다시 등록해 주세요.

Acknowledge글자형. 꼭 읽어야 하는 알림 — 파랑 글자 하나. 흘려도 되면 Toast

채권을 삭제할까요?

삭제하면 상환 이력도 함께 지워져요.

Destructive채움형. 되돌릴 수 없는 일 — 취소 Neutral + Danger. 포커스는 취소에

위임 계약을 해지할까요?

진행 중인 회수도 함께 멈춰요.

Stacked라벨이 길거나 모바일. 주 행동이 위, 폭 100%. 모양은 그대로 — 그림은 채움형

Buttons

둘까지. 왼쪽이 취소, 오른쪽이 주 행동. 라벨은 "예/아니오"가 아니라 행동의 이름이고, 취소는 늘 "취소". 모양은 둘 — 글자형은 취소 회색 글자(Ghost, Label/Body) + 주 행동 Information 파랑 글자 700. 보통의 확인과 알림에 쓰고, 답이 하나뿐이면 파랑 글자 하나. 채움형은 취소 Neutral + 주 행동 Danger 채움. 되돌릴 수 없는 실행(삭제 · 탈퇴 · 내보내기)에만 써요. 채운 Primary(오렌지)는 Alert에 쓰지 않아요 — 그건 Modal과 페이지의 몫.

Confirm · 글자형취소 회색 글자 + Information 글자
Acknowledge · 글자형Information 글자 하나
Destructive · 채움형취소 Neutral + Danger 채움

Behavior

동작규칙
열기가운데에서 96% → 100%로 150ms. 열리면 포커스는 Confirm에선 주 행동, Destructive에선 취소
닫기버튼으로만. ESC는 취소와 같고, 스크림을 눌러도 닫히지 않아요. 실수로 결정하지 않게
뒤 화면스크롤 잠금, 포커스 트랩. 닫히면 열었던 곳으로 돌아감
겹침Modal 위에 뜰 수 있는 유일한 것. Alert 위에 Alert는 없어요
처리 중주 행동을 누르면 그 버튼만 loading, 취소는 비활성. 끝나면 닫히고 Toast
모바일가운데 그대로, 폭은 화면 − 32. Bottom sheet로 바꾸지 않아요. 라벨이 길면 Stacked

Usage

계좌를 삭제할까요?

등록된 자동이체도 함께 해지돼요.

이렇게되돌릴 수 없는 실행은 채움형. 제목은 질문, 설명은 결과, 버튼은 행동의 이름

다음 단계로 갈까요?

입력한 내용은 저장돼요.

이렇게보통의 확인은 글자형. 취소 회색 글자 + 파랑 글자

메일을 보냈어요

받은 편지함에서 재설정 링크를 눌러 주세요.

이렇게답이 하나면 글자형 — 파랑 글자 버튼 하나

채권을 삭제할까요?

삭제하면 상환 이력도 함께 지워져요.

이건 피해요되돌릴 수 없는 실행인데 글자 버튼 — 채움형으로

다음 단계로 갈까요?

입력한 내용은 저장돼요.

이건 피해요보통의 확인에 채운 Primary. Alert에 오렌지는 없어요 — 글자형으로

알림

정말 진행하시겠습니까?

이건 피해요"알림", "예/아니오". 무엇을 하는지 알 수 없어요

저장되었습니다

이건 피해요끝난 일을 Alert로. Toast 한 줄이면 충분해요

계약을 해지할까요?

해지하면 진행 중인 회수 절차가 모두 중단되고, 채무자에게 발송된 안내도 취소되며, 이미 납부된 금액의 정산은 별도 문의가 필요하고…

이건 피해요설명 세 줄 넘게. 읽어야 할 게 많으면 Modal Sheet

Specification

부분크기글색 · 그림자여백
스크림화면 전체—Static/Black 40%좌우 16
컨테이너폭 360 · radius 12—Background/Normal · Elevation Modal24 24 24
제목—Title S 16 700 · 행간 1.35Label/Title—
설명—Body S 14 · 행간 1.5 · 두 줄까지Label/Alternative위 4
버튼M 36 · 둘까지Label M 13 · 취소 500(글자형) · 600(채움형) · 주 행동 700글자형 · 취소 Ghost Label/Body + 주 행동 Information 700 글자 / 채움형(되돌릴 수 없는 실행) · 취소 Neutral Fill/Normal + 주 행동 Danger 채움 흰 글자 · Primary 채움 없음위 20 · 사이 8 · 오른쪽 정렬
Stacked버튼 폭 100%—주 행동이 위사이 8
모바일폭 화면 − 32——가운데

Props

Prop값기본설명
open · onCloseboolean · fn—onClose는 취소와 ESC에서만. 스크림 클릭은 무시
titlestring—필수. 질문형 한 줄
descriptionstring—두 줄까지
confirm{ label, onClick, tone?: 'danger', loading? }—주 행동 — 기본은 파랑 글자 버튼. tone이 danger면 채움형 Danger 버튼이 되고 포커스가 취소로 가요
cancel{ label } · false{ label: '취소' }false면 Acknowledge. 파랑 글자 버튼 하나만 그려져요
appearance'text' · 'filled'auto버튼 모양. 생략하면 confirm.tone이 danger일 때 filled, 그 밖엔 text. filled = 취소 Neutral + Danger(되돌릴 수 없는 실행) · text = 취소 회색 글자 + 파랑 글자(확인 · 알림)
stackedbooleanauto라벨 합이 길거나 640 미만이면 자동으로 세로

쓰기

<AlertDialog open={ask} onClose={() => setAsk(false)} title="채권을 삭제할까요?" description="삭제하면 상환 이력도 함께 지워져요. 되돌릴 수 없어요." confirm={{ label: '삭제', tone: 'danger', onClick: remove, loading: removing }} /> {/* tone danger → 채움형(취소 Neutral + Danger) */} {/* 확인만 — 글자형, 파랑 글자 하나 */} <AlertDialog open={expired} onClose={ack} title="결제 수단이 만료됐어요" description="다음 자동이체 전에 카드를 다시 등록해 주세요." cancel={false} confirm={{ label: '확인', onClick: ack }} /> {/* 훅으로 쓰면 더 편해요 */} const ok = await confirm({ title: '변경 사항을 저장할까요?', confirm: { label: '저장' } }); if (ok) save();

클래스

.alert-scrim { @apply fixed inset-0 z-[60] flex items-center justify-center bg-black/40 px-4; } .alert { @apply w-full max-w-[360px] bg-white rounded-xl shadow-modal p-6; } .alert-title { @apply text-base font-bold leading-snug text-label-title; } .alert-desc { @apply mt-1 text-sm leading-normal text-label-alt; } .alert-foot { @apply mt-5 flex justify-end gap-2; } .alert-btn { @apply h-9 px-2.5 rounded-lg bg-transparent text-[13px] font-medium text-label-body hover:bg-fill hover:text-label-title; } /* 글자형 취소 · 회색 글자 */ .alert-btn.is-primary { @apply font-bold text-info hover:bg-info/[.06] hover:text-info; } /* 글자형 주 행동 · 파랑 글자 */ .alert-foot.is-filled .alert-btn { @apply px-4 bg-fill font-semibold text-label-title hover:bg-fill-strong; } /* 채움형 취소 · Neutral */ .alert-foot.is-filled .alert-btn.is-danger { @apply font-bold bg-danger text-white hover:opacity-90; } /* 채움형 주 행동 · Danger 채움(되돌릴 수 없는 실행) */ .alert-foot.is-stacked { @apply flex-col-reverse; } .alert-foot.is-stacked > * { @apply w-full; }

주의

항목설명
window.confirm브라우저 confirm()이 몇 곳 남아 있어요. 전부 AlertDialog로.
Modal과 구분입력 필드가 하나라도 있으면 Modal Dialog. Alert에는 필드가 없어요.
rolerole="alertdialog", aria-labelledby로 제목, aria-describedby로 설명.
취소 라벨"닫기", "아니오", "나중에"를 쓰지 않아요. 늘 "취소". 취소가 어울리지 않으면 Acknowledge.
버튼 모양되돌릴 수 없는 실행은 채움형(취소 btn-neutral + btn-danger), 그 밖의 확인·알림은 글자형(취소 btn-ghost + btn-ghost info). 섞지 않아요 — 삭제를 글자 버튼으로 묻지 않고, 보통의 확인을 채우지 않아요. btn-primary(오렌지)는 Alert 발에 없어요.

Banner

화면 안에 남아서 지금 알아야 할 것을 말하는 띠. 정보, 성공, 주의, 위험 네 가지 톤이고 흐름을 막지 않아요. 답을 받아야 하면 Alert, 잠깐 알리면 Toast.

9월 25일에 자동이체가 실행돼요

출금 계좌 잔액을 미리 확인해 주세요.

서류 2건이 아직 확인되지 않았어요

계약 검토가 끝나려면 사업자등록증과 통장 사본이 필요해요.

Anatomy

톤 색이 옅게 깔린 컨테이너에 아이콘, 제목, 설명. 필요하면 아래에 행동 버튼, 오른쪽 위에 닫기.

서류 2건이 아직 확인되지 않았어요

계약 검토가 끝나려면 통장 사본이 필요해요.

1
  1. 1컨테이너 · 톤 색 8% 채움 · radius 12 · 안쪽 여백 16 · 테두리 없음
  2. 2아이콘 · 20 · 톤 색 · 정보 info, 성공 circle-check, 주의 triangle-alert, 위험 circle-x
  3. 3제목 · Label M 14 600 · Label/Title · 한 줄
  4. 4설명 · Body S 13 · Label/Body · 위 2 · 두 줄까지
  5. 5행동 · Neutral S 버튼 · 하나만 · 위 10
  6. 6닫기 · X 18 · Label/Alternative · 정보와 성공에만

Variants

톤 넷. 톤은 상황이 정하고, 색은 배지의 System 색과 같습니다. 그냥 알려주면 정보, 잘 됐으면 성공, 곧 문제가 되면 주의, 지금 막혔으면 위험.

9월 25일에 자동이체가 실행돼요

출금 계좌 잔액을 미리 확인해 주세요.

Information알아두면 좋은 것. 닫을 수 있어요
계좌 인증이 끝났어요

이제 상환금을 이 계좌로 받을 수 있어요.

Success끝난 일. 한 화면에서만 보이고, 잠깐이면 토스트로
서류 2건이 아직 확인되지 않았어요

계약 검토가 끝나려면 통장 사본이 필요해요.

Warning곧 문제가 될 것. 해결하면 사라지고 닫기는 없어요
자동이체가 실행되지 않았어요

잔액이 부족해요. 입금 후 다시 시도해 주세요.

Danger지금 막힌 것. 원인과 다음 행동을 함께

Layout

내용 양에 따라 셋. 제목만 있으면 한 줄, 설명이 붙으면 두 줄, 할 일이 있으면 버튼까지. 폭은 놓인 곳을 가득 채워요.

한 줄
9월 25일에 자동이체가 실행돼요
두 줄
9월 25일에 자동이체가 실행돼요

출금 계좌 잔액을 미리 확인해 주세요.

행동 포함
서류 2건이 아직 확인되지 않았어요

계약 검토가 끝나려면 통장 사본이 필요해요.

Placement

페이지 제목 바로 아래, 또는 관련 있는 카드의 맨 위. 폼의 서버 오류는 폼 위에. 화면에 하나만 두고 스크롤을 따라오지 않아요.

대시보드
서류 2건이 아직 확인되지 않았어요
회수 예정액12,340,000
채무자148
연체7
페이지 상단제목 아래, 콘텐츠 폭 그대로. 카드와 12

출금 계좌

계좌 확인에 실패했어요

계좌번호를 다시 확인해 주세요.

110-123-45678X
카드 안머리 아래, 몸통 맨 위. 관련 필드는 함께 Danger

Behavior

동작규칙
닫기정보와 성공만 닫을 수 있어요. 주의와 위험은 원인이 해결되면 저절로 사라져요
사라짐시간이 지나도 사라지지 않아요. 몇 초 뒤 사라져야 하면 Toast
여러 개한 영역에 하나. 둘 이상이면 가장 급한 톤 하나만 보이고 나머지는 그 안에 목록으로
위치놓인 자리에 고정. 스크롤을 따라오는 띠는 쓰지 않아요
확인 필요사용자의 답이 필요하면 Banner가 아니라 Alert. 이 컴포넌트는 묻지 않아요

Usage

자동이체가 실행되지 않았어요

잔액이 부족해요. 입금 후 다시 시도해 주세요.

이렇게무슨 일이, 왜, 그래서 무엇을 하면 되는지
오류가 발생했습니다

다시 시도해 주세요.

이건 피해요"오류가 발생했습니다". 무엇이 왜 안 됐는지 없어요
자동이체가 25일에 실행돼요
서류 2건 확인 필요
계좌 인증 완료
이건 피해요여러 개 쌓기. 급한 것 하나만
이번 달 회수율이 92%로 올랐어요!
이건 피해요강조하려고 위험 톤. 톤은 상황이 정해요

Specification

부분크기글색여백
컨테이너폭 100% · radius 12—톤 색 8% (Warning 14%)16 · 아이콘과 10
아이콘20—Information #2768FF · Success #008A1E · Warning #FFB724 · Danger #EB003B—
제목—Label M 14 600 · 행간 20Label/Title—
설명—Body S 13 · 행간 20Label/Body위 2
행동버튼 S 32—Neutral위 10 · 한 줄형은 오른쪽 끝 Ghost
닫기18 · 히트 28—Label/Alternative오른콝 위
한 줄형높이 52제목만—세로 가운데

Props

Prop값기본설명
toneinfo · success · warning · dangerinfo색과 아이콘이 함께 정해져요
titlestring—필수. 한 줄
descriptionstring—두 줄까지
action{ label, onClick }—하나만. Neutral S로 그려져요
onClosefn—주면 닫기 버튼. info와 success에서만

쓰기

<Banner tone="info" title="9월 25일에 자동이체가 실행돼요" description="출금 계좌 잔액을 미리 확인해 주세요." onClose={dismiss} /> <Banner tone="warning" title="서류 2건이 아직 확인되지 않았어요" description="계약 검토가 끝나려면 통장 사본이 필요해요." action={{ label: '서류 올리기', onClick: openUpload }} /> {/* 폼의 서버 오류 */} {serverError && <Banner tone="danger" title="계좌 확인에 실패했어요" description={serverError} />} {/* 답이 필요하면 Banner가 아니라 AlertDialog */} <AlertDialog title="채권을 삭제할까요?" … />

클래스

.banner { @apply flex items-start gap-2.5 w-full p-4 rounded-xl; } .banner-info { @apply bg-info/[.08] [&_.banner-icon]:text-info; } .banner-success{ @apply bg-success/[.08] [&_.banner-icon]:text-success; } .banner-warning{ @apply bg-warning/[.14] [&_.banner-icon]:text-warning; } .banner-danger { @apply bg-danger/[.07] [&_.banner-icon]:text-danger; } .banner-icon { @apply w-5 h-5 shrink-0; } .banner-title { @apply block text-sm font-semibold leading-5 text-label-title; } .banner-desc { @apply mt-0.5 text-[13px] leading-5 text-label-body; } .banner-action { @apply mt-2.5 flex gap-2; } .banner-close { @apply -mt-1 -mr-1.5 p-1 rounded-lg text-label-alt hover:text-label-title hover:bg-black/[.04]; }

주의

항목설명
bg-red-50 조각지금 제품엔 text-status-bad와 bg-red-50을 조합한 오류 문구가 화면마다 달라요. Banner tone="danger" 하나로.
roledanger와 warning은 role="alert", info와 success는 role="status". 스크린리더가 바로 읽어요.
AlertDialog확인을 받는 대화상자는 이 컴포넌트가 아니에요. Alert 페이지.

Tooltip

요소 위에 잠깐 뜨는 한 줄 설명. 어두운 회색 말풍선에 흰 글자. 마우스를 올리거나 포커스하면 나타나고, 없어도 쓸 수 있어야 해요. 꼭 알아야 하는 건 툴팁이 아니라 화면에 씁니다.

지난달 같은 날 기준 회수액과 비교해요
권한 관리
관리자만 볼 수 있어요

Anatomy

트리거와 그 위에 뜨는 어두운 말풍선. 글 한 줄과 트리거를 가리키는 꼬리.

지난달 같은 날 기준 회수액과 비교해요2
  1. 1트리거 · 아이콘 버튼, 잠긴 항목, 잘린 글. 없어도 쓸 수 있는 것에만
  2. 2컨테이너 · Label/Body #555 채움 · radius 6 · 6 10 · 최대 폭 240 · 그림자 없음
  3. 3글 · Caption 12 500 · Static/White · 한 줄, 길면 두 줄까지
  4. 4꼬리 · 6 · 트리거 가운데를 가리킴 · 트리거와 8

Variants

넷. 대부분 Plain이고, 제목이 필요하면 Rich. 트리거가 무엇인지에 따라 Help와 Locked.

최근 3개월 상환 내역을 내려받아요
Plain한 줄. 버튼이 하는 일을 조금 더 설명할 때
회수율이번 달 회수액을 상환 예정액으로 나눈 값이에요. 연체분은 빼요.
Rich제목 + 설명 두 줄. 폭 240, 이보다 길면 도움말 페이지로
매월 25일
Help라벨 옆 도움 아이콘 14. 라벨 자체는 짧게 두고 설명은 툴팁으로
권한 관리
관리자만 볼 수 있어요
Locked비활성인 이유. 흐린 항목과 버튼에 호버하면 오른쪽에

Placement

기본은 위. 공간이 없으면 아래, 사이드바처럼 옆으로 열릴 자리면 오른쪽. 꼬리는 항상 트리거 가운데.

위 · 기본Top기본
아래Bottom위에 공간이 없을 때
오른쪽Right사이드바 항목
왼쪽Left오른쪽 끝 요소

Behavior

동작규칙
나타남마우스 올리고 300ms 뒤. 키보드 포커스는 바로. 100ms 페이드
사라짐마우스가 떠나면 바로. ESC로도. 툴팁 위로 마우스를 옮겨도 유지되지 않아요(안에 누를 게 없으니)
모바일호버가 없어서 탭하면 1.5초 보이고 사라져요. 중요한 설명이면 툴팁 대신 화면에
내용글만. 링크, 버튼, 이미지는 넣지 않아요. 그게 필요하면 Popover(나중에)
비활성 트리거disabled 버튼은 이벤트가 없어서 감싸는 span에 툴팁을 붙여요
자리말풍선은 body에 포털로 띄워요(position fixed). 카드·아코디언의 overflow나 흔들림(transform)에 잘려 꼬리만 남는 일이 없게. 화면 오른쪽 끝 가까우면 오른쪽 끝을 트리거에 맞추고 꼬리는 안쪽 12. 스크롤·리사이즈하면 닫혀요

Usage

엑셀로 내려받기
이렇게아이콘만 있는 버튼엔 이름을 툴팁으로
등록하면 채무자에게 문자가 발송되고 취소할 수 없어요
이건 피해요꼭 알아야 할 것을 툴팁에. 버튼 옆이나 Alert에 써요
회수율은 이번 달 회수액을 상환 예정액으로 나눈 값이며, 연체분은 제외되고, 부분 상환은 포함되며, 환불이 발생한 경우 다음 달에 반영되고…
이건 피해요세 줄 넘는 설명. 도움말 페이지로
자세히 보려면 여기를 누르세요
이건 피해요툴팁 안에 링크. 누르기 전에 사라져요

Specification

부분크기글색여백
컨테이너최대 폭 240 · radius 6—Label/Body #555555 · 그림자 없음6 10 · 트리거와 8
글—Caption 12 500 · 행간 1.5Static/White—
Rich폭 240제목 12 600 · 설명 12 · 80%같음8 12 · 제목 아래 2
꼬리6—컨테이너와 같음트리거 가운데
Help 아이콘14—Label/Alternative → 호버 Label/Body라벨과 4
시간———지연 300ms · 페이드 100ms

Props

Prop값기본설명
contentstring · { title, description }—객체면 Rich
placementtop · bottom · left · righttop공간이 없으면 반대편으로 자동
delaynumber300호버 지연 ms. 포커스는 무시
childrenReactElement—트리거 하나. disabled면 span으로 감싸 줘요

쓰기

<Tooltip content="엑셀로 내려받기"> <IconButton icon={Download} label="엑셀로 내려받기" /> </Tooltip> <Tooltip content={{ title: '회수율', description: '이번 달 회수액을 상환 예정액으로 나눈 값이에요.' }}> <HelpIcon /> </Tooltip> {/* 잠긴 메뉴 */} <Tooltip content="관리자만 볼 수 있어요" placement="right"> <span><NavItem locked … /></span> </Tooltip>

클래스

.tooltip { @apply z-[70] max-w-[240px] rounded-md bg-label-body px-2.5 py-1.5 text-xs font-medium leading-normal text-white; } .tooltip-rich { @apply px-3 py-2; } .tooltip-title { @apply block font-semibold mb-0.5; } .tooltip-desc { @apply opacity-80; } .tooltip-arrow { @apply h-1.5 w-1.5 rotate-45 bg-label-body; } .help-icon { @apply inline-flex w-3.5 h-3.5 text-label-alt hover:text-label-body cursor-help; }

주의

항목설명
title 속성브라우저 기본 title 툴팁(MonthCalendar, CreditorAccounts)은 늦게 뜨고 스타일이 달라요. 전부 Tooltip으로.
사이드바 잠김 툴팁Sidebar 안에 인라인으로 그린 group-hover 툴팁은 Tooltip placement="right"로 교체.
접근성role="tooltip", 트리거에 aria-describedby. 아이콘 버튼은 aria-label을 따로 둬요. 툴팁이 라벨을 대신하지 않아요.

Result

흐름이 끝났을 때의 결과 화면과, 보여줄 것이 없을 때의 대체 화면. 큼직한 아이콘, 한 줄 결론, 다음에 할 일 버튼. 결과는 성공 · 실패 · 처리 중, 대체 화면은 Empty · Error · No access.

위임 계약이 끝났어요

내일부터 회수가 시작돼요. 진행 상황은 대시보드에서 볼 수 있어요.

Anatomy

가운데 정렬 한 열. 톤 색 원 위 흰 아이콘, 제목, 설명, 그리고 버튼 둘까지 나란히. 주 행동은 회색 Neutral, 보조는 Ghost. 결과 화면은 이미 끝난 자리라 오렌지를 쓰지 않아요.

위임 계약이 끝났어요

내일부터 회수가 시작돼요.

  1. 1아이콘 · 원 64 톤 색 · 흰 글리프 30 선 2 (lucide 기본) · 그림자·글로우 없음
  2. 2제목 · 22 700 · Label/Title · 한 줄 결론 · 위 24
  3. 3설명 · Body M 15 · Label/Body · 두 줄까지 · 폭 340 · 위 8
  4. 4주 행동 · Neutral M · 글자 폭 + 20 · 위 28
  5. 5보조 행동 · Ghost · 오른쪽에 나란히 · 하나만

Variants

셋. 성공은 다음 할 일로, 실패는 원인과 다시 시도로, 처리 중은 기다리는 동안 무엇을 하면 되는지로 이어요.

계좌가 등록됐어요

상환금은 이 계좌로 들어와요.

Success끝났어요. 다음 할 일 하나

서명이 완료되지 않았어요

인증 시간이 지났어요. 처음부터 다시 진행해 주세요.

Failure왜 안 됐는지와 다시 시도. 제목은 사실만, 탓하지 않기

서류를 검토하고 있어요

보통 1~2일 걸려요. 끝나면 문자로 알려드려요.

Pending기다려야 할 때. 얼마나, 어떻게 알려줄지

Fallback

보여줄 것이 없을 때의 대체 화면도 같은 틀이에요. 아직 없으면 Empty, 못 불러왔으면 Error, 볼 수 없으면 No access. 카드 안에서는 작게.

아직 등록된 채무자가 없어요

첫 채무자를 등록하면 여기에 목록이 보여요.

Empty처음이라 없을 때. 다음 할 일 하나

목록을 불러오지 못했어요

잠시 뒤 다시 시도해 주세요.

Error불러오기 실패. 다시 시도

관리자만 볼 수 있어요

권한이 필요하면 관리자에게 요청해 주세요.

No access권한 없음. 왜 안 보이는지와 요청 방법

아직 등록된 채무자가 없어요

첫 채무자를 등록하면 여기에 목록이 보여요.

페이지표가 들어갈 자리 전체. 아이콘 56, 제목 20

최근 상환

아직 상환 내역이 없어요

첫 입금이 들어오면 여기에 쌓여요.

카드 안아이콘 32, 제목 15, 버튼 없어도 돼요

Layout

한 화면을 다 쓸 때는 세로 가운데에서 살짝 위. 카드 안에서는 카드 폭 그대로, 모바일에서는 버튼이 바닥에 붙어요.

위임 계약이 끝났어요

내일부터 회수가 시작돼요.

페이지온보딩 마지막. 위에서 40% 지점에 아이콘

계좌가 등록됐어요

상환금은 이 계좌로 들어와요.

모바일버튼은 바닥에 고정, 좌우 20 · 아래 safe area

Behavior

동작규칙
이동버튼으로만. 몇 초 뒤 자동으로 넘기지 않아요
뒤로 가기끝난 흐름으로 돌아가지 않게 히스토리를 바꿔요. 뒤로 가면 대시보드
처리 중5초마다 상태를 확인해서 끝나면 Success나 Failure로 바뀌어요. 화면을 떠나도 문자로
실패원인이 사용자 쪽이면 고칠 방법을, 우리 쪽이면 사과와 문의 버튼을
버튼둘까지. 주 행동 Neutral, 보조 Ghost를 오른쪽에. 모바일에선 세로로 쌓여요. 셋이 필요하면 흐름을 다시 봐요

Usage

위임 계약이 끝났어요

내일부터 회수가 시작돼요.

이렇게결론 한 줄, 그래서 무엇이 달라지는지, 다음 할 일

성공

요청이 정상적으로 처리되었습니다.

이건 피해요"성공", "정상 처리". 무엇이 끝났는지 없어요

오류가 발생했습니다

잠시 후 다시 시도해 주세요.

이건 피해요원인 없는 실패. 왜 안 됐고 무엇을 하면 되는지

계좌가 등록됐어요

이건 피해요버튼 셋. 다음 할 일은 하나, 보조는 하나

Specification

부분크기글색여백
컨테이너최대 폭 400 · 가운데—배경 없음40 24
아이콘원 64 · 글리프 30 선 2 (sm 48 · 24 / 카드 안 32 · 16)—Success #0FA53A · Failure #EB003B · Pending #2768FF · Empty · No access #9A9A9A · 글리프 Static/White아래 24
제목—22 700 · 자간 -1.5%Label/Title—
설명폭 최대 340Body M 15 · 행간 1.6 · 두 줄 · 어절 단위 줄바꿈Label/Body위 8
버튼M 36 · 글자 폭 + 20—Neutral · 보조 Ghost 옆에 · Primary 안 씀위 28 · 사이 8
모바일버튼 폭 100% · 세로로——바닥 고정 · 좌우 20

Props

Prop값기본설명
statussuccess · failure · pending · empty · error · forbidden—아이콘과 색이 함께 정해져요. empty·forbidden은 회색
sizemd · smmd페이지 · 카드 안(아이콘 32, 제목 15)
titlestring—필수. 한 줄 결론
descriptionstring—두 줄까지
action{ label, onClick · to }—주 행동. Neutral(회색). 결과 화면엔 오렌지가 없어요
secondary{ label, onClick · to }—보조 행동 하나. Ghost

쓰기

<Result status="success" title="위임 계약이 끝났어요" description="내일부터 회수가 시작돼요. 진행 상황은 대시보드에서 볼 수 있어요." action={{ label: '대시보드로', to: '/' }} secondary={{ label: '계약서 보기', onClick: openContract }} /> <Result status="failure" title="서명이 완료되지 않았어요" description="인증 시간이 지났어요. 처음부터 다시 진행해 주세요." action={{ label: '다시 시도', onClick: retry }} secondary={{ label: '문의하기', to: '/inquiry' }} /> <Result status="empty" title="아직 등록된 채무자가 없어요" description="첫 채무자를 등록하면 여기에 목록이 보여요." action={{ label: '채무자 등록', to: '/debtors/new' }} /> <Result status="error" size="sm" title="목록을 불러오지 못했어요" action={{ label: '다시 시도', onClick: refetch }} /> <Result status="pending" title="서류를 검토하고 있어요" description="보통 1~2일 걸려요. 끝나면 문자로 알려드려요." action={{ label: '대시보드로', to: '/' }} />

클래스

.result { @apply flex flex-col items-center text-center w-full max-w-[400px] mx-auto px-6 py-10; } .result-icon { @apply flex items-center justify-center w-16 h-16 rounded-full mb-6 text-white [&_svg]:w-[30px] [&_svg]:h-[30px] [&_svg]:stroke-2; } .result-success .result-icon { @apply bg-[#0FA53A]; } .result-failure .result-icon { @apply bg-danger; } .result-pending .result-icon { @apply bg-info; } .result-empty .result-icon, .result-forbidden .result-icon { @apply bg-[#9A9A9A]; } .result-sm .result-icon { @apply w-12 h-12 mb-3.5 [&_svg]:w-6 [&_svg]:h-6; } .result-sm .result-title { @apply text-[15px]; } .result-title { @apply text-[22px] font-bold tracking-tight text-label-title [text-wrap:balance] break-keep; } .result-desc { @apply mt-2 max-w-[340px] text-[15px] leading-relaxed text-label-body [text-wrap:balance] break-keep; } .result-actions{ @apply mt-7 flex flex-wrap justify-center items-center gap-2 w-full; } /* 주 행동 .btn-neutral, 보조 .btn-ghost */ @media (max-width: 639px) { .result-actions { @apply fixed inset-x-0 bottom-0 px-5 pb-[calc(20px+env(safe-area-inset-bottom))]; } }

주의

항목설명
완료 화면 여러 벌계좌 등록, 서명, 플랜 제안 완료 화면이 각자 CheckCircle과 문구를 그려요. Result 하나로.
그라데이션 배경EmptyOnboarding의 from-surface 그라데이션은 쓰지 않아요. 배경은 페이지 그대로.
EmptyOnboarding빈 화면을 따로 그리던 EmptyOnboarding은 Result status="empty"로. 카드 안 빈 상태는 size="sm".

Skeleton

데이터가 오는 동안 진짜 내용 자리에 놓는 회색 덩어리. 빈 화면이나 스피너 대신 곧 나올 것의 모양을 먼저 보여줍니다. 내용이 오면 같은 자리에서 바뀌어요.

Anatomy

조각은 셋뿐이에요. 글 줄, 덩어리, 원. 이 셋을 진짜 화면과 같은 자리에 같은 크기로 놓습니다.

  1. 1글 줄 · 높이는 글자 크기 − 4 · radius 6 · 폭 40 · 60 · 90% 중 하나
  2. 2덩어리 · 카드, 이미지, 표 자리 · 진짜와 같은 radius
  3. 3원 · 아바타, 아이콘 자리 · 진짜와 같은 크기

Variants

자주 쓰는 화면 넷을 미리 만들어 둡니다. 새 화면은 셋 조각으로 조합하되, 진짜 레이아웃에서 한 줄도 어긋나지 않게.

KPI라벨 10 · 값 22 · 델타 10. 카드는 진짜 카드
Table행 높이 48 그대로, 열마다 줄 하나. 머리글은 진짜로
List원 32 + 두 줄. 알림, 채무자 목록
Card제목 16 + 본문 두 줄 + 버튼 자리. 버튼은 덩어리로

Sizes

글 줄 높이는 글자 크기에서 4를 뺀 값. 그래야 진짜 글이 들어왔을 때 줄 위치가 그대로예요. 폭은 셋 중 하나만 씁니다.

Title 20 → 16제목 자리
Body 14 → 12본문 · 표 셀
Caption 12 → 10라벨 · 날짜

Behavior

동작규칙
나타남요청 후 300ms가 지나도 안 오면 그때 보여요. 빨리 오는 데이터엔 깜빡이지 않게
머무름한 번 보이면 최소 500ms. 보이자마자 사라지면 더 어수선해요
움직임왼쪽에서 오른쪽으로 옅은 빛이 지나가는 shimmer 1.6s. 화면 전체가 같은 박자로
바뀜내용이 오면 같은 자리에서 페이드 150ms. 높이가 달라 화면이 튀면 스켈레톤 크기가 틀린 거예요
범위지금 기다리는 영역만. 머리글, 제목, 탭처럼 이미 아는 건 진짜로 그려요
개수목록은 최대 5행. 그 아래는 비워요

Usage

출금 계좌

이렇게아는 건 진짜로, 기다리는 것만 스켈레톤
이건 피해요스켈레톤과 스피너를 같이. 하나만
이건 피해요글 길이를 그대로 흉내. 폭은 40 · 60 · 90 셋, 줄은 셋까지

계좌 인증

이건 피해요진짜와 다른 모양. radius와 높이를 그대로

Specification

부분크기색움직임여백
글 줄높이 글자 − 4 · radius 6Fill/Normal #F0F0F0shimmer → #F8F8F8줄 사이 8 · 폭 40 · 60 · 90%
덩어리진짜와 같음Fill/Normal같음진짜와 같음
원진짜와 같음Fill/Normal같음—
shimmer폭 200% 그라데이션#F0F0F0 → #F8F8F8 → #F0F0F01.6s linear 무한—
시간——지연 300ms · 최소 500ms · 교체 150ms—

Props

Prop값기본설명
varianttext · block · circletext조각 하나
width · heightnumber · '40%' · '60%' · '90%''60%' · 12text는 폭 셋 중 하나
lines1~31text 여러 줄. 마지막 줄은 60%
radiusnumber6 · block 12 · circle 50%진짜 요소와 같게
patternkpi · table · list · card—<SkeletonPattern>. rows로 반복 수(최대 5)

쓰기

{isLoading ? <SkeletonPattern pattern="table" rows={5} /> : <Table … />} {/* 카드 안에서 기다리는 부분만 */} <Card title="출금 계좌"> {account ? <AccountRow account={account} /> : <Skeleton lines={2} />} </Card> {/* 조합 */} <div className="flex items-center gap-3"> <Skeleton variant="circle" width={32} height={32} /> <div className="flex-1 space-y-2"><Skeleton width="40%" /><Skeleton width="70%" height={10} /></div> </div> {/* 300ms 지연, 최소 500ms는 훅이 처리 */} const showSkeleton = useDelayedLoading(isLoading, { delay: 300, minDuration: 500 });

클래스

.skeleton { @apply block bg-fill rounded-md bg-[linear-gradient(90deg,#F0F0F0_25%,#F8F8F8_50%,#F0F0F0_75%)] bg-[length:200%_100%] animate-[shimmer_1.6s_linear_infinite]; } .skeleton-circle { @apply rounded-full; } .skeleton-block { @apply rounded-xl; } @keyframes shimmer { from { background-position: 200% 0 } to { background-position: -200% 0 } } @media (prefers-reduced-motion: reduce) { .skeleton { animation: none; } }

주의

항목설명
animate-pulseTailwind 기본 pulse(투명도 깜빡임)는 쓰지 않아요. shimmer 하나로 통일.
로딩 스피너화면 가운데 스피너 하나로 기다리게 하던 페이지들은 Skeleton으로. 스피너는 버튼 안(loading)에서만.
aria감싸는 영역에 aria-busy="true", 스켈레톤 자체는 aria-hidden. 내용이 오면 aria-busy 해제.
reduced motion움직임 줄이기 설정이면 shimmer를 끄고 색만.

Toast

방금 한 일의 결과를 화면 아래에서 잠깐 알려주는 한 줄. 3초 뒤 스스로 사라지고, 흐름을 막지 않아요. 답을 받아야 하면 Alert, 남아 있어야 하면 Banner.

Anatomy

어두운 반투명 알약에 흰 글 한 줄. 필요하면 앞에 아이콘, 뒤에 행동 하나.

문자 3건을 보냈어요실행 취소1
  1. 1컨테이너 · Static/Black 86% + blur 8 · radius 10 · 10 16 · 최대 폭 360 · 화면 아래 가운데, 바닥에서 32
  2. 2아이콘 · 16 · 성공 · 실패에만 · 어두운 배경용 밝은 톤
  3. 3글 · Label S 13 600 · Static/White · 한 줄, 길면 두 줄까지
  4. 4행동 · 13 700 · Primary 밝은 톤 · 하나만 · 있으면 5초

Variants

넷. 대부분은 글만 있는 Default. 결과가 좋고 나쁨이 중요하면 아이콘, 되돌릴 수 있으면 행동, 설명이 꼭 필요하면 두 줄.

계좌를 저장했어요
Default글만. 저장, 복사, 변경 같은 결과
문자 3건을 보냈어요
엑셀을 내려받지 못했어요
With icon성공과 실패. 실패는 원인이 없어도 다음 할 일이 분명할 때만
채무자 1명을 삭제했어요실행 취소
With action되돌리기, 보기. 행동 하나, 5초 유지
위임 계약이 저장됐어요채무자에게 서명 요청 문자를 보냈어요.
Two lines설명이 꼭 필요할 때만. 그보다 길면 Banner

Placement

화면 아래 가운데, 바닥에서 32. 사이드바가 있어도 콘텐츠 영역이 아니라 화면 기준이에요. 모바일에서는 바닥 탭 위 16.

Behavior

동작규칙
나타남아래에서 12 올라오며 페이드 200ms
사라짐3초. 행동이 있으면 5초. 마우스를 올리면 멈추고, 떠나면 다시 세요. 누르면 바로
겹침한 번에 하나. 새 토스트가 오면 이전 것은 바로 바뀌어요. 쌓지 않아요
실패다시 시도가 필요한 실패는 Toast가 아니라 Banner나 Alert. Toast 실패는 "내려받지 못했어요"처럼 가벼운 것만
접근성role="status" aria-live="polite". 행동 버튼은 Tab으로 갈 수 있고, 포커스되면 사라지지 않아요

Usage

계좌를 저장했어요
이렇게무엇을 했는지 한 줄. 과거형, "~했어요"
성공적으로 저장되었습니다!
이건 피해요"성공적으로", 느낌표. 무엇을 저장했는지 없어요
계좌 확인에 실패했어요. 계좌번호를 다시 확인해 주세요
이건 피해요고쳐야 할 오류를 Toast로. 사라지면 못 봐요. 필드 Error나 Banner
계좌를 저장했어요
문자를 보냈어요
메모를 저장했어요
이건 피해요여러 개 쌓기. 한 번에 하나

Specification

부분크기글색여백
컨테이너최대 폭 360 · radius 10—Static/Black 86% + blur 8 · 그림자 0 8 24 18%10 16 · 두 줄 12 16 · 바닥 32
글—Label S 13 600 · 두 줄형 설명 12.5 400Static/White · 설명 80%아이콘과 10
아이콘16—성공 #7BE495 · 실패 #FF6B8A—
행동—13 700Primary 밝은 톤 #FF8A47글과 12
시간———등장 200ms · 유지 3s (행동 5s)

Props

Prop값기본설명
messagestring—한 줄. "~했어요"
toneneutral · success · dangerneutral아이콘이 붙어요
descriptionstring—두 줄형
action{ label, onClick }—하나. 있으면 5초
durationnumber3000 · 5000바꾸지 않는 게 기본

쓰기

toast('계좌를 저장했어요'); toast({ message: '문자 3건을 보냈어요', tone: 'success' }); toast({ message: '채무자 1명을 삭제했어요', action: { label: '실행 취소', onClick: undo } }); {/* 훅 없이 어디서든. 앱 루트에 <Toaster /> 하나 */} <Toaster position="bottom-center" offset={32} />

클래스

.toast { @apply fixed bottom-8 left-1/2 -translate-x-1/2 z-[80] inline-flex items-center gap-2.5 max-w-[360px] rounded-[10px] bg-black/[.86] backdrop-blur-lg px-4 py-2.5 text-[13px] font-semibold text-white shadow-[0_8px_24px_rgba(0,0,0,.18)]; } .toast-icon { @apply w-4 h-4 shrink-0; } .toast-success .toast-icon { @apply text-[#7BE495]; } .toast-danger .toast-icon { @apply text-[#FF6B8A]; } .toast-action { @apply ml-1.5 font-bold text-[#FF8A47] hover:underline; } .toast-enter { @apply translate-y-3 opacity-0; } .toast-enter-active { @apply translate-y-0 opacity-100 transition duration-200; }

주의

항목설명
alert()브라우저 alert로 알리던 곳(복사 완료, 저장 완료)은 전부 toast()로.
성공 모달"저장되었습니다" 모달은 Toast로. Modal 페이지의 don't.
위치fixed, 화면 기준. 사이드바 폭을 빼고 가운데 맞추지 않아요.

Loading

기다리는 동안 무언가 하고 있다는 신호. 화면 구조를 알면 Skeleton, 버튼이나 작은 영역이면 Spinner. 둘을 같이 쓰지 않아요.

불러오는 중

Anatomy

회색 트랙 위를 도는 진한 호. 옆에 글이 붙으면 무엇을 기다리는지 말해요.

불러오는 중
  1. 1트랙 · 원 20 (S 16 · L 32) · 선 2 (L 3) · Line/Normal
  2. 2호 · 4분의 1 · Label/Title · 0.8초에 한 바퀴 · 버튼 안에서는 Static/White
  3. 3글 · Body S 14 · Label/Alternative · "~하는 중" · 있어도 없어도

Variants

넷. 버튼을 눌러 기다릴 땐 In button, 한 줄로 알릴 땐 Inline, 카드나 영역 하나면 Area, 화면 전체를 막아야만 하면 Overlay.

In button라벨 앞에 S 16. 라벨은 "~하는 중"으로 바뀌고 버튼은 눌리지 않아요
상환 내역을 불러오는 중
Inline표 아래 더 보기, 검색 결과 대기. M 20 + 글
Area카드 하나가 통째로 기다릴 때. L 32, 가운데. 구조를 알면 Skeleton이 나아요
서명을 확인하는 중
Overlay화면을 떠나면 안 될 때만. Static/White 70%, 가운데 L + 글

Sizes

S · 16선 2 · 버튼 안
M · 20선 2 · 인라인
L · 32선 3 · 영역, 오버레이

Behavior

동작규칙
나타남300ms가 지나도 안 끝나면 그때. 빨리 끝나는 일엔 깜빡이지 않게
버튼누르는 순간 로딩. 같은 폼의 다른 버튼도 비활성. 끝나면 Toast나 화면 전환
오래 걸리면3초 넘으면 글을 붙여요("서명을 확인하는 중"). 10초 넘으면 "시간이 걸리고 있어요" + 취소
Skeleton과목록, 카드, 표처럼 모양을 아는 건 Skeleton. Spinner는 행동의 결과나 모양을 모르는 작은 영역
움직임 줄이기설정이 켜져 있으면 회전 대신 호가 천천히 깜빡여요

Usage

이렇게누른 버튼 안에서, 라벨은 진행형으로
이건 피해요Skeleton과 Spinner 같이. 하나만
이건 피해요카드마다 스피너. 모양을 아니까 Skeleton
이건 피해요로딩 중인데 라벨이 그대로, 옆 버튼은 살아 있음. "저장 중" + 다른 버튼 비활성

Specification

부분크기글색움직임
SpinnerS 16 · M 20 · L 32 · 선 2 · 2 · 3—트랙 Line/Normal · 호 Label/Title · 버튼 안 Static/White 35% / 100%0.8s linear 무한
Inline 글—Body S 14Label/Alternative스피너와 10
Overlay영역 전체13Static/White 70%등장 150ms
시간———지연 300ms · 글 3s · 취소 10s

Props

Prop값기본설명
sizesm · md · lgmd16 · 20 · 32
labelstring—옆에 글. 없어도 aria-label "불러오는 중"
delaynumber300이 시간 안에 끝나면 안 보여요
Button loadingbooleanfalseButton의 prop. 스피너 + 비활성 + 라벨 유지

쓰기

<Button loading={saving} onClick={save}>{saving ? '저장 중' : '저장'}</Button> <Spinner label="상환 내역을 불러오는 중" /> {/* 영역 하나 */} <Card>{loading ? <Spinner size="lg" center /> : <Chart … />}</Card> {/* 떠나면 안 될 때만 */} {signing && <LoadingOverlay label="서명을 확인하는 중" />}

클래스

.spinner { @apply inline-block w-5 h-5 rounded-full border-2 border-line border-t-label-title animate-[spin_.8s_linear_infinite]; } .spinner-sm { @apply w-4 h-4; } .spinner-lg { @apply w-8 h-8 border-[3px]; } .spinner-white { @apply border-white/35 border-t-white; } .spinner-label { @apply inline-flex items-center gap-2.5 text-sm text-label-alt; } .loading-overlay { @apply absolute inset-0 z-20 flex flex-col items-center justify-center gap-3 bg-white/70 text-[13px] text-label-alt; } @media (prefers-reduced-motion: reduce) { .spinner { @apply animate-pulse; animation-duration: 1.5s; } }

주의

항목설명
Loader2 아이콘lucide Loader2에 animate-spin을 붙인 곳이 여러 벌이에요. Spinner 하나로.
페이지 가운데 스피너목록 페이지의 전체 스피너는 SkeletonPattern으로 바꿔요.
ariarole="status" aria-live="polite" aria-label="불러오는 중". 오버레이는 aria-busy를 영역에.

Navigation

왼쪽 사이드바 256 고정. 지금 있는 곳만 오렌지, 나머지는 모두 회색이에요. 좁은 화면에서는 아래 탭으로 바뀝니다.

채권 관리
대시보드
채무자12
상환 관리
계약 서류
설정
계정 설정
권한 관리
리주식회사 리바운드채권자

Anatomy

로고, 그룹 라벨, 항목, 그리고 맨 아래 계정. 항목은 아이콘 자리와 라벨, 필요하면 오른쪽에 개수.

채권 관리2
대시보드3
채무자124
상환 관리
리주식회사 리바운드채권자5
  1. 1로고 영역 · 높이 56 · 왼쪽 정렬
  2. 2그룹 라벨 · Caption 12 500 · Label/Alternative · 대문자 아님
  3. 3활성 항목 · 굵은 글자(Label/Title 700)뿐 · 배경·오렌지 없음
  4. 4항목 · 높이 40 · lucide 선 아이콘 18(stroke 1.75) · Label M 14 · Label/Body · 호버는 글자만 Label/Title · 묶음 사이 여백 20
  5. 5계정 · 아바타 28 · 이름 13 600 · 역할 Caption · 누르면 메뉴

Variants

넷. 데스크톱은 Sidebar, 모바일은 Bottom tab, 사이드바가 없는 온보딩 화면은 Top bar, 계정을 누르면 Menu.

채권 관리
대시보드
채무자12
상환 관리
Sidebar256 고정, 흰 면, 오른쪽 Line/Alternative. 그룹은 라벨로 나눠요
Bottom tab모바일 640 미만. 다섯 개까지, 아이콘 22 + 라벨 11
주식회사 리바운드
Top bar온보딩과 인증처럼 사이드바가 없는 화면. 높이 64, 로고와 계정만
Menu계정을 누르면. 자세한 규칙은 Menu 페이지에

States

항목의 상태. 호버는 아주 옅은 회색이고 오렌지는 활성에만 씁니다. 권한이 없으면 흐리게 두고 이유를 툴팁으로.

채무자
DefaultLabel/Body 500
채무자
HoverStatic/Black 4%
채무자
ActivePrimary/Normal · Fill/Normal · 600
권한 관리
Lockedopacity .4 · 툴팁으로 이유

Layout

사이드바 256, 위에 붙는 헤더 64, 나머지가 콘텐츠. 콘텐츠는 회색 면에 안쪽 여백 40 32이고 최소 폭 1080 아래로는 모바일 구조로 바뀝니다.

Usage

대시보드
채무자
상환 관리
이렇게활성은 하나, 나머지는 회색. 아이콘은 전부 있거나 전부 없거나
대시보드
채무자
상환 관리
이건 피해요호버에 오렌지. 어디가 활성인지 헷갈려요
대시보드
채무자
상환 관리
이건 피해요아이콘 있는 항목과 없는 항목 섞기
대시보드
채무자
이건 피해요활성을 오렌지 채움으로. 활성은 굵은 글자만

Specification

부분크기글색여백
Sidebar폭 256 · 높이 100%—Background/Normal · 오른쳑 Line/Alternative로고 56 · 목록 16 12
그룹 라벨—Caption 11 600 · 대문자 · 자간 .08emLabel/Alternative12 12 4 · 첫 그룹 위 8
항목높이 38 · radius 8Label M 14 500Label/Body · 아이콘 Label/Alternative좌우 12 · 아이콘과 10 · 항목 사이 4
Hover——Static/Black 4%—
Active—600Primary/Normal 글자와 아이콘 · Fill/Normal 배경—
개수Count 배지 20Caption 11 700 tabularPrimary/Normal · 99+오른쪽 끝
계정아바타 28이름 13 600 · 역할 Caption위 Line/Alternative12 12
Bottom tab높이 56 + safe areaCaption 11 500Label/Alternative → 활성 Primary/Normal · 위 Line/Alternative아이콘 22 · 라벨 위 4
Top bar높이 64이름 15 600Background/Normal좌우 24
Menu폭 200 · radius 12Body S 14Elevation Dropdown · 로그아웃 Danger항목 10 16 · 위아래 4

Props

Prop값기본설명
groups{ label, items: NavItem[] }[]—그룹과 항목. 라벨 없는 그룹도 가능
NavItem{ to, label, icon, count?, locked?, lockReason? }—icon은 lucide 컴포넌트. count는 0이면 숨김
account{ name, role, menu: MenuItem[] }—맨 아래 계정과 눌렀을 때 메뉴
endbooleanfalseNavLink end. 대시보드처럼 루트 경로에만 true

쓰기

<Sidebar groups={[ { label: '채권 관리', items: [ { to: '/', label: '대시보드', icon: LayoutDashboard, end: true }, { to: '/debtors', label: '채무자', icon: Users, count: pending }, { to: '/payments', label: '상환 관리', icon: Wallet }, { to: '/documents', label: '계약 서류', icon: FileText }, ]}, { label: '설정', items: [ { to: '/settings', label: '계정 설정', icon: Settings }, { to: '/roles', label: '권한 관리', icon: Lock, locked: !isAdmin, lockReason: '관리자만 볼 수 있어요' }, ]}, ]} account={{ name: org.name, role: '채권자', menu: [ { label: '계정 정보', to: '/settings' }, { label: '비밀번호 변경', onClick: openPw }, { label: '로그아웃', onClick: logout, tone: 'danger' }, ]}} /> {/* 항목 하나 */} <NavLink to={to} end={end} className={({ isActive }) => cx('nav-item', isActive && 'is-active')}> <span className="nav-icon"><Icon size={16} /></span> <span className="truncate flex-1">{label}</span> {count > 0 && <Badge variant="count">{count}</Badge>} </NavLink>

클래스

.sidebar { @apply w-[256px] shrink-0 flex flex-col h-full bg-white border-r border-line-alt; } .sidebar-logo { @apply h-14 flex items-center px-5; } .nav-group { @apply px-3 pt-3 pb-1 text-[11px] font-semibold uppercase tracking-[.08em] text-label-alt; } .nav-item { @apply flex items-center gap-2.5 h-[38px] px-3 rounded-lg text-sm font-medium text-label-body hover:bg-black/[.04] transition-colors; } .nav-item.is-active { @apply font-semibold text-primary bg-fill; } .nav-item.is-active .nav-icon { @apply text-primary; } .nav-icon { @apply w-7 h-7 rounded-lg flex items-center justify-center shrink-0 text-label-alt; } .nav-item.is-locked { @apply opacity-40 cursor-not-allowed hover:bg-transparent; } .nav-account { @apply mt-auto flex items-center gap-2.5 px-3 py-3 border-t border-line-alt; } .bottom-tab { @apply fixed bottom-0 inset-x-0 h-14 flex bg-white border-t border-line-alt pb-[env(safe-area-inset-bottom)]; } .bottom-tab a { @apply flex-1 flex flex-col items-center justify-center gap-1 text-[11px] text-label-alt; } .bottom-tab a.is-active { @apply text-primary; } .top-bar { @apply h-16 flex items-center justify-between px-6 bg-white; } .menu { @apply w-[200px] p-1 bg-white rounded-xl shadow-dropdown; } .menu-item { @apply flex items-center gap-2 w-full px-4 py-2.5 text-sm text-label-body rounded-lg hover:bg-fill; } .menu-item.is-danger { @apply text-danger hover:bg-danger/[.06]; }

주의

항목설명
인라인 색지금 NavLink는 style={{ color: '#52525b' }}처럼 색을 직접 넣어요. 클래스로 옮기고 #52525b → Label/Body, #8A9099 → Label/Alternative, #F4F4F5 → Fill/Normal.
로고 영역 8080에서 56으로. 헤더 64와 같은 줄에 맞추려면 56 + 여백 8.
우선 안내 툴팁사이드바 안의 온보딩 말풍선(border-2 검정)은 Navigation 밖의 패턴이에요. 나중에 Tooltip으로.
모바일 드로어오른쪽에서 나오는 280 드로어는 Bottom tab으로 대체. 드로어는 메뉴가 다섯을 넘을 때만.

Menu

버튼이나 더보기를 누르면 열리는 행동 목록. 항목은 동사로 짧게, 되돌릴 수 없는 행동은 맨 아래 Danger로. 값을 고르면 Select, 화면을 옮기면 Navigation이에요.

수정
복사
내려받기
삭제
정렬 기준
최근 등록순
금액 높은순
이름순

Anatomy

트리거 아래 6 떨어진 흰 컨테이너. 안은 항목이고, 무리를 나눌 땐 구분선. 위험한 행동은 구분선 아래 맨 끝에.

수정3
복사
내려받기
삭제6
2
  1. 1트리거 · 아이콘 버튼 32 또는 Neutral 버튼. 열려 있으면 Fill/Normal
  2. 2컨테이너 · 폭 180~280 · radius 16 · Elevation Dropdown · 안쪽 8 · 트리거와 6
  3. 3항목 · 높이 44 · 14.5 · Label/Title · radius 10 · 좌우 12
  4. 4앞 아이콘 · 16 · Label/Alternative · 있으면 모든 항목에
  5. 5구분선 · Line/Alternative 1px · 위아래 4 · 좌우 8
  6. 6위험 항목 · Danger 글자와 아이콘 · 구분선 아래 맨 끝에 하나

Variants

넷. 대부분은 Action이고, 항목이 다섯을 넘으면 Sections로 나눠요. Check는 지금 고른 상태를 함께 보여줄 때.

수정
복사
내려받기
삭제
Action행동 목록. 동사 하나, 3~7개
수정
복사
내려받기
삭제
With icons아이콘은 전부 있거나 전부 없거나. 위험 항목은 아이콘도 Danger
내보내기
엑셀로
메일로 보내기
관리
수정
삭제
Sections구분선과 Caption 라벨로 무리를 나눠요. 다섯 넘을 때
정렬 기준
최근 등록순
금액 높은순
이름순
Check지금 상태를 고를 때. 고른 항목은 왼쪽 체크 + Primary 글자. 값이 폼에 저장되면 Select

Sizes

항목 높이 둘. 기본 40, 표의 행마다 붙는 더보기 메뉴처럼 좁은 곳은 36.

수정
복사
삭제
M · 40기본 · Body S 14 · 폭 160~280
수정
복사
삭제
S · 36표 행 메뉴 · 13 · 폭 140~200

States

항목의 상태 넷. 호버는 Fill/Normal, 위험은 Danger 글자에 호버 Danger 6%.

수정
DefaultLabel/Title
수정
HoverFill/Normal
수정
Disabledopacity .4 · 이유는 툴팁
삭제
DangerDanger · 호버 Danger 6%

Placement

트리거 바로 아래, 왼쪽을 맞춰 열립니다. 트리거가 오른쪽 끝에 있으면 오른쪽을 맞추고, 아래 공간이 없으면 위로.

수정
복사
삭제
아래 · 왼쪽 맞춤기본
수정
복사
삭제
아래 · 오른쪽 맞춤트리거가 오른쪽 끝일 때
수정
복사
삭제
위아래 공간이 모자랄 때. 간격은 같게 6

Behavior

동작규칙
열기클릭. 호버로 열지 않아요. 열리면 트리거가 Fill/Normal로 눌린 상태
닫기항목을 고르면, ESC, 바깥 클릭. 셋 다 같은 결과
키보드↑↓로 이동, Enter로 실행. 열리면 첫 항목에 포커스
겹침한 번에 하나. 다른 메뉴가 열리면 앞 메뉴는 닫혀요. 메뉴 안에 메뉴는 두지 않아요
위험 항목고르면 바로 실행하지 않고 Modal Alert로 한 번 물어요
모바일640 미만에서 Bottom sheet. 항목 높이 48

Usage

수정
복사
삭제
이렇게동사 하나씩, 위험은 구분선 아래 맨 끝
수정
이건 피해요항목이 하나. 그냥 버튼으로
삭제
수정
복사
이건 피해요위험 항목을 위나 중간에. 실수로 눌러요
수정
복사
삭제
이건 피해요아이콘 일부만. 전부 있거나 전부 없거나

Specification

부분크기글색 · 그림자여백
컨테이너폭 180~280 · radius 16—Background/Normal · Elevation Dropdown안쪽 8 · 트리거와 6
항목 M높이 44 · radius 1014.5Label/Title · 호버 Fill/Normal좌우 12 · 아이콘과 10
항목 S높이 38 · radius 813.5같음좌우 10 · 컨테이너 안쪽 6 · radius 12
아이콘16—Label/Alternative · 위험 Danger—
체크14고른 항목 600Primary 글자와 체크왼쪽 · 안 고른 항목도 자리 16 비움
구분선1—Line/Alternative위아래 4 · 좌우 8
섹션 라벨—12.5 500Label/Alternative8 12 6
위험 항목——Danger · 호버 Danger 6%구분선 아래
트리거아이콘 버튼 32 · radius 8—열리면 Fill/Normal—

Props

Prop값기본설명
triggerReactNode—버튼이나 아이콘 버튼. 열림 상태는 Menu가 넘겨줘요
items{ label, icon?, onSelect, tone?: 'danger', disabled?, checked? }[]—배열 안의 'divider'로 구분선, { label }만 있으면 섹션 라벨
sizemd · smmd40 · 36
alignstart · endstart트리거 기준 왼쪽 · 오른쪽 맞춤. 위아래는 공간 보고 자동
widthnumber200160~280

쓰기

<Menu trigger={<IconButton icon={MoreHorizontal} label="더보기" />} items={[ { label: '수정', icon: Pencil, onSelect: edit }, { label: '복사', icon: Copy, onSelect: duplicate }, { label: '내려받기', icon: Download, onSelect: download, disabled: !file }, 'divider', { label: '삭제', icon: Trash2, onSelect: () => setAsk(true), tone: 'danger' }, ]} /> {/* 상태를 고르는 메뉴 */} <Menu trigger={<Button variant="neutral" size="sm" trailing={ChevronDown}>정렬: {SORT[sort]}</Button>} items={Object.entries(SORT).map(([k, v]) => ({ label: v, checked: sort === k, onSelect: () => setSort(k) }))} /> {/* 표 행의 더보기 */} <td className="td text-right"><Menu size="sm" align="end" trigger={…} items={rowActions(r)} /></td>

클래스

.menu { @apply min-w-[180px] max-w-[280px] p-2 bg-white rounded-2xl shadow-dropdown; } .menu-item { @apply flex items-center gap-2.5 h-11 px-3 rounded-[10px] text-[14.5px] text-label-title cursor-pointer hover:bg-fill focus-visible:bg-fill outline-none; } .menu-item svg { @apply w-4 h-4 text-label-alt; } .menu-item[aria-disabled] { @apply opacity-40 pointer-events-none; } .menu-item.is-danger { @apply text-danger hover:bg-danger/[.06]; } .menu-item.is-danger svg { @apply text-danger; } .menu-item.is-checked { @apply font-semibold text-primary; } .menu-check { @apply w-3.5 h-3.5 text-primary; } /* 왼쪽. 안 고른 항목은 같은 폭을 비워 둬요 */ .menu-sep { @apply my-1 mx-2 h-px bg-line-alt; } .menu-label { @apply px-3 pt-2 pb-1.5 text-[12.5px] font-medium text-label-alt; } .menu-sm .menu-item { @apply h-9 px-2.5 text-[13px]; } .icon-btn { @apply w-8 h-8 inline-flex items-center justify-center rounded-lg text-label-body hover:bg-fill; } .icon-btn[aria-expanded="true"] { @apply bg-fill; }

주의

항목설명
드롭다운 세 벌OnboardingHeader, ViewToggle, Sidebar 계정 메뉴가 각자 div로 그려요. Menu 하나로 모으고 items만 넘겨요.
border + shadow지금 드롭다운은 테두리와 그림자를 같이 써요. Elevation Dropdown 그림자만.
role컨테이너 role="menu", 항목 role="menuitem" 또는 menuitemradio(체크). 트리거에 aria-expanded.
Select와 구분폼 값이 되는 선택은 Select. Menu의 check는 정렬, 보기 방식처럼 화면 상태만.

Stepper

여러 단계로 이어지는 흐름을 보여주는 표시. 두꺼운 진행 바 안에 단계 점을 놓고, 지금까지 온 만큼 옅은 오렌지로 채웁니다. 순서만 안내할 때는 List. 단계는 셋에서 다섯.

채권 정보플랜 제시마무리

Anatomy

두꺼운 트랙 안에 단계 점, 그 아래 라벨. 시작부터 지금 점까지 옅은 오렌지로 채우고, 지금 점만 진하게.

채권 정보플랜 제시마무리
  1. 1트랙 · 높이 12 · radius 6 · Fill/Normal · 폭 100%
  2. 2채움 · Primary 7% → 30% 그라데이션 · 시작부터 지금 점까지
  3. 3끝난 점 · 8 · Static/White · Primary 35% 테두리 · 그림자 살짝
  4. 4지금 점 · 14 · Primary · 링 4 Primary 22%
  5. 5남은 점 · 8 · Static/White · Line/Normal 테두리
  6. 6라벨 · Label S 13 · 트랙 아래 12 · 지금만 Label/Title 700 · 양끝은 끝에 맞춤

Variants

다섯. 대부분은 Line이고, 제목 줄 오른쪽에 작게 붙이면 Line small. 단계 번호가 중요하면 Pill. 할 일을 하나씩 끝내는 화면은 Vertical, 순서만 설명하는 안내는 List.

채권 정보플랜 제시마무리
Line기본. 온보딩 상단, 신청 진행 상태. 트랙 12에 점, 지나온 구간은 옅어지는 오렌지
단계 1단계 2단계 3
Line · Small제목 줄 오른쪽 폭 220. 트랙 10 · 점 7 · 지금 14 · 라벨 12. 아코디언 단계 화면
23
채권 정보플랜 제시마무리
Pill번호가 보여야 할 때. 트랙 36에 원 28, 지나온 구간은 Primary 12%
사업자 인증사업자등록번호 확인 완료
2출금 계좌 등록1원 인증으로 확인해요
3위임 계약 서명전자서명 5분
Vertical할 일 목록. 제목 아래 한 줄 설명, 끝난 것부터 위에
1채권 정보를 등록해요채무자와 금액, 상환 조건
2상환 플랜을 제시해요최대 3개까지 제안
3계약에 서명해요전자서명 5분
List진행 상태 없이 순서만. 번호 원 Fill/Normal, 오른쪽엔 chevron이나 버튼 하나

List 글 조합

List의 제목과 설명 크기는 셋 중 하나. 기본은 15 + 13, 큼직하게 보일 자리엔 17, 촘촘히 넣을 자리엔 12.

채권 정보 등록채무자와 금액, 상환 조건
A · 15 + 13기본
계좌 등록1원 인증으로 확인해요
B · 17 + 13온보딩 안내처럼 크게
계약 서명전자서명 5분
C · 15 + 12카드 안, 촘촘하게

States

단계 하나의 상태 넷. 순서대로 흘러가고, 검증에 걸린 단계만 Danger.

채권 정보
Done흰 원 · Primary 체크 · Line은 Primary 점 8
2플랜 제시
CurrentPrimary 채움 + 링 · 라벨 Label/Title 600
3마무리
Upcoming흰 원 · 숫자 Label/Alternative · 누를 수 없음
!플랜 제시
Error검증 실패 · Danger 테두리와 글자

Layout

온보딩에서는 제목 줄 오른쪽에 폭 320. 페이지 폭을 다 쓸 때는 카드 안 맨 위에 폭 100%. 좁은 화면에서도 같은 Line, 라벨은 지금 단계만 남겨요. 사이드바 없는 집중 단계 화면(단계별 채권 등록)은 머리띠 바로 아래 가운데 — 트랙 300 · 양옆 44 · 머리띠와 36 · 본문 제목까지 최소 64. 본문은 스테퍼 아래 남는 공간에서 세로 가운데에 두되, 이 64는 줄어들지 않게 해요 — 짧은 단계는 가운데로 내려오고, 화면을 넘기는 긴 단계는 64까지만 올라와서 제목이 라벨에 붙지 않아요. 상단바 안에는 넣지 않아요.

플랜 제시
채무자에게 제안할 상환 플랜을 만들어요
채권 정보플랜 제시마무리
폼이 들어가는 자리

Behavior

동작규칙
이동순서대로만 앞으로. 끝난 단계는 라벨을 눌러 돌아갈 수 있고, 남은 단계는 누를 수 없어요
채움시작 점에서 지금 점까지. 단계가 바뀌면 300ms에 걸쳐 늘어나요
돌아가기끝난 단계로 돌아가 고치면 그 뒤 단계는 다시 확인. 채움은 유지
단계 수3~5. 여섯이 넘으면 묶어서 줄이거나 Vertical로
라벨명사 2~4자. "채권 정보", "플랜 제시". 문장이나 동사는 쓰지 않아요
모바일640 미만에서도 Line 그대로. 라벨은 지금 단계만 남기고 나머지는 숨겨요

Usage

채권 정보플랜 제시마무리
이렇게셋. 어디까지 왔는지 한눈에
등록플랜완료
이건 피해요일곱 단계. 묶어서 다섯 안으로
정보를 입력해요플랜을 만들어요서명해요
이건 피해요문장 라벨. 명사 2~4자로
채권 정보플랜 제시마무리
이건 피해요지금 단계 표시 없이 전부 회색. 어디인지 몰라요

Specification

부분크기글색여백
Line 트랙12 · radius 6 · 폭 100%—Fill/Normal · 채움 Primary 7% → 30% 그라데이션라벨과 12 · 점 안쪽 6
Line 점8 · 지금 14—Done 흰 + Primary 35% 테두리 · Upcoming 흰 + Line/Normal · Current Primary + 링 4 22%트랙 가운데
Pill 트랙36 · radius 18—Fill/Normal · 지나온 구간 Primary 6% → 24% 그라데이션안쪽 4
Pill 원28숫자 12 700 · 체크 13Background/Normal · Done 체크 Primary · Current Primary + 링 4 20% · Error Danger—
라벨—Label S 13 500 · Current 700Label/Alternative · Current Label/Title · Done Label/Body점 아래 10 · 양끝 정렬
Vertical원 28 · 바 4제목 14 600 · 설명 12.5Pill 원과 같음원과 글 12 · 단계 사이 20
List원 28 · 선 2A 15 600 + 13 · B 17 700 + 13 · C 15 600 + 12원 Fill/Normal · 아이콘 원 Primary 10%행 위아래 10 · 원과 글 12

Props

Prop값기본설명
steps{ key, label, description?, icon? }[]—3~5개. description은 Vertical과 List에서만
currentkey—지금 단계. 앞은 Done, 뒤는 Upcoming으로 자동. List에선 무시
variantline · pill · vertical · listline형태
textsA · B · CAList 제목·설명 크기. 15+13 · 17+13 · 15+12
steps[].right'chevron' · { label, onClick }—List 오른쪽. chevron 또는 버튼 하나
errorkey[][]검증에 걸린 단계
onStepClick(key) => void—Done 단계에서만 불려요

쓰기

const STEPS = [ { key: 'debt-info', label: '채권 정보' }, { key: 'plans', label: '플랜 제시' }, { key: 'finalize', label: '마무리' }, ]; <Stepper steps={STEPS} current="plans" onStepClick={go} /> <Stepper variant="pill" steps={STEPS} current="plans" /> {/* 할 일 목록 */} <Stepper variant="vertical" current="account" steps={[ { key: 'verify', label: '사업자 인증', description: '사업자등록번호 확인 완료' }, { key: 'account', label: '출금 계좌 등록', description: '1원 인증으로 확인해요' }, { key: 'contract', label: '위임 계약 서명', description: '전자서명 5분' }, ]} /> {/* 순서 안내 */} <Stepper variant="list" texts="B" steps={[ { key: 1, label: '채권 정보를 등록해요', description: '채무자와 금액, 상환 조건', right: 'chevron' }, { key: 2, label: '계약에 서명해요', description: '전자서명 5분', right: { label: '시작', onClick: start } }, ]} />

클래스

.stepper { @apply relative w-full; } .stepper-track { @apply relative h-3 rounded-full bg-fill; } .stepper-fill { @apply absolute inset-y-0 left-0 rounded-full bg-gradient-to-r from-primary/[.07] to-primary/[.3] transition-[width] duration-300; } .stepper-dot { @apply absolute top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white border border-line; } .stepper-dot.is-done { @apply border-primary/45; } .stepper-dot.is-current { @apply h-3.5 w-3.5 bg-primary border-0 ring-4 ring-primary/20; } .stepper-labels { @apply relative mt-3 h-5 text-[13px] font-medium text-label-alt; } .stepper-labels span { @apply absolute -translate-x-1/2 whitespace-nowrap first:left-0 first:translate-x-0 last:left-auto last:right-0 last:translate-x-0; } .stepper-labels .is-current { @apply font-bold text-label-title; } .stepper-pill .stepper-track { @apply h-9 rounded-full bg-fill; } .stepper-pill .stepper-fill { @apply bg-gradient-to-r from-primary/[.06] to-primary/[.24]; } .stepper-pill .stepper-dot { @apply h-7 w-7 border-0 bg-white text-xs font-bold text-label-alt flex items-center justify-center; } .stepper-pill .stepper-dot.is-done { @apply bg-white text-primary; } .stepper-pill .stepper-dot.is-current { @apply bg-primary text-white ring-4 ring-primary/20; } .step-list .row { @apply flex items-center gap-3 py-2.5; } .step-list .num { @apply w-7 h-7 rounded-full bg-fill text-label-body text-xs font-bold flex items-center justify-center; }

주의

항목설명
OnboardingStepper흐름 판단(채권자·채무자·법인)은 그대로 두고, 그리는 부분만 Stepper variant="line"으로. 원+선 마크업은 지워요.
모바일 조각 바sm:hidden으로 따로 그리던 조각 바는 없애요. Line이 그대로 내려가고 라벨만 줄어요.
rolenav aria-label="진행 단계", 지금 단계 라벨에 aria-current="step".

Pagination

긴 목록을 페이지로 나눠 옮겨 다니는 조작. 표 아래 가운데에 번호 다섯 개와 양끝 화살표. 20건이 넘을 때만 쓰고, 무한 스크롤은 쓰지 않아요.

…1 / 12 · 234건

Anatomy

이전 화살표, 번호 다섯, 말줄임, 마지막 번호, 다음 화살표. 필요하면 오른쪽에 전체 개수.

…6 / 12 · 234건
  1. 1이전 · 아이콘 버튼 32 Ghost · 첫 페이지에선 비활성
  2. 2번호 · 32 정사각 Ghost · Label S 13 500 · 지금 페이지 앞뒤로 둘씩, 다섯 개
  3. 3지금 페이지 · Primary 채움 · Static/White 700
  4. 4말줄임 · 24 · Label/Alternative · 번호가 이어지지 않을 때만
  5. 5다음 · 마지막 페이지에선 비활성
  6. 6전체 · Caption 12 tabular · Label/Alternative · 왼쪽 여백 12

Variants

넷. 표 아래는 Default, 개수까지 알려주면 With total, 좁은 자리는 Compact, 한 페이지에 몇 건인지 바꿀 수 있으면 Page size.

…
Default번호 다섯 + 말줄임 + 마지막. 화살표는 아이콘
1 / 5 · 96건
With total오른쪽에 "지금 / 전체 · n건". 표 도구 줄에 개수가 없을 때
3 / 12
Compact화살표와 "지금 / 전체"만. 카드 안, 모바일
20건씩
Page size오른쪽 끝에 Select S. 20 · 50 · 100

States

DefaultGhost · Label/Body
HoverFill/Normal
CurrentPrimary · 700 · 누를 수 없음
Disabledopacity .4 · 양끝에서

Behavior

동작규칙
번호전체가 7쪽 이하면 번호를 전부 보여요(말줄임 없음). 8쪽부터는 지금 페이지 앞뒤 둘씩 다섯 개 + 첫 페이지 · 마지막 페이지, 이어지지 않는 자리에만 말줄임
이동페이지가 바뀌면 표 맨 위로 스크롤. 선택한 행과 필터는 유지
URL?page=3처럼 쿼리에. 새로 고쳐도, 뒤로 가도 그 페이지
개수한 페이지 20건 기본. 전체가 20 이하면 페이지네이션 자체를 그리지 않아요
키보드Tab으로 번호 이동, Enter. ←→는 이전·다음
모바일640 미만에서 Compact로 자동

Usage

채무자원금
김리바8,400,000
이바운3,200,000
이렇게표 바로 아래 가운데, 같은 카드 안에
채무자원금
김리바8,400,000
이건 피해요위아래 둘 다. 아래 하나만
이건 피해요번호 열 개. 다섯 + 말줄임으로
이건 피해요글자 버튼 화살표. 아이콘 버튼으로

Specification

부분크기글색여백
번호 · 화살표32 정사각 · radius 8Label S 13 500 tabularGhost · Label/Body · 호버 Fill/Normal사이 4
지금 페이지같음700Primary · Static/White—
말줄임폭 2413Label/Alternative—
전체—Caption 12 tabularLabel/Alternative왼쪽 12
Compact화살표 32 · 글 사이 813 · 지금 600Label/Title · 전체 Label/Alternative—
자리표 아래 · 가운데—표와 같은 카드위아래 14

Props

Prop값기본설명
page · onChangenumber · fn—1부터
total · pageSizenumber— · 20전체 건수와 페이지당 건수. 페이지 수는 여기서 계산
showTotalbooleanfalse"지금 / 전체 · n건"
compactbooleanauto640 미만이면 자동
pageSizeOptionsnumber[]—주면 오른쪽에 Select S
syncQuerystring'page'URL 쿼리 이름

쓰기

<Pagination page={page} total={total} pageSize={20} onChange={setPage} showTotal /> {/* Table과 함께 */} <Table rows={rows} … pagination={{ page, limit: 20, total, onChange: setPage }} />

클래스

.pagination { @apply flex items-center justify-center gap-1 py-3.5; } .pagination-btn { @apply w-8 h-8 rounded-lg text-[13px] font-medium tabular-nums text-label-body hover:bg-fill disabled:opacity-40 disabled:hover:bg-transparent; } .pagination-btn[aria-current="page"] { @apply bg-primary text-white font-bold pointer-events-none; } .pagination-dots { @apply w-6 text-center text-[13px] text-label-alt; } .pagination-total{ @apply ml-3 text-xs tabular-nums text-label-alt; }

주의

항목설명
Pagination.tsx지금 컴포넌트는 border 버튼과 "이전/다음" 글자예요. Ghost 아이콘 버튼과 말줄임 규칙으로 바꿔요.
arianav aria-label="페이지", 지금 페이지에 aria-current="page", 화살표에 aria-label.

Segmented control

같은 자리의 내용을 두세 갈래로 바꿔 보는 조작. 회색 트랙 안에서 흰 조각이 고른 쪽으로 미끄러져요. 페이지가 바뀌면 Tabs, 여럿을 고르면 Chip, 켜고 끄면 Switch.

Anatomy

회색 트랙, 그 안의 조각들, 고른 조각 뒤의 흰 판. 판만 움직이고 글자는 자리를 지켜요.

1
  1. 1트랙 · 높이 40 · radius 12 · Fill/Normal · 안쪽 4
  2. 2조각 · 높이 32 · 좌우 16 · Label M 14 500 · Label/Alternative · 호버 Label/Body
  3. 3고른 조각 · 뒤에 흰 판 radius 9 + 그림자 살짝 · 글자 Label/Title 600 · 판은 0.25s 미끄러짐

Variants

넷. 글자만 있는 Default가 기본이고, 뜻이 아이콘으로 더 빨리 읽히면 With icon, 개수를 알려주면 With count, 폼 안에서 폭을 채우면 Full width.

Default2~5 조각. 라벨은 한두 글자에서 넷까지
With icon아이콘 16 + 글자. 보기 방식 전환처럼 뜻이 그림으로 더 빠를 때
With count글자 뒤 개수 tabular Label/Alternative. 필터 결과가 얼마나 될지 미리
Full width폼 안에서 필드 폭을 채워요. 조각 폭은 균등

Sizes

S · 32조각 28 · 13 · 카드 머리
M · 40조각 32 · 14 · 기본
L · 44조각 36 · 15 · 모바일, 페이지 상단

States

UnselectedLabel/Alternative 500
HoverLabel/Body · 배경 없음
Selected흰 판 · Label/Title 600
Disabledopacity .4

Behavior

동작규칙
고르기누르면 바로 바뀌어요. 저장 버튼 없음. 흰 판이 0.25s에 걸쳐 옮겨가고 글자 색은 0.2s
개수2~5 조각. 여섯이 넘거나 라벨이 길면 Tabs나 Select
항상 하나고른 조각이 없는 상태는 없어요. 처음엔 첫 조각
키보드Tab으로 들어가고 ←→로 옮기면 바로 골라져요. role="radiogroup"
유지보기 방식처럼 취향이면 기억(localStorage), 필터면 URL 쿼리
Tabs와같은 데이터를 다르게 보면 Segmented, 다른 내용으로 넘어가면 Tabs. 화면에 둘 다 있으면 Tabs가 위

Usage

이렇게같은 차트를 기간만 바꿔 보기. 짧은 라벨 셋
이건 피해요페이지 이동을 Segmented로. Tabs나 Navigation
이건 피해요일곱 조각. 다섯을 넘으면 Select
이건 피해요여러 개 고르기. 그건 Chip이나 Checkbox

Specification

부분크기글색움직임
트랙S 32 · M 40 · L 44 · radius 12 (S 10) · 안쪽 4—Fill/Normal—
조각28 · 32 · 36 · 좌우 12 · 16 · 2013 · 14 · 15 · 500Label/Alternative · 호버 Label/Body글자 색 .2s
흰 판조각과 같음 · radius 9 (S 7)고른 글자 600Background/Normal · 그림자 0 1 3 8%left · width .25s cubic-bezier(.4,0,.2,1)
개수—12 tabularLabel/Alternative글자와 4

Props

Prop값기본설명
value · onChangeT · fn—항상 하나가 골라져 있어요
options{ value, label, icon?, count?, disabled? }[]—2~5개
sizesm · md · lgmd32 · 40 · 44
fullbooleanfalse폭 100%, 조각 균등

쓰기

<Segmented value={range} onChange={setRange} options={[{ value: 'month', label: '월' }, { value: 'week', label: '주' }, { value: 'day', label: '일' }]} /> <Segmented value={view} onChange={setView} size="sm" options={[{ value: 'list', label: '목록', icon: List }, { value: 'card', label: '카드', icon: LayoutGrid }]} /> <Segmented full value={type} onChange={setType} options={[{ value: 'individual', label: '개인' }, { value: 'sole', label: '개인사업자' }, { value: 'corp', label: '법인' }]} />

클래스

.segmented { @apply relative inline-flex p-1 rounded-xl bg-fill; } .segmented-thumb { @apply absolute top-1 bottom-1 rounded-[9px] bg-white shadow-[0_1px_3px_rgba(0,0,0,.08)] transition-[left,width] duration-250 ease-[cubic-bezier(.4,0,.2,1)]; } /* left·width는 JS가 고른 버튼 크기로 */ .segmented-item { @apply relative z-10 h-8 px-4 rounded-[9px] text-sm font-medium text-label-alt transition-colors hover:text-label-body disabled:opacity-40; } .segmented-item[aria-checked="true"] { @apply font-semibold text-label-title; } .segmented-count { @apply ml-1 text-xs tabular-nums text-label-alt; } .segmented-sm .segmented-item { @apply h-7 px-3 text-[13px]; } .segmented-lg .segmented-item { @apply h-9 px-5 text-[15px]; } .segmented-full { @apply flex w-full [&_.segmented-item]:flex-1; }

주의

항목설명
ViewToggle보기 방식을 드롭다운으로 바꾸던 ViewToggle은 Segmented(With icon)로. 두 갈래면 드롭다운이 과해요.
roleradiogroup + radio, aria-checked. 버튼이 아니라 라디오로 읽혀야 "하나만 고른다"가 전달돼요.
움직임 줄이기prefers-reduced-motion이면 판이 바로 옮겨가요.

페이지 구성

모든 화면은 같은 순서로 시작해요. 제목, 한 줄 설명, 요약, 목록 순으로.

페이지 헤더

회수 내역
CMS 출금 기반 상환 이행 현황
<h1 className="text-title-m text-label-title">회수 내역</h1> <p className="mt-1 text-[13.5px] text-label-alt">CMS 출금 기반 상환 이행 현황</p> 제목 Title M 22 · 700 · 오른쪽에 주 CTA 하나(있을 때만) 설명 13.5 Label/Alternative · 위 4 아래 20 (옛 page-title 32는 쓰지 않아요)

섹션 사이 간격은 20px. 인사형 제목은 “안녕하세요, 김리바님” + 오늘 날짜를 부제로 둡니다.

빈 화면

처음 온 사용자가 가장 먼저 보는 화면이에요. 사과하지 말고, 다음에 할 일 하나만 크게 보여줍니다.

아직 등록된 채권이 없습니다

첫 채권을 등록하면 상환 일정과 회수 현황을 자동으로 따라가요.

제목 22/800 tracking-tight 설명 14px text-secondary · 최대 500px · 가운데 CTA px-7 py-3 r12 · 화면에 단 하나 아래 3단계 안내 카드(bg #F7F7F7 · r12 · p-5)를 나란히

상태별 화면

상태표현
로딩스켈레톤(회색 #F4F4F4 블록). 스피너는 버튼 안에서만
빈 결과한 문장 + 되돌리는 행동(필터 초기화)
오류무슨 일이 있었는지 + 다시 시도. 코드·스택은 보여주지 않아요
권한 없음메뉴는 보이되 흐리게 + 툴팁으로 이유(“관리자만 신청할 수 있어요”)
성공토스트 한 줄. 화면을 막지 않아요

문구

돈과 법이 걸린 화면이에요. 친근함보다 정확함이 먼저고, 정확한 다음에 짧게 씁니다.

기본

항목규칙
말투존댓말 해요체. “등록되었습니다”보다 “등록했어요”. 단, 법적 고지·동의서는 문어체 그대로
용어채권자 / 채무자 / 상환 / 회수 / 위임 / 연체. 화면마다 다르게 부르지 않아요
주어사용자가 한 일은 사용자 기준으로. “시스템이 처리했습니다” ✕
길이버튼 2~6자, 설명 한 문장, 도움말 두 줄 이내

숫자 표기

대상표기비고
금액12,340,000원축약·소수점 없이
건수148명 · 7건단위 붙여쓰기
비율62%필요할 때만 소수점 한 자리(62.5%)
날짜2026.09.15요일이 필요하면 2026.09.15 (화)
기간2026.09.15 ~ 10.14같은 해면 뒤쪽 연도 생략
남은 기간D-7당일은 D-day

예시

이렇게

  • 계좌를 등록하면 상환금이 자동으로 들어와요.
  • 9월 25일에 출금될 예정이에요.
  • 10자리 숫자만 입력해 주세요.
  • 관리자만 신청할 수 있어요.

이건 피해요

  • 처리 중 오류가 발생하였습니다 (E-500)
  • 유효하지 않은 값입니다
  • 정말로 삭제하시겠습니까?!
  • Loading… 처럼 영문 섞기

배경과 면

화면의 배경은 두 갈래예요. 카드가 여럿인 앱 화면은 흰 페이지에 선 카드, 카드 한 장짜리 화면은 회색 페이지에 무테두리 카드. 이 둘을 섞지 않아요. 이 규칙은 쓰면서 계속 고쳐요. 예외가 생기면 여기 적어요.

두 갈래

화면페이지 배경카드예
앱 화면흰색 #FFFFFF흰 면 + Line/Alternative(#F0F0F0) 1px 테두리 · radius 12 · 그림자 없음. 표 머리글은 중성 회색 #F5F5F5, 검색창 · 필터도 흰 면 + 1px 선대시보드 · 목록 · 상세 · 설정 · 폼 + 요약 · 문서 서명 · 앱 안 온보딩 단계
카드 한 장 화면회색 #F5F6F7흰 면 · 테두리 · 그림자 없음 · radius 12. 회색 배경과의 대비로 경계를 만들어요로그인 · 회원가입 · 비밀번호 재설정(앱 밖, 사이드바 없는 화면만)
시스템 페이지흰색카드 없음. 글과 버튼만404 · 권한 없음 · 점검 중
회색 계열#F5F6F7 · #F7F7F7 · #F0F0F0 · #E4E4E4한 계열만. 페이지 바닥 #F5F6F7(중성에 아주 살짝 차가운 기), 카드 안 회색 블록 #F7F7F7, Fill #F0F0F0, Fill/Strong #E4E4E4, 표 머리글 #F5F5F5. 파란 기(#F7F8FA · #F0F1F3)나 붉은 기 회색을 섞지 않아요모든 화면

왜 나눴나: 회색 페이지 위에 회색 채움 요소(검색창 · 세그먼트 · 표 머리글)가 많으면 서로 겹쳐 보여요. 요소가 많은 앱 화면은 흰 바탕에서 선으로 구분하고, 요소가 적은 카드 한 장 화면은 회색 바탕에서 카드가 떠 보이게 해요.

카드 안

상황규칙
카드 안에 묶음카드 안에 카드를 넣지 않아요. 회색 블록 #F7F7F7(radius 8)로 묶어요. 동의 박스 · 서명 패드 · 요약 블록이 이 방식
회색 페이지 위 Neutral 버튼기본 Neutral(#F0F0F0)이 배경과 겹치면 Fill/Strong(#E4E4E4)로 한 단계 진하게. 흰 카드 안에서는 기본 Neutral
그림자떠 있는 것(메뉴 · 모달 · 툴팁 · 토스트)에만. 카드에는 쓰지 않아요
사이드바쓰지 않아요. 위 Top bar 64(로고 왼쪽 · 계정 오른쪽)가 기본. 콘텐츠 안쪽 40 32, 최대 1680(화면 폭을 따라 늘어남)

이렇게 · 이건 피해요

주제
2,400,000
이렇게앱 화면: 흰 페이지 위 선 카드
주제
2,400,000
이건 피해요그림자 카드. 그림자는 떠 있는 것에만
카드 안의 카드
이건 피해요카드 안에 카드. 회색 블록으로 묶어요

저장 방식

바뀐 값이 언제 저장되는지 사용자가 예측할 수 있어야 해요. 한 화면 · 한 카드 안에서는 방식을 하나만 써요. 이 규칙은 쓰면서 계속 고쳐요. 예외가 생기면 여기 적어요.

셋 중 하나

방식언제어떻게
① 저장 버튼필드가 있는 폼 · 카드카드(또는 화면) 맨 아래 오른쪽 Primary 하나. 누르면 "저장하는 중" 스피너 → Toast "저장했어요". 취소는 왼쪽 Neutral. 버튼을 비활성으로 두지 않아요 — 눌러야 무엇이 빠졌는지 알려 줄 수 있어요
② 즉시 저장켜고 끄기(Switch) · 역할 Select · 정렬 · 필터바꾸는 순간 저장하고 Toast 한 줄("저장했어요" · "역할을 뷰어로 바꿨어요"). 저장 버튼 없음. 실패하면 값을 되돌리고 Toast로 이유
③ 접어 두고 Modal가끔 하는 일(비밀번호 변경 · 상세 수정 · 초대)카드 머리 오른쪽 Neutral 버튼 하나(변경 · 수정) → Modal · Sheet에서 처리 → 닫히고 Toast. 폼을 화면에 펼쳐 두지 않아요. 필드가 많으면 Sheet 560

단계가 있는 흐름

상황규칙
임시 저장온보딩 · 신청처럼 여러 단계면 "다음"을 누를 때마다 임시 저장. 새로 고쳐도 그 단계로 돌아와요
나가기입력 중 다른 곳으로 가려 하면 Alert "작성 중인 내용이 사라져요" (입력이 지워지니 채움형: 취소 Neutral · 나가기 Danger). 저장된 게 없을 때만
끝흐름 전체의 끝(신청 완료 · 서명 완료)만 Result. 저장 한 번은 Toast

이렇게 · 이건 피해요

이렇게필드 카드: 취소 왼쪽 · 저장 오른쪽, 항상 활성
이건 피해요비활성 저장 버튼. 왜 안 되는지 숨겨요
알림 받기
이건 피해요Switch와 저장 버튼을 한 카드에. Switch는 즉시 저장

검증과 오류

틀린 걸 빨리, 고치는 법과 함께 알려요. 타이핑하는 동안 빨갛게 하지 않고, 떠날 때 판정하고, 제출할 때 첫 오류로 데려가요. 이 규칙은 쓰면서 계속 고쳐요. 예외가 생기면 여기 적어요.

언제 판정하나

시점규칙
필드를 떠날 때(blur)필수 · 형식(이메일 · 전화 · 금액)을 판정. 필드 테두리 Danger + 아래 문구(도움말 자리를 대신). 타이핑 중엔 판정하지 않아요
고치는 동안오류 상태에서 글자를 넣으면 바로 오류를 지워요. 다시 떠날 때 재판정
서버 확인아이디 · 이메일 중복처럼 서버가 아는 것은 입력을 멈춘 뒤 500ms에 확인. 필드 오른쪽 스피너 → 초록 체크 + "사용할 수 있어요" / Danger + "이미 쓰는 이메일이에요"
제출할 때전부 다시 판정하고 첫 오류로 스크롤 + 포커스. 비어 있는 필수 필드는 빨간 선 + 아래 문구 + 좌우 흔들림 .35s(동의 박스와 같은 움직임). 버튼은 항상 활성. 실패 후 버튼 라벨을 되돌려요
보조 동작은 판정 없음'추가' '삭제' '파일 추가' 같은 보조 버튼을 눌렀다고 빨갛게 하지 않아요. 제출(다음)을 누르면 빈 필수 필드 전부가 Danger 테두리 + 도움말 자리 문구 + 0.35초 좌우 흔들림이 되고 첫 오류로 스크롤·포커스. 제출 시점에 있던 필드만 판정하고, 그 뒤 드러난 필드(사업자를 고른 뒤 나오는 사업자번호)는 다음 제출까지 조용히. 고치면 즉시 풀려요
서버 실패필드에 귀속되면 그 필드 아래, 아니면 Toast(재시도 가능) · Banner(화면에 남아야 할 때). 로그인 실패는 비밀번호 아래 "이메일 또는 비밀번호가 맞지 않아요" 한 줄(어느 쪽인지 말하지 않음)

필수 동의

체크박스 아래 작은 글자만으로는 눈이 가지 않아요. 동의 박스로 묶고, 실패하면 박스 전체가 말해요.

동의해야 계속할 수 있어요
동의 박스 bg #F7F7F7 · r8 · p 14 16 · 하위 항목이 있으면 아래 Checkbox 세로(전체 ↔ 하위 연동) 실패 bg Danger 5% + border Danger 1px + 칸 Danger + 문구 · 0.35s 좌우 흔들림 · 박스로 스크롤 회복 켜는 순간 원래대로

문구

이렇게이건 피해요
내용을 입력해 주세요필수 항목입니다 · 값이 없습니다
이메일 형식이 아니에요Invalid email · 올바르지 않은 형식
8자 이상, 숫자와 영문을 섞어 주세요비밀번호 규칙 위반
동의해야 계속할 수 있어요약관에 동의하세요

오류 문구는 무엇이 틀렸는지가 아니라 어떻게 고치면 되는지를 말해요. "~해요"체, 기술 용어 없이.

위험 동작

되돌릴 수 없는 일은 눈에 덜 띄는 곳에 두고, 실행 전에 한 번 묻고, 끝난 뒤 짧게 알려요. 이 규칙은 쓰면서 계속 고쳐요. 예외가 생기면 여기 적어요.

어디에 두나

동작위치
항목 삭제 · 복제상세 머리 오른쪽 더보기(⋯) 메뉴 안. 삭제는 메뉴 마지막, Danger 색 글자. 머리에 삭제 버튼을 바로 두지 않아요
목록에서 내보내기 · 삭제행 오른쪽 Ghost 글자 버튼(내보내기). 여러 개면 체크 후 도구 줄에 Neutral
탈퇴설정 · 보안 섹션 맨 아래 별도 카드. 제목만 Danger 색, 설명 한 줄, 오른쪽 Danger 버튼 "탈퇴". "계정 삭제"라 쓰지 않고, 빨간 테두리 · 빨간 배경 카드도 쓰지 않아요

한 번 묻기

되돌릴 수 없는 실행은 채움형 Alert — 취소 Neutral + 실행 Danger. 보통의 확인에 쓰는 글자형(회색 글자 + 파랑 글자)으로 묻지 않아요. 닫기 X 없음, 스크림 클릭으로 닫히지 않음.

항목을 삭제할까요?

삭제하면 되돌릴 수 없어요.

Alert 확인형 제목 = "○○할까요?" · 설명 = 결과 한 줄("되돌릴 수 없어요" · "다시 초대할 수 있어요") 버튼 채움형 · 취소 Neutral(회색 채움) 왼쪽 → 실행 Danger(빨강 채움) 오른쪽 · 실행 버튼 라벨은 동사 그대로(삭제 · 탈퇴 · 내보내기), "확인" 금지 · 글자 버튼으로 묻지 않아요 끝난 뒤 Toast "삭제했어요" · 상세에서 지웠으면 목록으로 돌아감 답이 하나 정보만 알리는 Alert는 글자형 — Information 파랑 글자 버튼 하나("확인")

이렇게 · 이건 피해요

탈퇴되돌릴 수 없어요
이렇게별도 카드 · 제목만 빨강 · Danger 버튼
계정 삭제 영역
이건 피해요빨간 배경 · 빨간 테두리 카드
이건 피해요실행 버튼에 "확인". 무엇을 하는지 동사로

알림

알림 하나에 페이지를 만들지 않아요. 한 줄이면 Toast, 읽고 넘어가야 하면 Alert, 화면에 남아야 하면 Banner. Result는 흐름 전체의 끝에만. 이 규칙은 쓰면서 계속 고쳐요. 예외가 생기면 여기 적어요.

무엇을 쓰나

수단언제규칙
Toast저장 · 복사 · 삭제 · 보냄처럼 결과가 한 줄이고 사용자가 이미 다음 일을 할 수 있을 때아래 가운데 · 어두운 알약 · 2~3초 · 성공은 체크 아이콘. 되돌리기가 필요하면 오른쪽 글자 버튼 하나
Alert읽고 넘어가야 할 때(메일을 보냈어요 · 세션이 끝났어요) · 실행 전 확인제목 + 설명 한 줄. 답이 하나면 글자형 — 파랑 글자 "확인" 하나. 둘이면 보통의 확인은 글자형(취소 회색 글자 · 실행 파랑 글자), 되돌릴 수 없는 실행은 채움형(취소 Neutral · 실행 Danger)
Banner화면에 남아야 하는 상태(결제 실패 · 인증 필요 · 점검 예고)콘텐츠 맨 위 · 회색 배경 · 왼쪽 아이콘 · 오른쪽 행동 하나 · 닫기. 파랑 · 오렌지 틴트 배너 금지
Result흐름 전체의 끝(신청 완료 · 서명 완료) · 대체 화면(빈 · 오류 · 권한 없음)톤 색 원 64 + 흰 아이콘 · 제목 22 · 설명 15 · Ghost 왼쪽 · Neutral 오른쪽. 로그인 · 가입 · 비밀번호 재설정 · 저장 한 번에는 쓰지 않아요
필드 문구특정 필드의 문제그 필드 아래. 다른 수단으로 중복 알리지 않아요

색과 아이콘

상태색
성공Success #008A1E · 체크. Toast · Result · 필드 성공 문구
진행 · 대기 · 정보Information #2768FF. "서명 대기" · "초대 대기"처럼 잘못된 게 아닌 중간 상태. Warning 노랑을 쓰지 않아요
주의Warning #FFB724 · 곧 만료 · 확인 필요처럼 사용자가 봐야 할 것
실패 · 위험Danger #EB003B · 오류 문구 · 삭제 · 탈퇴
오렌지알림에 쓰지 않아요. 오렌지는 행동(Primary)과 진행 표시

이렇게 · 이건 피해요

저장했어요
이렇게저장 한 번은 Toast 한 줄
메일을 보냈어요
이건 피해요확인 한 번짜리 알림에 Result 페이지
서명 대기
이건 피해요대기 상태에 Warning 노랑. 정상 중간 상태는 Information

빈 · 로딩 · 오류

데이터가 없거나, 아직 안 왔거나, 못 왔을 때. 화면을 바꾸지 말고 그 자리에서 말해요. 이 규칙은 쓰면서 계속 고쳐요. 예외가 생기면 여기 적어요.

자리에서 말하기

상황표현
불러오는 중그 자리에 Skeleton(표 5행 · KPI 카드 · 문단). 머리글 · 도구 줄 · 제목은 진짜로. 스피너는 버튼 안과 짧은 확인에만
검색 결과 없음표 안 한 줄 "'검색어'로 찾은 결과가 없어요" + 검색어 지우기(Neutral S). 표 머리글은 남겨요
처음부터 없음표 · 카드 자리에 문장 한 줄("아직 항목이 없어요") + 만들기 Ghost 또는 CTA. 아이콘 · 일러스트 없음
숫자가 0"0"으로 두고 증감 문구만 생략. 비우거나 "—"로 바꾸지 않아요
값이 없음(상세)키는 두고 값에 "—"
불러오기 실패그 자리에 문장 + 다시 시도(Neutral S). 화면 전체를 오류 페이지로 바꾸지 않아요
페이지 자체가 없음 · 권한 없음 · 점검시스템 페이지(404 · 403 · 503). 코드 13 회색 → 제목 → 설명 → Ghost + Neutral. 그림 없음

빈 상태 예

머리글머리글머리글
아직 항목이 없어요
표 안 빈 상태 머리글 유지 · 본문 자리에 문장 13.5 Label/Body + 버튼 Neutral S · 위아래 32 검색 결과 없음 "'검색어'로 찾은 결과가 없어요" + 검색어 지우기 카드 빈 상태 카드 안 한 줄 "아직 없어요" + 추가 Ghost

이렇게 · 이건 피해요

이렇게그 자리에 Skeleton
이건 피해요화면 가운데 큰 스피너 하나
데이터가 없습니다
이건 피해요빈 상태에 아이콘 원 + "없습니다"체

로고

심볼과 워드마크, 그리고 쓰면 안 되는 방식이에요. 워드마크는 검정 한 가지, 색은 심볼 안의 네 조각에만 들어갑니다.

파일

로고(심볼 + 워드마크) 심볼 파비콘
logo.svg 136 × 38 심볼 + 워드마크 · 기본 symbol.svg 38 × 38 심볼만 · 모바일 헤더, 앱 아이콘 favicon.svg 33 × 33 워드마크의 a 글리프 · 탭 아이콘 워드마크 검정(#000) 한 가지 · 어두운 배경에서는 흰색으로 교체 조합 심볼 38 : 워드마크 92, 사이 간격 6px

심볼의 색

심볼은 검정 조각 열두 개와 색 조각 네 개, 모두 16조각으로 되어 있어요. 이 네 색은 로고 전용이고, 화면의 UI 색으로 가져다 쓰지 않습니다. 화면 색은 Semantic에서 고르세요.

자리값비고
왼쪽#FF5900비즈의 primary와 같은 오렌지. 유일하게 UI와 공유하는 색
위#009DFF로고 전용 블루 (UI의 tertiary와 다른 값)
오른쪽#C7F64F로고 전용 라임
아래#C0FFF9로고 전용 민트
나머지#000000심볼의 획과 워드마크

규칙

규칙내용
최소 크기심볼 20px, 조합 로고 높이 24px 이상. 그보다 작아지면 색 조각이 뭉개져요
여백사방으로 심볼 높이의 절반(19px 기준 약 10px) 이상 비웁니다
배경흰색·아주 옅은 회색 위에만. 사진이나 색이 있는 면 위에는 흰 판을 깔고 올려요
어두운 배경워드마크와 심볼의 검정 조각만 #FFFFFF로 교체하고, 색 네 조각은 그대로 둡니다
파비콘탭 아이콘은 워드마크의 a 글리프(favicon.svg)를 씁니다. 16px에서는 심볼의 갈래가 뭉개지기 때문이에요. 본문·헤더에는 쓰지 않아요
금지비율 변경, 회전, 그림자·그라디언트, 색 조각 교체, 심볼과 워드마크 간격 임의 조정

코드로 쓰기

SVG 파일을 그대로 쓰는 것이 기본이에요. 색을 바꿔야 하면 currentColor로 바꾼 사본을 따로 두지 말고, 어두운 배경용 파일을 하나 더 만들어 씁니다.

{/* 기본: 헤더, 문서, 이메일 */} <img src="/logo.svg" alt="로고" className="h-[22px]" /> {/* 심볼만: 모바일 헤더, 로딩 화면 */} <img src="/symbol.svg" alt="" className="h-7 w-7" aria-hidden /> {/* 파비콘 */} <link rel="icon" href="/favicon.svg" />

AI 스킬

ARC 규칙을 AI 코딩 도구가 알아서 따르게 하는 설명서 파일. 저장소에 스킬과 템플릿 사본을 한 번 넣어 두면, 누구든 Claude Code에 템플릿 URL과 화면 이름만 붙여서 같은 결의 ARC 화면을 만들 수 있어요.

설치

세 단계예요. 1 · 2는 저장소 안에서 한 번 실행하고 커밋해요. 그러면 팀 전체가 설치 없이 같은 버전을 쓰고, 규칙이 바뀌면 같은 명령을 다시 실행해 PR 하나로 갱신돼요.

1. 스킬 설치 (저장소 안, 권장)

Claude Code가 "ARC" · "리바운드" · 템플릿 URL을 보면 자동으로 읽는 규칙 파일이에요. 개인 설치는 .claude 대신 ~/.claude로 — 모든 프로젝트에서 쓰이지만 팀과 버전이 어긋날 수 있어요.

mkdir -p .claude/skills/arc-design && curl -fsSL https://gb-design-7ob.pages.dev/skill/SKILL.md -o .claude/skills/arc-design/SKILL.md

2. 템플릿 사본 받기

템플릿 갤러리는 회사 번호로 잠겨 있어요. 열 개의 .md를 design/templates/에 받아 두면 AI가 번호 없이 바로 읽어요.

KEY=회사번호 # 템플릿 갤러리 번호. 코드·커밋에 남기지 마세요 mkdir -p design/templates && for t in form form-grid onboarding auth dashboard list detail settings sign system; do curl -fsSL "https://gb-design-7ob.pages.dev/templates/$t.md?key=$KEY" -o "design/templates/$t.md"; done

각 .md에는 구조 · 토큰 · 컴포넌트 · 코드 뼈대와 함께 인터랙션(전환 시간 · 검증 · 로딩 동작)이 적혀 있어서, AI가 생김새뿐 아니라 동작까지 같게 만들어요. 템플릿이 바뀌면 같은 명령을 다시 실행하면 사본이 갱신돼요.

3. 토큰 넣기 (선택, 새 프로젝트만)

rebound-biz에는 이미 들어 있어요(frontend/tailwind.arc.preset.js · frontend/src/styles/arc.css). 새 저장소에서 처음 시작할 때만.

curl -fsSL https://gb-design-7ob.pages.dev/tokens/tailwind.preset.js -o tailwind.arc.preset.js mkdir -p src/styles && curl -fsSL https://gb-design-7ob.pages.dev/tokens/arc.css -o src/styles/arc.css
// tailwind.config.js const arc = require('./tailwind.arc.preset'); module.exports = { presets: [arc], content: ['./index.html', './src/**/*.{ts,tsx}'] };
/* src/index.css 맨 위 */ @import './styles/arc.css';

쓰는 법

단계설명
1 고르기만들 화면과 가장 가까운 템플릿을 갤러리에서 골라요. 열 개 — form · form-grid · onboarding · auth · dashboard · list · detail · settings · sign · system
2 붙이기Claude Code에 템플릿 URL과 화면 이름을 함께 붙여요 — 예) https://gb-design-7ob.pages.dev/templates/list 로 회수 현황 목록 화면 만들어 줘. 토큰 JSON이나 색 코드를 붙일 필요는 없어요(스킬이 알고 있어요)
3 만들기AI가 저장소의 design/templates/<이름>.md를 먼저 읽고 구조 → 토큰 → 컴포넌트 → 인터랙션 순서로 만들어요. 구조가 요청과 다르면 먼저 물어봐요
확인끝나면 AI가 어떤 템플릿 · 컴포넌트 · 토큰을 썼는지 세 줄로 보고하고, SKILL.md 9장 체크리스트를 스스로 돌려요. 보고에 없는 컴포넌트가 화면에 있으면 되물어 보세요
갱신규칙이나 템플릿이 바뀌면 설치 1 · 2 명령을 다시 실행해요(덮어쓰기). 스킬은 작업 전 최신본 버전을 확인하도록 되어 있어요

함께 쓰는 파일

파일용도주소
SKILL.md규칙 설명서. 절대 규칙, 토큰 표, 글자·레이아웃, 컴포넌트 30개 요약, 체크리스트/skill/SKILL.md
templates/<이름>.md템플릿 열 개의 설계 원문. 구조 · 토큰 · 컴포넌트 · 코드 뼈대 · 인터랙션 절. 저장소 design/templates/ 사본으로 읽어요/templates/<이름>.md
arc.cssCSS 변수 토큰/tokens/arc.css
tailwind.preset.jsTailwind 프리셋. presets: [require('./tailwind.arc.preset')]/tokens/tailwind.preset.js
arc.json기계용 토큰(색·간격·라운드·그림자·글자·레이아웃·모션)/tokens/arc.json
설치 안내공유용 한 장짜리 페이지/skill/

왜 스킬인가

보안 장치가 아니라 설명서예요. 없어도 URL을 붙이면 페이지는 만들어지지만 규칙을 자꾸 놓치거나 매번 프롬프트에 길게 써야 해요. 스킬이 있으면 키워드가 나올 때 자동으로 불려 와서 일관성이 생겨요. 사이트는 공개라 스킬에 비밀은 넣지 않아요.

토큰

프로젝트에 복사해 넣는 파일 두 개예요 — Tailwind 프리셋 tailwind.preset.js와 CSS 변수 arc.css. 아래 내용은 이 사이트의 /tokens/ 원본을 그대로 읽어 와요. 값은 Primitive와 같습니다.

토큰만 붙이면 색·간격·라운드만 맞아요. 화면을 같은 결로 만들려면 Resources · AI 스킬을 설치하고 템플릿 URL을 쓰세요.

Tailwind 설정

/tokens/tailwind.preset.js 원문. 프로젝트 루트에 tailwind.arc.preset.js로 저장하고 tailwind.config.js에 presets: [require('./tailwind.arc.preset')]를 넣어요.

불러오는 중…

CSS 변수

/tokens/arc.css 원문. src/styles/arc.css로 저장하고 src/index.css 맨 위에서 @import './styles/arc.css';. Tailwind를 쓰지 않는 페이지(정적 HTML, 이메일 템플릿)에서는 이 변수만 복사해 쓰세요.

불러오는 중…

어디에 있나

항목위치
토큰 원본이 사이트 /tokens/tailwind.preset.js · /tokens/arc.css
rebound-biz 사본frontend/tailwind.arc.preset.js (tailwind.config.js의 presets: [arc]) · frontend/src/styles/arc.css (src/index.css에서 import)
컴포넌트 클래스frontend/src/index.css @layer components + frontend/src/components/arc/* (Button · TextField · Checkbox · Radio · Dialog · KpiStrip · OnboardingShell · Spinner)
화면 카탈로그rebound-biz / design. 전체 라우트와 모달을 시나리오별로 확인
아이콘lucide-react
폰트Pretendard v1.3.9 (jsdelivr static)
© 2026 GroundBasic