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천만)은 쓰지 않아요.
날짜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 · 코드 순서로 읽어요.

Actions

Data Display

Inputs

Feedback

Loading

Navigation

Button

행동을 실행하는 컴포넌트. 기본은 회색 Neutral이고, 오렌지 Primary는 화면에서 가장 중요한 한 곳에만 포인트로 씁니다. 배경 없는 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.

Neutral기본. 특별한 일이 아니면 전부 이것. Fill/Normal 채움, Label/Title 글자, 테두리 없음
Primary화면에서 가장 중요한 행동 하나에만. 오렌지가 포인트로 남아야 눈에 띕니다
Ghost취소, 닫기, 표 안의 작은 행동. 배경 없이 글자만
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
Ghost없음Label/BodyFill/Normal 배경 · Label/Titleopacity .4
DangerSystem/Danger #EB003BStatic/Whiteopacity .9opacity .4
Size높이좌우 여백글자Radius아이콘
S32spacing-12Label S · Bold814
M36spacing-16Label M · Bold816
L48spacing-24Label L · Bold12없음
Icon only28816

Props

Prop기본설명
variantneutral · primary · ghost · dangerneutral종류. 기본은 회색, 화면의 주 행동 하나만 primary
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="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흰 배경 + 1px 테두리 버튼은 줌 배율에서 선이 끊겨 보여서 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
countnumber0이면 숨김, 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 52Background/Normal · 아래 Line/Alternative · 호버 Background/Alternative16 20 · 요소 사이 14
제목한 줄 말줄임Body M 15 600 · 읽음 500Label/Title · 읽음 Label/Body
설명한 줄 말줄임Body S 13Label/Alternative위 2
안 읽음 점6Primary제목과 6
아이콘 원36 · radius 10 · 아이콘 16Fill/Normal · Label/Body글과 14
메타 · chevronchevron 16Caption 12Label/Alternative · chevron Line/Strong사이 8
카드radius 12Card Default안쪽 0

Props

Prop기본설명
title · descriptionstringdescription 없으면 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. 선택 항목은 뒤에 (선택)
  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 + 도움말 자리에 오류 문구. 입력 중에는 지우지 않아요
빈 필수 값제출할 때첫 오류 필드로 스크롤하고 포커스. 상단 요약 배너는 쓰지 않아요
서버 오류응답 후관련 필드가 있으면 그 필드에, 없으면 폼 위에 한 줄
도움말과 오류같은 자리를 씁니다. 오류가 있으면 도움말은 사라져요
필수 · 선택대부분이 필수라 표시하지 않고, 선택 항목만 라벨 뒤에 (선택)
자동 형식입력 중전화번호와 사업자번호는 하이픈을 자동으로. 금액은 천 단위 콤마

Usage

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

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

하이픈 없이 입력해 주세요

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

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

Specification

부분크기색 · 선여백
라벨Label S 13 600Label/Title · (선택) Label/Alternative 500아래 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
Textarea최소 96 · radius 8Body S 14 · 행간 1.6같음10 14
Checkbox · Radio16 · radius 4 / 원Body S 14 라벨Line/Strong 1px → 켜지면 Information 채움라벨과 8
FocusInformation 1px · 그림자 없음
ErrorCaption 12Danger 테두리와 글자위 6
Disabled · Read-onlyLabel/Disable · Label/BodyBackground/Alternative 채움 · 선 Line/Alternative · 없음
도움말Caption 12Label/Alternative위 6
필드 사이20짝 필드 12 · 붙는 컨트롤 8

Props

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

쓰기

<TextField label="채무자 이름" 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="메모" optional 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

목록에서 찾을 때 쓰는 입력칸. 회색 채움에 검색 아이콘, 글자가 있으면 지우기 버튼. 입력하는 동안 결과가 따라오고, 결과가 없으면 표가 빈 상태로 말해줘요.

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

Anatomy

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

김리1
  1. 1컨테이너 · 높이 40 · radius 8 · Fill/Normal · 테두리 없음 · 포커스하면 흰 배경 + 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

채무자 검색
DefaultFill/Normal · 플레이스홀더
채무자 검색
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)Fill/Normal · 포커스 Background/Normal + Information 1px좌우 12 · 안쪽 간격 8
아이콘16 (S 14)Label/Alternative
지우기원 18 · X 10Line/Strong · 호버 Label/Alternative
시간debounce 300ms

Props

Prop기본설명
value · onChangestring · fnonChange는 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차이를 설명해야 할 때. 한 줄씩
10일
매월
25일
매월
Card고르는 것이 화면의 중심일 때. Card의 Selectable

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 · 고르면 링 5Line/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 · fnRadioGroup에
options{ value, label, description?, disabled? }[]2~5개
labelstring그룹 라벨. 없으면 aria-label
directionvertical · horizontalverticalhorizontal은 셋 이하
sizemd · lgmd16 · 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로 두고 점을 그려요.
플랜 선택PlanConfirm의 플랜 고르기는 Card Selectable(Radio 의미)로. 버튼 여러 개로 만들지 않아요.

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) · 안쪽 2Static/White · 그림자 0 1 2 15%이동 200ms
라벨 · 설명Body S 14 600 · Caption 12.5Label/Title · Label/Alternative행 위아래 14

Props

Prop기본설명
checked · onChangeboolean · fnonChange 안에서 바로 저장. 실패 시 되돌리기
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로 제출은 허용
크기시작 높이는 예상 글 양으로 정해 두고, 글이 넘치면 한 줄씩 늘어나요. 최대 줄을 넘으면 안에서 스크롤. 손잡이로 늘리게 두지 않아요
글자 수입력하는 동안 실시간. 제한을 넘어도 입력은 막지 않고 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 · DisabledText 필드와 같음

Props

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

쓰기

<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 12Background/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' · fn24시간 문자열
step1 · 5 · 10 · 15 · 305분 단위
min · max'HH:MM'고를 수 있는 범위. 밖은 흐리게
rangebooleanfalsetrue면 [start, end] 값
label · help · errorstringTextField와 같음

쓰기

<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.

Default라벨 + 필드. 플레이스홀더는 예시 값
%
Affix앞·뒤 단위. 글자와 8, Label/Alternative. 값은 오른쪽 정렬하지 않아요
Leading icon16 · Label/Alternative. 이메일, 전화, 검색처럼 뜻이 뻔한 것만
Clearable글자가 있을 때 지우기. 검색·필터처럼 자주 비우는 필드
Action필드 오른쪽에 Neutral 버튼. 라벨 하나 아래, 간격 8
Password눈 아이콘으로 보기·숨기기. 붙여넣기 막지 않아요

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 10Line/Strong · 호버 Label/Alternative
상태 아이콘채운 원 16 (메시지 앞 14) · 스피너 S 16Success #008A1E · Danger #EB003B · 글리프 Static/White
아이콘 16 · 히트 28Label/Alternative → 호버 Label/Body오른쪽 -6
메시지Caption 12Label/Alternative · Success · Danger위 6 · 아이콘 14와 6
규칙 목록체크 원 14Caption 12Line/Strong → Success 채움위 8 · 줄 사이 4
Focus · Error · Disabled · Read-onlyInformation 1px · Danger 1px · Background/Alternative + Label/Disable · Background/Alternative 선 없음

Props

Prop기본설명
label · helpstring라벨 필수. help는 메시지 자리의 기본 글
status · messageidle · checking · ok · error · stringidle상태 아이콘과 메시지 색이 함께. message가 help를 대신해요
prefix · suffixstring₩ · 원 · %
leadingIconLucideIcon16
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, (선택)은 Label/Alternative
  4. 4설명 · Caption 12.5 · Label/Alternative · 위 2

Variants

넷. 하나면 Single, 설명이 붙으면 With description, 여러 항목은 Group, 표 머리글처럼 전체 선택은 Indeterminate.

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

Sizes

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

States

UncheckedLine/Strong 1
CheckedInformation
IndeterminateInformation · 가로선
Disabledopacity .4

동의해야 계속할 수 있어요

ErrorDanger 테두리 · 아래 문구

Behavior

동작규칙
토글칸, 라벨, 설명 어디를 눌러도. Space로도
전체 선택모두 켜져 있으면 체크, 일부면 Indeterminate, 없으면 빈 칸. Indeterminate를 누르면 전체 선택
필수라벨 뒤 (필수). 제출할 때 안 켜져 있으면 Error와 문구, 첫 오류로 스크롤
저장폼의 저장 버튼으로 반영. 누르는 즉시 적용되어야 하면 Switch
Radio와여럿 중 하나만 고르면 Radio. 체크박스 여러 개를 배타적으로 만들지 않아요

Usage

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

Specification

부분크기여백
칸 M16 · radius 4 · 선 1Line/Strong → 켜짐 Information · Error Danger라벨과 8
칸 L20 · radius 5라벨 15같음라벨과 10
체크 · 가로선10 (L 14) · 선 3Static/White
라벨 · 설명Body S 14 · Caption 12.5Label/Title · Label/Alternative설명 위 2
(필수) · (선택)13 500Danger · Label/Alternative라벨과 4
Group항목 사이 12 · 가로 배열 24

Props

Prop기본설명
checked · onChangeboolean · fn
indeterminatebooleanfalse일부 선택. checked보다 우선
label · descriptionReactNode · stringlabel 필수. 숨기면 aria-label
required · optionalbooleanfalse(필수) · (선택) 표시
sizemd · lgmd16 · 20
errorstringDanger 테두리 + 아래 문구

쓰기

<Checkbox checked={agree.terms} onChange={v => set('terms', v)} required label="서비스 이용약관 동의" /> <Checkbox checked={agree.marketing} onChange={v => set('marketing', v)} optional 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
범위min·max 밖은 Disabled. 과거를 못 고르는 필드는 오늘 이전을 흐리게, 숨기지 않아요
키보드←→ 하루, ↑↓ 한 주, PageUp·Down 한 달, Enter 확정, ESC 닫기
모바일640 미만에서 Bottom sheet에 같은 달력. OS 기본 피커는 쓰지 않아요

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 12Background/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
오늘점 4Primary아래 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 · errorstringTextField와 같음

쓰기

<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여럿 중 하나를 고르는 카드. 안 고른 건 회색 채움, 고르면 오렌지 테두리

안내

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

Padding

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

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

States

Selectable만 상태가 있습니다. 나머지 카드는 누를 수 없어요.

10일
매월
DefaultFill/Normal
10일
매월
HoverFill/Strong
10일
매월
SelectedPrimary 테두리 1 · Primary 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 → 선택 시 Primary 4%선택 시 Primary/Normal 1px1220
ElevatedBackground/NormalElevation Card1224
회색 블록Background/Alternative #F7F8FA없음1220

Props

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

쓰기

<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-primary/[0.04] border-primary; } .card-block { @apply bg-[#F7F8FA] 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회색 페이지 위 카드에 테두리를 두른 화면이 남아 있어요. 교체 대상.
선택 카드 링선택 표시는 1px 테두리만. 글로우나 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 · 왼쪽
Hover · SelectedBackground/Alternative · Information 6%
합계 행48Bold · Label/TitleBackground/Alternative · 위 Line/Normal좌우 20
EmptyBody 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
단일답이 하나면 Primary 글자 버튼 · 안내만 할 때

Behavior

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

Usage

계좌 등록

상환금을 받을 계좌예요

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

채무자 등록

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

저장되었습니다

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

계좌 등록

정말 닫을까요?

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

Specification

부분크기색 · 그림자여백
스크림화면 전체Static/Black 40%좌우 16
컨테이너448 · 560 · radius 12Background/Normal · Elevation Modal
머리Title M 18 800 · 설명 Body S 14 Label/Alternative24 24 8 · 설명 위 4
닫기아이콘 18 · 히트 28Label/Alternative → 호버 Label/Title + Fill/Normal머리 오른쪽 끝
몸통Body S 1416 24 0 · 항목 사이 16
버튼 M 3620 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 · fnESC, 스크림, X가 모두 onClose를 불러요
variantdialog · sheetdialog폭과 구조. 모바일에선 자동으로 bottom sheet. 확인만 받으면 AlertDialog
title · descriptionstringtitle은 필수. 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

지금 결정을 받아야 할 때 띄우는 작은 대화상자. 제목은 질문, 버튼은 그 행동의 이름. 되돌릴 수 없는 일이면 Danger. 알리기만 할 거면 Toast.

채권을 삭제할까요?

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

Anatomy

스크림 위 폭 360의 흰 컨테이너. 제목과 설명, 그 아래 버튼 둘. 닫기 X는 없어요. 버튼으로만 답합니다.

채권을 삭제할까요?

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

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취소 · Neutral · 항상 왼쪽 · 라벨은 "취소"
  6. 6주 행동 · Primary 또는 Danger · 행동의 이름 · 답이 하나면 Primary 글자 버튼

Variants

넷. 주 행동이 무엇인지에 따라 버튼 색이 정해지고, 답이 하나면 버튼도 하나. 라벨이 길거나 모바일이면 세로로 쌓아요.

변경 사항을 저장할까요?

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

Confirm계속할지 묻기. 취소 + Primary

채권을 삭제할까요?

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

Destructive되돌릴 수 없는 일. 취소 + Danger. 포커스는 취소에

결제 수단이 만료됐어요

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

Acknowledge꼭 읽어야 하는 알림. 답이 하나면 채움 없이 Primary 글자 버튼. 흘려도 되면 Toast

위임 계약을 해지할까요?

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

Stacked라벨이 길거나 모바일. 주 행동이 위, 폭 100%

Buttons

둘까지. 왼쪽이 취소, 오른쪽이 주 행동. 라벨은 "예/아니오"가 아니라 행동의 이름이고, 취소는 늘 "취소". 답이 하나뿐이면 채운 버튼 대신 Primary 글자 버튼으로 가볍게.

ConfirmNeutral + Primary
DestructiveNeutral + Danger
AcknowledgePrimary 글자 버튼 하나 · 채움 없음

Behavior

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

Usage

계좌를 삭제할까요?

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

이렇게제목은 질문, 설명은 결과, 버튼은 행동의 이름

알림

정말 진행하시겠습니까?

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

저장되었습니다

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

계약을 해지할까요?

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

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

Specification

부분크기색 · 그림자여백
스크림화면 전체Static/Black 40%좌우 16
컨테이너폭 360 · radius 12Background/Normal · Elevation Modal24 24 24
제목Title S 16 700 · 행간 1.35Label/Title
설명Body S 14 · 행간 1.5 · 두 줄까지Label/Alternative위 4
버튼M 36 · 최소 폭 64Label M 13 700취소 Neutral · 주 행동 Primary 또는 Danger · 하나뿐이면 Primary 글자(Ghost)위 20 · 사이 8 · 오른쪽 정렬
Stacked버튼 폭 100%주 행동이 위사이 8
모바일폭 화면 − 32가운데

Props

Prop기본설명
open · onCloseboolean · fnonClose는 취소와 ESC에서만. 스크림 클릭은 무시
titlestring필수. 질문형 한 줄
descriptionstring두 줄까지
confirm{ label, onClick, tone?: 'danger', loading? }주 행동. tone이 danger면 포커스가 취소로 가요
cancel{ label } · false{ label: '취소' }false면 Acknowledge. confirm이 Primary 글자 버튼으로 그려져요
stackedbooleanauto라벨 합이 길거나 640 미만이면 자동으로 세로

쓰기

<AlertDialog open={ask} onClose={() => setAsk(false)} title="채권을 삭제할까요?" description="삭제하면 상환 이력도 함께 지워져요. 되돌릴 수 없어요." confirm={{ label: '삭제', tone: 'danger', onClick: remove, loading: removing }} /> {/* 확인만 */} <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-foot.is-single .btn { @apply bg-transparent text-primary font-bold hover:bg-primary/[.08]; } /* 답이 하나면 글자 버튼 */ .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.

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
아이콘20Information #2768FF · Success #008A1E · Warning #FFB724 · Danger #EB003B
제목Label M 14 600 · 행간 20Label/Title
설명Body S 13 · 행간 20Label/Body위 2
행동버튼 S 32Neutral위 10 · 한 줄형은 오른쪽 끝 Ghost
닫기18 · 히트 28Label/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에 툴팁을 붙여요

Usage

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

Specification

부분크기여백
컨테이너최대 폭 240 · radius 6Label/Body #555555 · 그림자 없음6 10 · 트리거와 8
Caption 12 500 · 행간 1.5Static/White
Rich폭 240제목 12 600 · 설명 12 · 80%같음8 12 · 제목 아래 2
꼬리6컨테이너와 같음트리거 가운데
Help 아이콘14Label/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 · 글자 폭 + 20Neutral · 보조 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 10Static/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 11 600 · Label/Alternative · 대문자 · 자간 .08em
  3. 3활성 항목 · Primary/Normal 글자와 아이콘 · Fill/Normal 배경 · 600
  4. 4항목 · 높이 38 · 아이콘 16 in 28 · Label M 14 500 · Label/Body · 개수는 Count 배지
  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
HoverStatic/Black 4%
Active600Primary/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 16Background/Normal · Elevation Dropdown안쪽 8 · 트리거와 6
항목 M높이 44 · radius 1014.5Label/Title · 호버 Fill/Normal좌우 12 · 아이콘과 10
항목 S높이 38 · radius 813.5같음좌우 10 · 컨테이너 안쪽 6 · radius 12
아이콘16Label/Alternative · 위험 Danger
체크14고른 항목 600Primary 글자와 체크왼쪽 · 안 고른 항목도 자리 16 비움
구분선1Line/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이고, 단계 번호가 중요하면 Pill. 할 일을 하나씩 끝내는 화면은 Vertical, 순서만 설명하는 안내는 List.

채권 정보플랜 제시마무리
Line기본. 온보딩 상단, 신청 진행 상태. 트랙 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, 라벨은 지금 단계만 남겨요.

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

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 · 지금 14Done 흰 + Primary 35% 테두리 · Upcoming 흰 + Line/Normal · Current Primary + 링 4 22%트랙 가운데
Pill 트랙36 · radius 18Fill/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) => voidDone 단계에서만 불려요

쓰기

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

동작규칙
번호지금 페이지 앞뒤 둘씩 다섯 개. 첫 페이지와 마지막은 항상 보이고, 이어지지 않으면 사이에 말줄임
이동페이지가 바뀌면 표 맨 위로 스크롤. 선택한 행과 필터는 유지
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 · fn1부터
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) · 안쪽 4Fill/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="page-title">회수 내역</h1> <p className="page-subtitle">CMS 출금 기반 상환 이행 현황</p> .page-title text-[32px] font-bold text-text-primary tracking-tight .page-subtitle text-[15px] text-text-secondary mt-1

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

빈 화면

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

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

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

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

상태별 화면

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

문구

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

기본

항목규칙
말투존댓말 해요체. “등록되었습니다”보다 “등록했어요”. 단, 법적 고지·동의서는 문어체 그대로
용어채권자 / 채무자 / 상환 / 회수 / 위임 / 연체. 화면마다 다르게 부르지 않아요
주어사용자가 한 일은 사용자 기준으로. “시스템이 처리했습니다” ✕
길이버튼 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… 처럼 영문 섞기

로고

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

파일

로고(심볼 + 워드마크) 심볼 파비콘
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 코딩 도구가 알아서 따르게 하는 설명서 파일. 한 번 설치하면 "ARC", "리바운드", 템플릿 URL이 나오는 작업에서 자동으로 적용돼요. 개발자는 문서 URL만 붙이면 됩니다.

설치

개인은 홈 폴더에, 팀은 rebound-biz 저장소 안에 두고 커밋해요. 저장소에 두면 설치 없이 모두 같은 버전을 쓰고, 규칙이 바뀌면 PR 하나로 갱신돼요.

mkdir -p ~/.claude/skills/arc-design && curl -fsSL https://gb-design-7ob.pages.dev/skill/SKILL.md -o ~/.claude/skills/arc-design/SKILL.md
# 저장소 안(권장) mkdir -p .claude/skills/arc-design && curl -fsSL https://gb-design-7ob.pages.dev/skill/SKILL.md -o .claude/skills/arc-design/SKILL.md

쓰는 법

단계설명
1 고르기만들 화면과 가장 가까운 컴포넌트나 템플릿 페이지의 URL을 복사해요
2 붙이기Claude Code에 "이 URL로 ○○ 페이지 만들어 줘". 스킬이 같은 주소의 .md를 먼저 읽어요
3 확인구조 → 토큰 → 컴포넌트 순서로 만들고, 끝에 체크리스트로 스스로 점검. 구조가 요청과 다르면 먼저 물어봐요
갱신같은 명령을 다시 실행하면 덮어써요. 스킬은 작업 전 최신본 버전을 확인하도록 되어 있어요

함께 쓰는 파일

파일용도주소
SKILL.md규칙 설명서. 절대 규칙 아홉, 토큰 표, 글자·레이아웃, 컴포넌트 29개 요약, 체크리스트/skill/SKILL.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을 붙이면 페이지는 만들어지지만 규칙을 자꾸 놓치거나 매번 프롬프트에 길게 써야 해요. 스킬이 있으면 키워드가 나올 때 자동으로 불려 와서 일관성이 생겨요. 사이트는 공개라 스킬에 비밀은 넣지 않아요.

토큰

새 프로젝트에 그대로 붙여 넣을 정의예요. 값은 Primitive와 같습니다.

Tailwind 설정

CSS 변수

Tailwind를 쓰지 않는 페이지(정적 HTML, 이메일 템플릿)에서는 이 변수만 복사해 쓰세요.

어디에 있나

항목위치
토큰 원본rebound-biz / tailwind.config.js
컴포넌트 클래스rebound-biz / src/index.css → @layer components
화면 카탈로그rebound-biz / design. 전체 라우트와 모달을 시나리오별로 확인
아이콘lucide-react
폰트Pretendard v1.3.9 (jsdelivr static)
© 2026 GroundBasic