# UI Design System — Wanted Montage

> 어떤 스택에서든 동일한 톤을 이식하기 위한 단일 소스. 원티드 몬타주(Montage) 공개 문서와 오픈소스 구현(montage-android)의 토큰 체계를 웹 이식용으로 재해석한 문서다.
>
> **한 줄 정의**: 화이트 배경 위 **쿨 뉴트럴(cool neutral) 그레이 스케일**과 **원티드 블루(`#0066ff`)** 포인트, **투명도 기반 라벨·라인 토큰**으로 구성된 **커리어 플랫폼** 시스템. 원시(atomic) 팔레트 위에 시맨틱 층을 얹고, 라이트/다크는 시맨틱 매핑 교체만으로 전환한다. 하나의 스펙이 Design·Web·iOS·Android 4개 탭으로 문서화되는 멀티 플랫폼 단일 소스가 핵심이다.

---

## 목차

1. [디자인 원칙](#1-디자인-원칙)
2. [색상 토큰](#2-색상-토큰)
3. [타이포그래피](#3-타이포그래피)
4. [레이아웃 & 라운드](#4-레이아웃--라운드)
5. [핵심 컴포넌트](#5-핵심-컴포넌트)
6. [인터랙션](#6-인터랙션)
7. [다크 모드](#7-다크-모드)
8. [접근성](#8-접근성)
9. [안티패턴](#9-안티패턴)
10. [프레임워크 어댑터](#10-프레임워크-어댑터)
11. [이식 가이드](#11-이식-가이드)

---

## 1. 디자인 원칙

| 원칙 | 정의 |
|---|---|
| **Atomic → Semantic 2계층** | 원시 스케일(`cool_neutral_50`, `blue_50`)은 정의에만 쓰고, 컴포넌트는 시맨틱 토큰(`label-normal`, `background-normal`)만 소비한다. |
| **투명도 라벨** | 보조 텍스트·라인은 불투명 그레이가 아니라 **투명도를 섞은 그레이**(`rgba`)로 정의 — 어떤 표면 위에서도 자연스럽게 혼색된다. |
| **단일 블루 포인트** | 액션·강조는 `primary`(blue) 하나로 수렴. 유채색 accent 팔레트는 데이터 시각화·일러스트 전용. |
| **상태 3색 고정** | positive(그린)·cautionary(오렌지)·negative(레드)만 의미 색상으로 사용. 각각 8% 투명 배경 짝을 가진다. |
| **멀티 플랫폼 단일 스펙** | 컴포넌트마다 Design / Web / iOS / Android 문서가 1:1 대응. 플랫폼별 재해석 없이 같은 토큰을 공유한다. |
| **다크 = 매핑 교체** | 다크 모드는 색을 새로 만들지 않는다. 같은 atomic 팔레트에서 시맨틱 매핑만 바꾼다 (blue_50 → blue_60 등). |
| **높이 고정 컴포넌트** | 버튼 등 인터랙티브 요소는 너비만 커스텀하고 높이는 시스템 값에 고정한다. |

---

## 2. 색상 토큰

### 2-1. 원시 스케일 (Atomic)

```css
:root {
  /* Common */
  --static-white: #ffffff;
  --static-black: #000000;

  /* Cool Neutral — UI 그레이의 원천 (밝음 99 → 어두움 5) */
  --cool-neutral-99: #f7f7f8;
  --cool-neutral-98: #f4f4f5;
  --cool-neutral-97: #eaebec;
  --cool-neutral-96: #e1e2e4;
  --cool-neutral-95: #dbdcdf;
  --cool-neutral-90: #c2c4c8;
  --cool-neutral-80: #aeb0b6;
  --cool-neutral-70: #989ba2;
  --cool-neutral-60: #878a93;
  --cool-neutral-50: #70737c;
  --cool-neutral-40: #5a5c63;
  --cool-neutral-30: #46474c;
  --cool-neutral-25: #37383c;
  --cool-neutral-22: #2e2f33;
  --cool-neutral-20: #292a2d;
  --cool-neutral-17: #212225;
  --cool-neutral-15: #1b1c1e;
  --cool-neutral-10: #171719;
  --cool-neutral-7:  #141415;
  --cool-neutral-5:  #0f0f10;

  /* Blue — 단일 액션 컬러 */
  --blue-90: #c9defe;
  --blue-80: #9ec5ff;
  --blue-70: #69a5ff;
  --blue-60: #3385ff;
  --blue-55: #1a75ff;
  --blue-50: #0066ff;   /* 기준 포인트 */
  --blue-45: #005eeb;
  --blue-40: #0054d1;

  /* Status */
  --red-50:    #ff4242;  /* negative */
  --green-50:  #00bf40;  /* positive */
  --orange-50: #ff9200;  /* cautionary */
}
```

Accent 팔레트(lime·cyan·lightblue·violet·purple·pink·redorange)는 각 11단계 스케일로 별도 존재 — 태그·차트·일러스트에서만 background(50 레벨)/foreground(40~48 레벨) 짝으로 사용한다.

### 2-2. 시맨틱 토큰 (라이트) — 컴포넌트가 실제로 쓰는 층

```css
:root {
  /* Primary */
  --primary-normal: var(--blue-50);       /* #0066ff */
  --primary-strong: var(--blue-45);       /* pressed */
  --primary-heavy:  var(--blue-40);

  /* Label — 투명도 기반 텍스트 위계 */
  --label-normal:      var(--cool-neutral-10);      /* 본문·제목 #171719 */
  --label-strong:      var(--static-black);
  --label-neutral:     rgba(46, 47, 51, 0.88);      /* cool_neutral_22 88% */
  --label-alternative: rgba(55, 56, 60, 0.61);      /* 보조 설명 */
  --label-assistive:   rgba(55, 56, 60, 0.28);      /* 힌트 */
  --label-disable:     rgba(55, 56, 60, 0.16);

  /* Background */
  --background-normal:      #ffffff;
  --background-alternative: var(--cool-neutral-99); /* 구분 영역 #f7f7f8 */
  --background-elevated:    #ffffff;                /* 카드·시트 표면 */

  /* Line — normal(투명) / solid(불투명) 이중 체계 */
  --line-normal:            rgba(112, 115, 124, 0.22);
  --line-neutral:           rgba(112, 115, 124, 0.16);
  --line-alternative:       rgba(112, 115, 124, 0.08);
  --line-solid-normal:      var(--cool-neutral-96); /* #e1e2e4 */
  --line-solid-neutral:     var(--cool-neutral-97);
  --line-solid-alternative: var(--cool-neutral-98);

  /* Status — 전경 + 8% 배경 짝 */
  --status-positive:   var(--green-50);
  --status-cautionary: var(--orange-50);
  --status-negative:   var(--red-50);
  --background-status-positive:   rgba(0, 191, 64, 0.08);
  --background-status-cautionary: rgba(255, 146, 0, 0.08);
  --background-status-negative:   rgba(255, 66, 66, 0.08);

  /* Fill — hover·pressed 등 상태 면 */
  --fill-normal:      rgba(112, 115, 124, 0.08);
  --fill-strong:      rgba(112, 115, 124, 0.16);
  --fill-alternative: rgba(112, 115, 124, 0.05);

  /* Interaction */
  --interaction-inactive: var(--cool-neutral-70);
  --interaction-disable:  var(--cool-neutral-98);

  /* Material */
  --material-dimmer: rgba(23, 23, 25, 0.52);        /* 모달 뒤 딤 */
}
```

### 2-3. 사용 규칙

| 역할 | 토큰 |
|---|---|
| 제목·본문 | `var(--label-normal)` |
| 보조 설명·입력 라벨 | `var(--label-alternative)` |
| 힌트·플레이스홀더 | `var(--label-assistive)` |
| 페이지 배경 | `var(--background-normal)` |
| 섹션 구분 배경 | `var(--background-alternative)` |
| 카드·시트 표면 | `var(--background-elevated)` + 그림자 |
| CTA·링크·선택 강조 | `var(--primary-normal)` |
| 표면 위에 얹는 구분선 | `var(--line-normal)` (투명 계열) |
| 표면 색과 무관한 실선 | `var(--line-solid-normal)` |
| 오류·성공·주의 | `--status-*` + `--background-status-*` 짝 |

> **핵심 규칙**: 겹침이 생기는 곳(카드 위 구분선, 이미지 위 보더)에는 투명 `line-normal` 계열을, 단독 실선에는 `line-solid` 계열을 쓴다. 둘을 혼용하면 다크 모드에서 위계가 무너진다.

---

## 3. 타이포그래피

### 3-1. 폰트 — Pretendard + Wanted Sans

UI 본문은 **Pretendard**, 브랜드·디스플레이 영역은 자체 오픈소스 서체 **Wanted Sans**를 쓴다.

```css
:root {
  --font-display: 'Wanted Sans Variable', 'Wanted Sans',
                  'Pretendard Variable', Pretendard, sans-serif;
  --font-sans: 'Pretendard Variable', Pretendard,
               -apple-system, BlinkMacSystemFont,
               'Noto Sans KR', system-ui, sans-serif;
}

body {
  font-family: var(--font-sans);
  color: var(--label-normal);
  background: var(--background-normal);
}
```

```html
<link rel="stylesheet"
  href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css" />
<link rel="stylesheet"
  href="https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/complete/WantedSansVariable.min.css" />
```

### 3-2. 타입 스케일 — 19레벨 × 3웨이트

Weight는 Regular(400) / Medium(500) / Bold(700) 3단계. 큰 사이즈일수록 자간을 음수로 좁힌다(최대 −0.032em).

| 토큰 | `font-size` | `line-height` | 용도 |
|---|---|---|---|
| `display1` | 56px | 72px | 히어로·마케팅 |
| `display2` | 40px | 52px | 대형 디스플레이 |
| `display3` | 36px | 48px | 디스플레이 |
| `title1` | 32px | 44px | 페이지 제목 |
| `title2` | 28px | 38px | 큰 제목 |
| `title3` | 24px | 32px | 표준 제목 |
| `heading1` | 22px | 30px | 섹션 제목 |
| `heading2` | 20px | 28px | 서브 섹션 |
| `headline1` | 18px | 26px | 강조 본문 |
| `headline2` | 17px | 24px | 강조 본문(소) |
| `body1` | 16px | 24px | 본문 |
| `body2` | 15px | 22px | 작은 본문 |
| `label1` | 14px | 20px | 버튼·입력 라벨 |
| `label2` | 13px | 18px | 작은 라벨 |
| `caption1` | 12px | 16px | 캡션 |
| `caption2` | 11px | 14px | 최소 캡션 |

### 3-3. Reading 변형

`body1`·`body2`·`label1`은 장문 독서용 **Reading 변형**을 별도로 가진다 — 같은 크기에 행간만 +2px (예: body1 24px → body1Reading 26px). 아티클·상세 설명에는 Reading을, UI 요소에는 기본형을 쓴다.

```css
.body1         { font-size: 1rem; line-height: 1.5rem; }
.body1-reading { font-size: 1rem; line-height: 1.625rem; }
.label1        { font-size: 0.875rem; line-height: 1.25rem; font-weight: 500; }
.caption1      { font-size: 0.75rem; line-height: 1rem; color: var(--label-alternative); }
```

---

## 4. 레이아웃 & 라운드

```css
:root {
  --radius-small:  0.25rem;   /* 4px — 뱃지·태그 */
  --radius-medium: 0.25rem;   /* 4px — 입력·작은 컨트롤 */
  --radius-large:  0.5rem;    /* 8px — 버튼·카드 */
  --radius-full:   624.9375rem; /* 풀 라운드 — 칩·아바타 */
  --spacing-unit:  0.25rem;   /* 4px 배수 스페이싱 */
}
```

- 모바일 우선. 콘텐츠는 단일 컬럼, 리스트 셀(List cell)이 정보 밀도의 기본 단위.
- 섹션 구분은 `--background-alternative` 면 + Section header 컴포넌트로.
- 겹침 표면(바텀시트·팝오버)은 `--background-elevated` + `--material-dimmer` 딤 조합.

---

## 5. 핵심 컴포넌트

전체 카탈로그 — **Actions**: Action area, Button, Chip, Icon button, Text button · **Contents**: Accordion, Avatar, Avatar group, Card, Content badge, List card, List cell, Play badge, Section header, Table · **Feedback**: Toast 등 · **Loading**: Loading, Skeleton · **Navigations**: Bottom navigation, Category, Page counter, Pagination, Progress indicator, Progress tracker, Tab, Top navigation · **Presentation**: Autocomplete, Bottom sheet 등. 아래는 시그니처 4종.

### 5-1. Button — Solid / Outlined × Primary / Assistive

색은 Primary(블루)와 Assistive(그레이) 딱 2종. 너비는 자유, **높이는 고정**.

```css
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  padding: 0.75rem 1.25rem;
  border: none;
  border-radius: var(--radius-large);
  font-size: 0.9375rem;
  font-weight: 500;
  cursor: pointer;
}
.btn--solid-primary {
  background: var(--primary-normal);
  color: #ffffff;
}
.btn--solid-primary:active { background: var(--primary-strong); }
.btn--solid-assistive {
  background: var(--fill-normal);
  color: var(--label-normal);
}
.btn--outlined-primary {
  background: transparent;
  border: 1px solid var(--line-normal);
  color: var(--primary-normal);
}
.btn:disabled {
  background: var(--interaction-disable);
  color: var(--label-disable);
}
```

- 아이콘은 leading 또는 trailing 한쪽만 — 동시 사용은 지양.
- 로딩 상태는 라벨을 스피너로 교체하고 너비를 유지한다.

### 5-2. Chip — 필터·선택의 기본 단위

```css
.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.5rem 0.875rem;
  border: 1px solid var(--line-solid-normal);
  border-radius: var(--radius-full);
  background: var(--background-normal);
  color: var(--label-normal);
  font-size: 0.875rem;
}
.chip[aria-pressed="true"] {
  border-color: var(--primary-normal);
  background: rgba(0, 102, 255, 0.08);   /* primary 8% */
  color: var(--primary-normal);
}
```

### 5-3. List cell — 리스트 행

```html
<button class="list-cell">
  <span class="list-cell__texts">
    <span class="list-cell__title">프론트엔드 개발자</span>
    <span class="list-cell__sub">원티드랩 · 서울</span>
  </span>
  <span class="list-cell__arrow">›</span>
</button>
```

```css
.list-cell {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  padding: 1rem 1.25rem;
  background: transparent;
  border: none;
  text-align: left;
  cursor: pointer;
}
.list-cell:active { background: var(--fill-normal); }
.list-cell__title { font-size: 1rem; color: var(--label-normal); }
.list-cell__sub   { font-size: 0.875rem; color: var(--label-alternative); }
```

### 5-4. Toast & 상태 배지

```css
.toast {
  position: fixed;
  bottom: 4rem;
  left: 50%;
  transform: translateX(-50%);
  background: var(--cool-neutral-20);
  color: var(--cool-neutral-99);
  padding: 0.75rem 1.25rem;
  border-radius: var(--radius-large);
  font-size: 0.9375rem;
}

.badge--positive {
  color: var(--status-positive);
  background: var(--background-status-positive);
  padding: 0.125rem 0.5rem;
  border-radius: var(--radius-small);
  font-size: 0.75rem;
  font-weight: 500;
}
```

---

## 6. 인터랙션

- hover·pressed 피드백은 `--fill-normal`(8%) → `--fill-strong`(16%) 투명 면 오버레이로 — 표면 색을 바꾸지 않는다.
- Primary 버튼의 pressed는 `--primary-strong`(blue_45)으로 한 단계 어둡게.
- 비활성은 두 토큰의 조합: 면 `--interaction-disable` + 텍스트 `--label-disable`.
- 로딩은 Skeleton이 기본. 버튼 내부 로딩은 라벨 교체형 스피너(§5-1).
- 모달·바텀시트 뒤에는 항상 `--material-dimmer`.

```css
.pressable { transition: background-color 0.15s ease; }
.pressable:hover  { background: var(--fill-alternative); }
.pressable:active { background: var(--fill-strong); }
```

---

## 7. 다크 모드

`.dark` 클래스 오버라이드. 같은 atomic 팔레트에서 시맨틱 매핑만 교체한다 — **블루는 50 → 60으로 밝게**, 배경은 cool neutral 하단 스케일로.

```css
.dark {
  color-scheme: dark;

  --primary-normal: var(--blue-60);       /* #3385ff */
  --primary-strong: var(--blue-55);
  --primary-heavy:  var(--blue-50);

  --label-normal:      var(--cool-neutral-99);   /* #f7f7f8 */
  --label-strong:      var(--static-white);
  --label-neutral:     rgba(194, 196, 200, 0.88);
  --label-alternative: rgba(174, 176, 182, 0.61);
  --label-assistive:   rgba(174, 176, 182, 0.28);
  --label-disable:     rgba(152, 155, 162, 0.16);

  --background-normal:      var(--cool-neutral-15);  /* #1b1c1e */
  --background-alternative: var(--cool-neutral-5);   /* #0f0f10 */
  --background-elevated:    var(--cool-neutral-17);  /* #212225 */

  --line-normal:            rgba(112, 115, 124, 0.32);
  --line-neutral:           rgba(112, 115, 124, 0.28);
  --line-alternative:       rgba(112, 115, 124, 0.22);
  --line-solid-normal:      var(--cool-neutral-25);
  --line-solid-neutral:     var(--cool-neutral-23, #333438);
  --line-solid-alternative: var(--cool-neutral-22);

  --status-positive:   #1ed45a;   /* green_60 */
  --status-cautionary: #ffa938;   /* orange_60 */
  --status-negative:   #ff6363;   /* red_60 */

  --fill-normal:      rgba(112, 115, 124, 0.16);
  --fill-strong:      rgba(112, 115, 124, 0.28);
  --interaction-inactive: var(--cool-neutral-40);
  --interaction-disable:  var(--cool-neutral-22);
}
```

- 표면 계층: background(15) → elevated(17) — 다크에서는 **밝아지는 방향**으로 쌓인다. alternative는 반대로 더 어둡다(5).
- status 색은 50 → 60 레벨로 일괄 시프트 — 어두운 배경에서 채도·명도를 확보.

---

## 8. 접근성

- `--label-normal`(#171719) 대비 17.4:1, `--label-alternative`(61% 혼색 시 ≈#858689) 3.6:1 — 보조 설명 전용, 본문 사용 금지.
- 투명도 라벨은 **배경에 따라 실제 대비가 달라진다** — 어두운 이미지 위에는 `label-*` 대신 `static-white` 계열을 쓴다.
- 상태는 색 + 아이콘 + 텍스트 3중 전달. `--background-status-*` 면만으로 의미를 전하지 않는다.
- 터치 타깃 최소 44×44px. 높이 고정 컴포넌트가 이를 보장한다.
- 크기는 `rem` 기반 — 시스템 글자 확대에 비례 대응.

---

## 9. 안티패턴

| 금지 | 이유 |
|---|---|
| 원시 스케일 직접 참조 (`var(--cool-neutral-40)`) | 시맨틱 층 우회 — 다크 매핑이 깨진다 |
| `line-normal`(투명)과 `line-solid` 혼용 | 겹침 여부 기준의 이중 체계 — 용도가 다르다 |
| accent 팔레트를 버튼·링크에 사용 | 액션은 primary 블루 단일 |
| accent background/foreground 짝 어기기 | 50 레벨 면 + 40 레벨 전경이 대비 보장 짝 |
| 버튼 높이 커스텀 | 높이는 시스템 고정 — 너비만 조절 |
| `label-assistive` 이하를 본문에 사용 | 대비 미달 — 힌트·플레이스홀더 전용 |
| leading + trailing 아이콘 동시 사용 | 버튼 아이콘은 한쪽만 |
| 다크 모드에서 라이트 hex 하드코딩 | 시맨틱 토큰이 스왑을 담당한다 |

---

## 10. 프레임워크 어댑터

### 10-1. Tailwind CSS v4

```css
@import "tailwindcss";

@theme {
  --color-primary:        #0066ff;
  --color-label:          #171719;
  --color-label-alt:      rgba(55, 56, 60, 0.61);
  --color-background:     #ffffff;
  --color-background-alt: #f7f7f8;
  --color-line:           rgba(112, 115, 124, 0.22);
  --color-line-solid:     #e1e2e4;
  --color-positive:       #00bf40;
  --color-cautionary:     #ff9200;
  --color-negative:       #ff4242;
  --radius-small:         0.25rem;
  --radius-large:         0.5rem;
}

@custom-variant dark (&:where(.dark, .dark *));
```

### 10-2. Tailwind CSS v3

```js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        primary:          'var(--primary-normal)',
        label:            'var(--label-normal)',
        'label-alt':      'var(--label-alternative)',
        background:       'var(--background-normal)',
        'background-alt': 'var(--background-alternative)',
        line:             'var(--line-normal)',
        'line-solid':     'var(--line-solid-normal)',
      },
      borderRadius: { small: '0.25rem', large: '0.5rem' },
    },
  },
};
```

### 10-3. React

웹 공식 구현은 `@wanteddev/wds` 패키지 컨벤션을 따른다. 자체 구현 시에도 같은 프롭 어휘(`variant`, `color`)를 유지한다.

```tsx
import { FlexBox, Button } from '@wanteddev/wds';

<Button color="primary">지원하기</Button>
<Button variant="outlined" color="assistive">저장</Button>
```

---

## 11. 이식 가이드

### Step 1 — 토큰 2계층 복사

§2 atomic 스케일 + 시맨틱 층, §7 다크 블록을 전역 CSS에 붙인다.

### Step 2 — 폰트 연결

Pretendard(본문)와 Wanted Sans(디스플레이) CDN을 `<head>`에 추가한다.

### Step 3 — 시그니처 3종 적용

| 패턴 | 핵심 CSS |
|---|---|
| **Button** | Solid/Outlined × Primary/Assistive, 높이 고정, `border-radius: 8px` |
| **투명 라벨 위계** | 보조 텍스트는 `rgba` 라벨 토큰 — 불투명 그레이 금지 |
| **fill 오버레이** | hover/pressed는 표면 교체가 아닌 8%/16% 투명 면 |

### Step 4 — 다크 검증

`.dark` 토글 후 (1) 표면이 15→17로 밝아지며 쌓이는지, (2) 블루·status가 60 레벨로 시프트됐는지 확인한다.

### 신규 컴포넌트 결정 트리

```
새 컴포넌트가 필요하다
  → 기존 카탈로그(Actions/Contents/Navigations/Presentation)의 조합으로 가능한가?
    yes → 조합 사용 (List cell + Content badge 등)
    no  → 시맨틱 토큰만으로 색을 구성할 수 있는가?
      yes → 추가하되 높이 고정·투명 라벨·fill 오버레이 규칙 상속
      no  → atomic 팔레트에 색을 추가하는 것부터 재검토
```
