컴포넌트 스펙
모든 컴포넌트는 cn() 유틸리티(src/utils/cn.ts)를 사용하고, CSS 변수 토큰을 통해 색상을 지정한다.
Button
변형 (Variant)
| Variant |
배경 |
텍스트 |
테두리 |
용도 |
primary |
--color-primary |
white |
none |
주요 CTA |
secondary |
--color-secondary |
--color-secondary-text |
none |
보조 액션 |
accent |
--color-accent |
--color-accent-text |
none |
포인트 액션 |
outline |
transparent |
--color-primary |
--color-primary |
조용한 강조 |
ghost |
transparent |
--color-text-secondary |
none |
최소 강조 |
danger |
--color-danger |
white |
none |
삭제, 경고 |
사이즈
| Size |
padding |
font-size |
height |
border-radius |
sm |
px-3 py-1.5 |
text-sm |
32px |
rounded-md |
md |
px-4 py-2 |
text-base |
40px |
rounded-lg |
lg |
px-6 py-3 |
text-lg |
48px |
rounded-lg |
상태
- disabled:
opacity-50 cursor-not-allowed pointer-events-none
- loading: 스피너 아이콘 표시, 텍스트 유지, disabled 처리
- hover: 각 variant에 맞는
-hover 토큰 적용
- active: 각 variant에 맞는
-active 토큰 적용
- focus-visible:
ring-2 ring-[var(--color-primary)] ring-offset-2
구현 예시
const buttonVariants = {
primary: 'bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-hover)] active:bg-[var(--color-primary-active)]',
secondary: 'bg-[var(--color-secondary)] text-[var(--color-secondary-text)] hover:bg-[var(--color-secondary-hover)]',
accent: 'bg-[var(--color-accent)] text-[var(--color-accent-text)] hover:bg-[var(--color-accent-hover)]',
outline: 'border border-[var(--color-primary)] text-[var(--color-primary)] hover:bg-[var(--color-primary-subtle)]',
ghost: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] hover:text-[var(--color-text-primary)]',
danger: 'bg-[var(--color-danger)] text-white hover:opacity-90',
};
Badge
상태, 카테고리, 태그 표시에 사용한다. Shape에 따라 Pill과 Filled로 구분한다.
Pill 변형 (rounded-full) — 상태 표시용
| Variant |
용도 |
Light 배경 |
Dark 배경 |
default |
기본 |
--color-bg-base |
--color-bg-base |
success |
성공/활성 |
green-50 |
green-500/15 |
warning |
경고/대기 |
amber-50 |
amber-500/15 |
danger |
에러/비활성 |
red-50 |
red-500/15 |
info |
정보/식별자 |
blue-50 |
blue-500/15 |
Filled 변형 (rounded-md) — 카테고리/서비스 라벨용
Chart Palette와 1:1 매핑. className="rounded-md"를 추가하여 Filled shape 적용.
| Variant |
Chart # |
Light 배경 |
Light 텍스트 |
Dark 배경 |
Dark 텍스트 |
blue |
1 |
primary-100 |
primary-900 |
primary-900 |
primary-300 |
gold |
2 |
accent-100 |
accent-900 |
accent-900 |
accent-300 |
rose |
3 |
#fce4ec |
#B5607D |
rgba(212,131,155,0.12) |
#D4839B |
teal |
4 |
#e0f2f1 |
#2E7D6E |
rgba(91,181,166,0.12) |
#5BB5A6 |
lavender |
5 |
#f3e8ff |
#7B5DA5 |
rgba(183,159,212,0.12) |
#B79FD4 |
coral |
6 |
#fff3e0 |
#A86440 |
rgba(212,149,107,0.12) |
#D4956B |
주의: success, warning, danger, info는 Pill 전용. Filled에 사용 금지.
스펙
- Pill:
rounded-full, px-2 py-0.5, text-xs font-medium
- Filled:
rounded-md, px-3 py-1, text-xs font-medium
- 사이즈:
sm (px-1.5 py-0.5 text-[10px]), md (px-2 py-0.5 text-xs)
Card
콘텐츠를 담는 기본 컨테이너.
구조
<div class="rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-surface)] shadow-sm">
<!-- Card Header -->
<div class="border-b border-[var(--color-border)] px-6 py-4">
<h3 class="text-base font-semibold text-[var(--color-text-primary)]">제목</h3>
<p class="text-sm text-[var(--color-text-secondary)]">부제목</p>
</div>
<!-- Card Body -->
<div class="p-6">
<!-- 콘텐츠 -->
</div>
<!-- Card Footer (선택) -->
<div class="border-t border-[var(--color-border)] px-6 py-4">
<!-- 액션 버튼 등 -->
</div>
</div>
변형
| Variant |
설명 |
default |
border + shadow-sm |
elevated |
shadow-md, 모달 내부 카드 |
flat |
border만, shadow 없음 |
interactive |
hover 시 shadow-md + scale-[1.01] |
Input
기본 스펙
<div class="flex flex-col gap-1.5">
<label class="text-sm font-medium text-[var(--color-text-primary)]">레이블</label>
<input
class="
w-full rounded-lg border border-[var(--color-border)]
bg-[var(--color-bg-surface)] px-3 py-2
text-base text-[var(--color-text-primary)]
placeholder:text-[var(--color-text-tertiary)]
focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent
disabled:opacity-50 disabled:cursor-not-allowed
"
placeholder="입력하세요"
/>
<p class="text-xs text-[var(--color-text-tertiary)]">도움말 텍스트</p>
</div>
상태
| 상태 |
테두리 색상 |
| 기본 |
--color-border |
| 포커스 |
--color-primary (ring) |
| 오류 |
--color-danger |
| 성공 |
--color-success |
| 비활성 |
--color-border + opacity-50 |
사이즈
| Size |
padding |
font-size |
height |
sm |
px-3 py-1.5 |
text-sm |
32px |
md |
px-3 py-2 |
text-base |
40px |
lg |
px-4 py-3 |
text-lg |
48px |
Modal
구조
<!-- Backdrop -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<!-- Dialog -->
<div class="
relative w-full max-w-md mx-4 rounded-2xl
bg-[var(--color-bg-elevated)] shadow-xl
">
<!-- Header -->
<div class="flex items-center justify-between border-b border-[var(--color-border)] px-6 py-4">
<h2 class="text-lg font-semibold text-[var(--color-text-primary)]">제목</h2>
<button class="...">✕</button>
</div>
<!-- Body -->
<div class="px-6 py-6">...</div>
<!-- Footer -->
<div class="flex justify-end gap-3 border-t border-[var(--color-border)] px-6 py-4">
<Button variant="ghost">취소</Button>
<Button variant="primary">확인</Button>
</div>
</div>
</div>
사이즈
| Size |
max-width |
sm |
max-w-sm (384px) |
md |
max-w-md (448px) |
lg |
max-w-lg (512px) |
xl |
max-w-xl (576px) |
접근성
role="dialog", aria-modal="true", aria-labelledby 필수
- 열릴 때 첫 번째 포커스 가능 요소로 포커스 이동
- Escape 키로 닫기
- Backdrop 클릭 시 닫기 (선택적)
- 열린 동안 body 스크롤 잠금
Toast
위치
fixed bottom-4 right-4 z-[60] (Modal보다 위)
변형
| Variant |
아이콘 |
색상 |
success |
CheckCircle |
--color-success |
warning |
AlertTriangle |
--color-warning |
error |
XCircle |
--color-danger |
info |
Info |
--color-info |
구조
<div class="
flex items-start gap-3 w-80 rounded-xl p-4
bg-[var(--color-bg-elevated)] shadow-lg
border border-[var(--color-border)]
">
<!-- 아이콘 -->
<!-- 텍스트 -->
<!-- 닫기 버튼 -->
</div>
동작
- 기본 자동 닫힘: 4000ms
- 진입:
translate-y-2 opacity-0 → translate-y-0 opacity-100
- 퇴장:
translate-y-0 opacity-100 → translate-y-2 opacity-0
- 트랜지션:
duration-200 ease-out