---
name: new-ods-ui-builder
description: Figma 디자인을 번들된 ODS 디자인 시스템(design-tokens.js + icons.js + ods-components-index.json) 자산으로 실제 DOM/CSS/JS 기반 HTML 프로토타입을 빌드하고, pixel-diff로 픽셀 단위 정확도를 검증한다. 43개 ODS 컴포넌트 전수 사용법은 references/components.md 참조. 사용자가 "Figma로 프로토타입", "ODS로 정확히 그려줘", "토큰·아이콘·컴포넌트 써서 빌드", "pixel-diff로 검증" 같은 요청을 할 때 트리거.
---

# New ODS UI Builder — Figma → 컴포넌트 기반 HTML + pixel-diff 검증

ods-ui-build 스킬의 후속 버전. 두 가지가 다르다:

1. **자산이 스킬 내부에 번들됨** — `assets/design-tokens.js`, `assets/icons.js`, `assets/ods-components-index.json`. Downloads 경로 의존 없음.
2. **43개 ODS 컴포넌트 전수 사용법** — `references/components.md`에 모든 컴포넌트의 node_id · variant props · 권장 DOM이 정리됨. Figma 인스턴스를 만나면 이 문서에서 매핑 찾고 시작.

## 이 스킬이 하는 일

1. **Figma 노드 수집** — 사용자가 준 Figma URL에서 `fileKey`/`nodeId` 추출, Figma REST API로 노드 트리 + 실측 좌표/fill/font 수집
2. **ODS 컴포넌트 매칭** — Figma 인스턴스의 `componentId`/`componentSetId`를 `assets/ods-components-index.json`에서 조회 → `references/components.md`의 DOM 스니펫으로 렌더
3. **토큰 주입** — `assets/design-tokens.js`의 `color.light` → CSS 변수, `typography` → 자동 생성 CSS 클래스
4. **아이콘 주입** — `assets/icons.js`의 SVG를 `[data-icon="name"]` 속성으로 런타임 주입, 없는 아이콘은 Figma API로 export 후 `ASSETS` 객체에 인라인 등록
5. **DOM 빌드** — `<img class="bg">` 없이 실제 요소 기반 HTML 작성
6. **pixel-diff 실행** — Figma 렌더 vs 실제 HTML 렌더 비교, `diff.png` + `report.json` 생성
7. **수정 루프** — diff 비율 > 3%면 빨간 영역 분석 → HTML 수정 → 재실행 (최대 8회)

## 번들 자산 (이 스킬 폴더 내부)

세 가지 파일이 스킬 폴더의 `assets/`에 번들되어 있다. **항상 이 경로에서 로드** — 외부 Downloads 경로 참조 금지.

- `assets/design-tokens.js` — `window.TOKENS.color.light` (33 semantic), `.typography` (64 styles), `.primitive` (대부분 undefined, 사용 X)
- `assets/icons.js` — `window.ICONS[name]` 184개 SVG (snake_case key)
- `assets/ods-components-index.json` — 43개 ODS 컴포넌트 스펙 (node_id + variant properties + variants 배열)

HTML 로드 패턴:
```html
<script src="{스킬경로}/assets/design-tokens.js"></script>
<script src="{스킬경로}/assets/icons.js"></script>
```

빌드 결과물 폴더로 복사해서 상대 경로로 로드하는 게 일반적. 절대 경로 그대로 쓰면 `file://` fetch 이슈 없음.

## 셋업 — pixel-diff 의존성 (최초 1회, 자동)

pixel-diff 도구(`checklist.js`/`diff.js`/`diff-flat.js`/`inspect.js`/`hub.js`)는 puppeteer·pixelmatch·pngjs가 필요하다. **어떤 pixel-diff 스크립트를 돌리기 전에 node_modules가 없으면 먼저 설치한다** (있으면 건너뜀):

```bash
OHOUSE_SKILL_ROOT="${CLAUDE_PLUGIN_ROOT:-$(git rev-parse --show-toplevel)}"
PD="${OHOUSE_SKILL_ROOT}/skills/new-ods-ui-builder/pixel-diff"
[ -d "$PD/node_modules" ] || (cd "$PD" && npm install)
```

> `OHOUSE_SKILL_ROOT` 는 플러그인 실행 시 `${CLAUDE_PLUGIN_ROOT}`, repo project skill 실행 시 repo root 이다. puppeteer Chromium(~200MB)은 `~/.cache/puppeteer`에 1회만 받는다(이후 사용자 전체 공유). 플러그인 버전이 올라가면 캐시 경로가 바뀌므로 위 가드가 새 버전에서 다시 설치를 수행한다.

## 사용 시점

- "Figma 링크 드림, ODS로 프로토타입 만들어줘"
- "이 화면 정확한 UI로 그려줘"
- "토큰 써서 구현 + diff 돌려서 확인"
- "Box Button m 사이즈로 그려줘" → `references/components.md` 참고
- "ods-components-index의 43개 컴포넌트 중 X를 써서..."

## 절대 금지 사항

1. **스크린샷 bg 금지** — `<img class="bg" src="screens/....png">` 패턴 절대 사용 X
2. **토큰 값 하드코딩 금지** — `#141414` 직접 쓰지 말고 `window.TOKENS.color.light['FG/Neutral/foreground'].value` → CSS 변수
3. **아이콘 인라인 SVG 직접 그리기 금지** — `window.ICONS[name]`를 써야 함. icons.js에 없는 경우만 Figma API에서 export
4. **검증 생략 금지** — pixel-diff를 반드시 돌려 수치로 정확도 보고
5. **ODS 컴포넌트 자체 변형 금지** — Figma 인스턴스가 ODS 컴포넌트면 `references/components.md`의 권장 DOM을 따른다. 색·간격을 마음대로 바꾸지 않음 (디자이너 override는 따로 처리)

## 표준 워크플로우

### Step 1 — Figma URL 파싱

```
https://figma.com/design/{fileKey}/{fileName}?node-id={nodeId}
→ fileKey = {fileKey}
→ nodeId = {nodeId}  (하이픈은 콜론으로 변환: "1512-38967" → "1512:38967")
```

### Step 2 — Figma 노드 트리 수집

```js
const url = `https://api.figma.com/v1/files/${fileKey}/nodes?ids=${nodeId}&depth=20&geometry=paths`;
// X-Figma-Token 헤더로 Personal Access Token 전송
```

각 노드에서 추출:
- `absoluteBoundingBox` → 스크린 기준 상대 좌표 (`x - screen.x`, `y - screen.y`)
- `fills[0].color` → 16진수 hex 변환
- `style.fontFamily`, `fontWeight`, `fontSize`, `lineHeightPx`
- `characters` (텍스트 노드)
- `componentId` / `componentSetId` (인스턴스) → `assets/ods-components-index.json` 매칭

### Step 3 — ODS 컴포넌트 매칭

매칭은 `pixel-diff/match-ods.js` 의 `matchOdsComponent(node, odsIndex)` 로 수행한다. **3단계: componentId 직접 → variant node_id → 이름 폴백.**

```js
const { matchOdsComponent, collectMatches } = require('./pixel-diff/match-ods.js');
// 또는 트리 전체를 한 번에:
//   node pixel-diff/match-ods.js --node={workdir}/node.json --root={nodeId}
//   → JSON [{ id, name, ods, via:'componentId'|'variant'|'name' }]
```

**왜 이름 폴백이 필수인가 (드라이런 발견):** `ods-components-index.json` 의 componentId 는 ODS 마스터 라이브러리 파일의 node_id 다. 실제 화면/프로토타입 파일의 인스턴스는 **자기 파일 로컬 component id** 를 가지므로 componentId 만으로는 매칭 실패한다. 인스턴스 이름(`🌀 Tab` 등)은 마스터 컴포넌트 이름을 따르므로 이름(🌀·이모지 정리 후 대소문자 무시)으로는 안정적으로 매칭된다.

매칭되면 `references/components.md`에서 해당 컴포넌트의 DOM 스니펫·CSS 클래스 네이밍 적용.

### Step 3-1 — **빌드 전 체크리스트 생성 (필수)**

pixel-diff와 inspect.js는 픽셀 통계만 봄 → "요소 자체가 빠짐"은 절대 못 잡음 (예: 시트 핸들 바 32×4 누락 = 0.01%만 기여, 통계에 묻힘). **빌드 시작 전에 반드시 핵심 요소 체크리스트를 자동 생성:**

```bash
node ${OHOUSE_SKILL_ROOT}/skills/new-ods-ui-builder/pixel-diff/checklist.js \
  --node={workdir}/node.json \
  --root={nodeId} \
  --out={workdir}/checklist.md
```

산출물 `checklist.md`는 다음 카테고리로 정리됨:
- **System UI**: Status Bar, Dim 오버레이, Home Indicator, 시트 Handle/Grabber
- **Top Nav**: Standard Top Navigation, Pagination
- **Inputs**: Search Field, Chat Input, Text Field (정확한 cornerRadius/fill 포함)
- **Buttons**: Primary/Secondary Button
- **Chips**: 🌀 Chip × N
- **Selectable**: CheckBox, Radio, Switch — **빈 div 만들지 말고 data-icon 주입**
- **Icons**: [Icon] *, [Asset] * — 각각 어디에 어떤 아이콘 박는지
- **Cards**: Item Module, Carousel Card
- **자주 누락되는 핵심 요소 사전 점검** 섹션: 위 카테고리 중 잘 빠뜨리는 7가지를 별도 강조

→ 이 파일 옆에 두고 빌드하면 누락 거의 0. 끝나면 `- [ ]` → `- [x]`로 체크하면서 한 번 더 훑기.

### Step 4 — HTML 스켈레톤 생성

`references/example-home.html` 의 패턴을 기본으로:
- Pretendard 폰트 (CDN)
- `assets/design-tokens.js`, `assets/icons.js` 로드
- `applyTokens()` → CSS 변수, `applyTypography()` → `.text-*` 클래스, `applyIcons()` → SVG 주입
- 각 화면은 `<div class="screen" data-id="...">`

**checklist.md 항목을 1:1로 구현**. 카테고리 순서대로 진행하면 빠뜨릴 확률이 크게 줄어듦.

**필수 컨벤션**: `<title>`과 헤더 주석에 **Figma 프레임명·node-id·URL** 자동 포함 (식별·추적용):

```html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>{Figma 프레임명 그대로}</title>
<!--
  Figma frame : {프레임명}
  Node ID     : {nodeId}
  File        : {fileKey}
  Figma URL   : https://www.figma.com/design/{fileKey}?node-id={nodeId-with-dash}
  Built       : {YYYY-MM-DD}
-->
```

프레임명은 `node.json` → `nodes[rootId].document.name` 에서 그대로 가져옴 (한글/공백 보존). 브라우저 탭에서 식별 + 시간 지나도 출처 추적 가능.

상세 패턴은 `references/tokens-usage.md` 참조.

### Step 5 — pixel-diff 실행

**기본: `diff-flat.js` 사용 권장** (`diff.js`를 한번 돌린 뒤 figma의 투명 영역을 흰색으로 평탄화하고 재계산. Figma export는 frame bbox 밖을 alpha=0으로 두는데, HTML body는 보통 흰색이라 그게 가짜 diff로 잡힘.):

```bash
node ${OHOUSE_SKILL_ROOT}/skills/new-ods-ui-builder/pixel-diff/diff-flat.js \
  --file={fileKey} \
  --node={nodeId} \
  --html={출력 HTML 경로} \
  --token={Figma 토큰} \
  --viewport=375x812 \
  --scale=2 \
  --threshold=0.2 \
  --out={workdir}/diff-{화면명}
```

산출물: `figma.png`, `actual.png`, `diff.png`, `figma-flat.png`, `actual-flat.png`, `diff-flat.png`, `report.json`, `report-flat.json`.

판정은 `report-flat.json`의 `diffRatio` 기준. `fullPage: true` 화면(긴 스크롤)은 `--fullPage=true` 추가.

### Step 5-1 — structural inspect (필수)

**pixel-diff만으론 부족함** — 픽셀 평균은 모서리 둥글기 누락(차이 ~0.01%)·요소 시프트(겹치는 픽셀이 같은 흰색이라 차이 0)·작은 모양 변화(60×30 영역이 전체 168만 픽셀의 0.1%) 같은 구조적 이슈를 묻어버린다. `inspect.js`를 반드시 함께 돌려 시각화 + 셀별 분석을 받는다:

```bash
node ${OHOUSE_SKILL_ROOT}/skills/new-ods-ui-builder/pixel-diff/inspect.js \
  --out={workdir}/diff-{화면명} \
  --cell=40 \
  --topN=15
```

산출물 3종:
- `overlay-stripe.png` — figma와 actual을 20px 가로 줄무늬로 교차 합성. **요소 위치/크기 차이가 줄무늬 경계에서 단절로 보임**.
- `region-grid.png` — 화면을 40×40 render px(=20×20 design px) 그리드로 나눠 셀별 diff%를 빨간 강도로 칠함. **국소적 문제 영역**을 한눈에.
- `region-report.json` — diff% 상위 N개 셀의 디자인 좌표 + diff% 리스트. **"이 좌표를 보세요"** 가이드.

읽는 법: `region-report.json`의 worst 셀을 좌표로 짚어 → `overlay-stripe.png`에서 줄무늬 단절 확인 → 해당 영역 figma 노드 실측과 비교 → 수정.

### Step 6 — 수정 루프

1. `report-flat.json`의 `diffRatio` 확인 (flat 값이 진짜 시각 차이)
2. `region-report.json`의 worst 셀 좌표 ≤ 5%가 모두면 + diffRatio ≤ 3%: 합격, 보고
3. diff > 3% 또는 worst 셀 > 30%: 
   - `region-report.json`에서 worst 셀 좌표 추출 → Figma 노드 데이터에서 해당 좌표의 요소 찾기 → bbox/색/모양 실측 비교 → 수정
   - `overlay-stripe.png`에서 줄무늬 단절을 시각적으로 추가 확인
   - Step 5 재실행
4. 8회까지 반복. 그 후에도 > 3%지만 worst 셀이 모두 ≤ 10%이고 폰트/이미지 텍스처 같은 OS-level 영역에만 분포하면 합격 처리 (불가피 차이).

### Step 7 — 배치 모드 (여러 화면 동시 빌드 + hub 페이지) ⚙️ **옵션**

> 🚨 **이 단계는 옵션입니다.** 사용자가 명시적으로 "여러 화면 같이 만들어줘" / "허브 페이지 만들어줘" / "한 번에 N개 빌드" 같은 요청을 한 경우에만 사용. 단일 화면 빌드에는 적용하지 말 것.

여러 노드를 한 번에 빌드하고 카드 그리드 인덱스 페이지로 묶을 때 사용 (시퀀스 화면: 인트로 1→2→3, 온보딩 step1~step5 등).

**표준 패턴**:

```bash
# 1. 데이터 병렬 다운로드 (한 API 호출에 여러 노드 ID 함께)
curl -s -H "X-Figma-Token: $TOKEN" \
  "https://api.figma.com/v1/files/$FILE/nodes?ids=NODE1,NODE2,NODE3&depth=20" -o /tmp/nodes.json

# 2. 토큰 만료 검증 (필수)
if head -c 60 /tmp/nodes.json | grep -q "Token expired"; then
  echo "ERROR: Figma 토큰 만료. 새 토큰 받아서 재실행"; exit 1
fi

# 3. 각 노드별로 Step 3-1 ~ Step 6 진행 (병렬 또는 순차)

# 4. 모든 빌드 완료 후, hub 인덱스 페이지 생성
node ${OHOUSE_SKILL_ROOT}/skills/new-ods-ui-builder/pixel-diff/hub.js \
  --builds=build-NODE1,build-NODE2,build-NODE3 \
  --title="{시퀀스 이름}" \
  --out={hub 폴더명}/
```

`hub.js`가 자동으로:
- 각 빌드의 `<title>` (프레임명) 추출
- HTML 헤더 주석의 `Node ID` + `Figma URL` 파싱
- `figma-render.png` 썸네일 사용
- 최신 `diff-*/report-flat.json` 의 diff% → 색상 배지 (≤3% 초록, ≤7% 주황, >7% 빨강)

산출물: `{out}/index.html` (카드 그리드) + `{out}/{buildName}/` (각 빌드 복사본). Vercel에 통째로 deploy하면 한 URL에서 모든 화면 접근 가능.

### Step 7-1 — 화면 간 플로우 연결 ⚙️ **옵션**

> 🚨 **이 단계는 옵션입니다.** 사용자가 **명시적으로 플로우 정보를 줬을 때만** 실행 (이미지에 박스+화살표 표시 또는 텍스트 "1→2→3"). 사용자가 언급 안 하면 절대 자동 wire-up 하지 말 것 — 단일 빌드 / 독립 화면으로 두기.

여러 화면 빌드 후, 사용자가 **플로우(화면 → 화면 이동)를 알려준 경우** 버튼을 wire-up해서 클릭하면 다음 화면으로 가게 만든다.

**사용자 입력 형식**:
- 이미지: 박스+화살표로 트리거 표시 ("이 버튼 누르면 → 저 화면")
- 텍스트: "1 → 2 → 3 → hub" 같은 시퀀스

**표준 wire-up 패턴** — `<button>`을 `<a>` 태그로 교체:

```html
<!-- BEFORE -->
<button class="btn-start">시작하기</button>

<!-- AFTER -->
<a class="btn-start"
   href="../build-{nextNodeId}/index.html"
   style="text-decoration:none;display:inline-flex;align-items:center;justify-content:center">시작하기</a>
```

- `href`는 **hub 폴더 기준 상대 경로** (`../build-XXX/index.html`)
- `style`은 인라인으로 `text-decoration:none + flex align`만 추가 (기존 `.btn-start` 클래스 스타일은 보존)
- 마지막 화면의 출구는: 허브 (`../index.html`) / 외부 URL / 또는 클릭 안 되게 그대로 button 유지

**적용 후 hub sync 필수**: 원본 빌드 폴더를 수정한 뒤 hub 폴더 내부 복사본도 같이 갱신
```bash
for build in build-A build-B build-C; do
  cp /path/to/$build/index.html /path/to/hub/$build/index.html
done
```

이렇게 만들면 사용자가 hub의 1번 카드 → 인터랙티브하게 1→2→3→hub 순회 가능. Figma prototype 정식 연결과 동일한 사용감.

## 최종 보고 형식

```
✅ Build 완료 — {출력 경로}

검증 결과:
| 화면 | diff % | 합격 |
|------|--------|------|
| home | 2.1%   | ✓    |

사용한 자산:
- 토큰 키 N개
- 아이콘 M개
- ODS 컴포넌트 K개 (이름 + variant)

불가피 차이: {폰트 렌더링 차이 / OS 키보드 / 상태바 시스템 아이콘 등}
```

## 참조 문서

- `references/components.md` — **43개 ODS 컴포넌트 전수 사용법** (Figma 인스턴스 매칭 시 첫 참조)
- `references/tokens-usage.md` — TOKENS/ICONS 상세 사용 패턴
- `references/gotchas.md` — 자주 실수하는 포인트 (**매 작업 시작 시 점검**)
- `references/example-home.html` — 실제로 빌드된 홈 화면 예시
- `pixel-diff/README.md` — diff 도구 파라미터
- `pixel-diff/checklist.js` — **빌드 전 필수**. Figma node.json → 핵심 요소 마크다운 체크리스트 자동 생성
- `pixel-diff/diff.js` — 기본 diff (raw figma↔actual)
- `pixel-diff/diff-flat.js` — 권장. diff.js + alpha 평탄화 후 재계산
- `pixel-diff/inspect.js` — 구조적 inspect (stripe overlay + region grid + worst-cell report)
- `pixel-diff/hub.js` — 여러 빌드를 카드 그리드 인덱스 페이지로 묶음 (배치/시퀀스 빌드용)

## 주의

- **Figma 토큰 보안**: 토큰이 커밋되지 않게 주의. 환경변수나 한시적 입력으로 처리.
- **fullPage 화면**: result/composite 같은 스크롤 화면은 `--fullPage=true`.
- **scale=2 일치**: Figma 렌더/HTML 캡처 배율 모두 2로 맞춰야 픽셀 정확도 확보.
- **디자인 토큰 primitive는 대부분 undefined**: `primitive`는 피하고 `color.light`(semantic)만 사용.
- **ods-components-index의 변경**: ODS가 업데이트되면 `assets/`의 세 파일을 재추출해서 교체. `references/components.md`도 같이 업데이트 필요.
