# UI Design System — Shopify Polaris

> 어떤 스택에서든 동일한 톤을 이식하기 위한 단일 소스. Shopify Polaris(polaris.shopify.com)의 공개 체계를 웹 이식용으로 재해석한 문서다.
>
> **한 줄 정의**: **상인(merchant)이 일하는 어드민을 위한 시스템.** 옅은 그레이 캔버스(`#f6f6f7`) 위 화이트 카드로 작업 단위를 묶고, 그린(`#008060`)은 "가게에 이로운 주 액션"에만 쓴다. 모든 화면은 Page → Layout → Card → 컴포넌트의 고정 계층으로 조립된다.

---

## 목차

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

---

## 1. 디자인 원칙

| 원칙 | 정의 |
|---|---|
| **상인의 시간을 아낀다** | 화면의 목적은 아름다움이 아니라 주문 처리·재고 관리의 속도. 클릭 수·시선 이동을 줄인다. |
| **카드 = 작업 단위** | 관련 정보·액션은 한 카드에 — 카드 하나가 하나의 일이다. |
| **주 액션은 하나** | 페이지당 primary 버튼(그린)은 1개. 나머지는 default/plain으로 강등. |
| **캔버스 위 카드** | 옅은 그레이 캔버스 + 화이트 카드 + 얇은 보더 — 그림자 대신 면 대비. |
| **상태는 뱃지로** | 주문·상품 상태는 정형화된 뱃지(색+텍스트)로만 표기. |
| **차분한 톤** | 채도 낮은 그린·그레이. 화려함 대신 신뢰 — 돈을 다루는 화면이다. |
| **빈 상태도 설계** | 데이터가 없을 때가 첫인상 — Empty state에 안내와 CTA를 반드시 배치. |

---

## 2. 색상 토큰

### 2-1. 코어 팔레트

```css
:root {
  --bg:           #f6f6f7;  /* 어드민 캔버스 */
  --surface:      #ffffff;  /* 카드 */
  --surface-sub:  #fafbfb;  /* 카드 내 구획 */

  --text:         #202223;
  --text-sub:     #6d7175;  /* 보조·라벨 */
  --text-disabled:#8c9196;

  --border:       #e1e3e5;  /* 카드·구분선 */
  --border-input: #898f94;  /* 입력 보더 — 더 진하게 */

  --primary:        #008060;  /* Shopify 그린 — 주 액션 */
  --primary-hover:  #006e52;
  --primary-pressed:#005e46;

  --interactive:    #2c6ecb;  /* 링크·정보성 액션 */
  --critical:       #d72c0d;  /* 삭제·위험 */
  --critical-bg:    #fff4f4;
  --warning:        #b98900;
  --warning-bg:     #fff5ea;
  --success:        #008060;
  --success-bg:     #f1f8f5;
  --highlight:      #5bcdda;
  --highlight-bg:   #ebf9fc;
}
```

### 2-2. 사용 규칙

| 역할 | 토큰 |
|---|---|
| 본문·값 | `var(--text)` |
| 라벨·보조 | `var(--text-sub)` |
| 캔버스 | `var(--bg)` |
| 카드 | `var(--surface)` + `var(--border)` 1px |
| 주 액션 (페이지당 1개) | `var(--primary)` |
| 링크·보조 액션 | `var(--interactive)` |
| 삭제·되돌릴 수 없는 액션 | `var(--critical)` |
| 상태 뱃지 | `--success-bg`/`--warning-bg` 등 파스텔 면 + 진한 텍스트 |

> **핵심 규칙**: 그린은 "가게에 이로운 확정 액션"(저장·발송·게시)에만. 탐색·취소·보기엔 절대 쓰지 않는다.

---

## 3. 타이포그래피

### 3-1. 폰트

```css
:root {
  --font-sans: Inter, 'Noto Sans KR', -apple-system, BlinkMacSystemFont,
               'Segoe UI', system-ui, sans-serif;
}

body {
  font-family: var(--font-sans);
  font-size: 0.875rem;      /* 어드민 기본 14px */
  color: var(--text);
  background: var(--bg);
}
```

```html
<link rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Noto+Sans+KR:wght@400;500;700&display=swap" />
```

### 3-2. 타입 스케일

| 역할 | `font-size` | `font-weight` | 용도 |
|---|---|---|---|
| Heading XL | 24px | 600 | 페이지 제목 |
| Heading LG | 20px | 600 | 섹션 제목 |
| Heading MD | 16px | 600 | 카드 제목 |
| Heading SM | 14px | 600 | 카드 내 그룹 제목 |
| Body | 14px | 400 | 본문·표 |
| Body SM | 12px | 400 | 캡션·헬프 텍스트 |

- 숫자(금액·수량)는 `font-variant-numeric: tabular-nums`, 표에서 우측 정렬.
- 통화는 값과 단위를 붙여 표기 — `₩45,000`.

---

## 4. 레이아웃 계층

**Page → Layout → Card → 컴포넌트** — 이 계층을 건너뛰지 않는다.

```css
.page {
  max-width: 62rem;          /* 998px — 어드민 콘텐츠 폭 */
  margin-inline: auto;
  padding: 1.25rem 1.5rem;
}
.page__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1.25rem;
}
.page__title { font-size: 1.5rem; font-weight: 600; }

/* 2:1 레이아웃 (본문 + 사이드) */
.layout { display: grid; grid-template-columns: 2fr 1fr; gap: 1rem; }
@media (max-width: 768px) { .layout { grid-template-columns: 1fr; } }

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 0.5rem;
  padding: 1rem;
}
.card + .card { margin-top: 1rem; }
.card__title { font-size: 1rem; font-weight: 600; margin-bottom: 0.75rem; }
.card__section + .card__section {
  border-top: 1px solid var(--border);
  margin-top: 1rem;
  padding-top: 1rem;
}
```

---

## 5. 핵심 컴포넌트

### 5-1. 버튼

```css
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  min-height: 2.25rem;
  padding: 0.375rem 1rem;
  border-radius: 0.5rem;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.1s ease;
}
.btn--primary {
  background: var(--primary);
  color: #ffffff;
  border: 1px solid transparent;
}
.btn--primary:hover  { background: var(--primary-hover); }
.btn--primary:active { background: var(--primary-pressed); }

.btn--default {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border-input);
  box-shadow: 0 1px 0 rgba(0,0,0,0.05);
}
.btn--default:hover { background: #f6f6f7; }

.btn--critical { background: var(--critical); color: #fff; border: none; }
.btn--plain    { background: none; border: none; color: var(--interactive); padding: 0; }
.btn--plain:hover { text-decoration: underline; }
.btn:disabled  { background: #f1f1f1; color: var(--text-disabled); border-color: transparent; }
```

### 5-2. 상태 뱃지 — 시그니처

```css
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.125rem 0.5rem;
  border-radius: 62.4375rem;
  font-size: 0.75rem;
  font-weight: 500;
  background: #e4e5e7;
  color: var(--text);
}
.badge--success  { background: #aee9d1; color: #0c5132; }  /* 결제 완료 */
.badge--attention{ background: #ffea8a; color: #5e4200; }  /* 확인 필요 */
.badge--warning  { background: #ffd79d; color: #663d00; }  /* 부분 처리 */
.badge--critical { background: #fed3d1; color: #611207; }  /* 실패·취소 */
.badge--info     { background: #a4e8f2; color: #084e5e; }  /* 진행 중 */
```

### 5-3. 인덱스 테이블 (주문 목록)

```css
.index-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.index-table th {
  text-align: left;
  font-weight: 500;
  color: var(--text-sub);
  padding: 0.625rem 1rem;
  border-bottom: 1px solid var(--border);
}
.index-table td {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
}
.index-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; }
.index-table tbody tr:hover { background: #f6f6f7; cursor: pointer; }
.index-table tbody tr[aria-selected="true"] { background: var(--success-bg); }
```

### 5-4. 배너 (페이지·카드 상단 안내)

```css
.banner {
  display: flex;
  gap: 0.75rem;
  padding: 1rem;
  border-radius: 0.5rem;
  border: 1px solid var(--border);
  background: var(--surface);
  font-size: 0.875rem;
}
.banner--critical { background: var(--critical-bg); border-color: #febcb9; }
.banner--warning  { background: var(--warning-bg);  border-color: #ffd79d; }
.banner--success  { background: var(--success-bg);  border-color: #aee9d1; }
.banner__title { font-weight: 600; margin-bottom: 0.25rem; }
```

### 5-5. 빈 상태 (Empty State)

```css
.empty-state {
  text-align: center;
  padding: 3rem 1.5rem;
}
.empty-state__img   { width: 8rem; margin-bottom: 1rem; }
.empty-state__title { font-size: 1rem; font-weight: 600; margin-bottom: 0.375rem; }
.empty-state__desc  { color: var(--text-sub); margin-bottom: 1rem; }
```

- 빈 상태에는 반드시: 상황 설명 + 다음 행동 CTA(primary) 1개.

---

## 6. 인터랙션

- 전환 `0.1s ease` — 어드민은 즉답이 미덕.
- 저장되지 않은 변경이 있으면 상단에 **컨텍스트 저장 바**(저장/취소)가 떠서 이탈을 막는다.
- 행 클릭 = 상세 이동, 체크박스 = 벌크 선택 — 두 동작을 섞지 않는다.
- 토스트는 화면 하단 중앙, 실행 취소(Undo) 액션을 함께 제공.

---

## 7. 다크 모드

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

  --bg:           #1a1c1d;
  --surface:      #212425;
  --surface-sub:  #26292a;

  --text:         #e3e5e7;
  --text-sub:     #999fa4;
  --text-disabled:#6d7175;

  --border:       #333637;
  --border-input: #5b5f62;

  --primary:        #00a47c;   /* 그린 한 단계 밝게 */
  --primary-hover:  #00b98d;
  --primary-pressed:#008f6b;

  --interactive:  #6ea7f0;
  --critical:     #f56c58;
  --critical-bg:  #3a201c;
  --warning-bg:   #3a2f1a;
  --success-bg:   #1b2f27;
}
```

- 뱃지 파스텔 면은 다크에서 저채도 딥 톤으로 교체 — 형광화 금지.

---

## 8. 접근성

- `--text`(#202223) 대비 16.4:1, `--text-sub`(#6d7175) 5.3:1 — AA 통과.
- 상태 뱃지는 색+텍스트 병기 — 색만으로 주문 상태를 전달하지 않는다.
- 테이블 행 클릭과 별개로 상세 링크가 키보드 포커스 가능해야 한다.
- 컨텍스트 저장 바는 `role="status"`로 변경 사항 존재를 알린다.
- 파괴적 액션(삭제)은 critical 색 + 확인 다이얼로그 이중 안전장치.

---

## 9. 안티패턴

| 금지 | 이유 |
|---|---|
| 페이지당 primary 버튼 2개 이상 | 주 액션은 하나 |
| 그린을 탐색·취소·보기에 사용 | 그린 = 이로운 확정 액션 |
| 카드 없이 캔버스에 직접 콘텐츠 | 카드 = 작업 단위 계층 |
| 임의 색 뱃지 | 5종 상태 뱃지 규격만 |
| 그림자로 카드 구분 | 보더 + 캔버스 대비 |
| 빈 상태 방치 (테이블만 덩그러니) | Empty state 설계 필수 |
| 금액 좌측 정렬·비례 숫자 | 우측 정렬 + tabular-nums |
| 저장 없이 이탈 허용 | 컨텍스트 저장 바로 보호 |

---

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

### 10-1. Tailwind CSS v4

```css
@import "tailwindcss";

@theme {
  --color-bg:          #f6f6f7;
  --color-surface:     #ffffff;
  --color-text:        #202223;
  --color-text-sub:    #6d7175;
  --color-border:      #e1e3e5;
  --color-primary:     #008060;
  --color-interactive: #2c6ecb;
  --color-critical:    #d72c0d;
  --radius:            0.5rem;
}

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

### 10-2. Tailwind CSS v3

```js
module.exports = {
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        bg:          'var(--bg)',
        surface:     'var(--surface)',
        text:        'var(--text)',
        'text-sub':  'var(--text-sub)',
        border:      'var(--border)',
        primary:     'var(--primary)',
        interactive: 'var(--interactive)',
        critical:    'var(--critical)',
      },
      borderRadius: { DEFAULT: '0.5rem' },
    },
  },
};
```

### 10-3. React

공식 `@shopify/polaris` React 라이브러리가 있다. 직접 구축 시에도 Page→Layout→Card 계층과 "primary 1개" 규칙을 컴포넌트 구조로 강제한다.

---

## 11. 이식 가이드

### Step 1 — 토큰 복사

§2 + §7 블록을 전역 CSS에 붙인다.

### Step 2 — 폰트 연결

Inter + Noto Sans KR 링크를 추가하고 기본 14px을 설정한다.

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

| 패턴 | 핵심 CSS |
|---|---|
| **캔버스 위 카드** | `#f6f6f7` 캔버스 + 화이트 카드 + `#e1e3e5` 보더 |
| **상태 뱃지** | 파스텔 pill + 진한 텍스트 5종 규격 |
| **그린 primary 1개** | `background: #008060` — 페이지당 1개 검증 |

### Step 4 — 워크플로 검증

목록→상세→저장 흐름에서 클릭 수, 빈 상태, 미저장 보호가 설계됐는지 확인한다.

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

```
새 컴포넌트가 필요하다
  → 어느 카드(작업 단위)에 속하는가? 카드 밖이면 재검토
  → 액션인가?
    → 이로운 확정 액션? → primary (페이지당 1개 확인)
    → 파괴적? → critical + 확인 다이얼로그
    → 그 외 → default/plain
  → 상태 표기인가? → 5종 뱃지 중 선택
```
