Files
Figma-to-HTML/PROCESS.md
kyeongmin beb5fd0c61 Figma-to-HTML 에이전트 초기 커밋
- 10단계 변환 프로세스 (PROCESS.md)
- 수학 공식 레퍼런스 (MATH.md, gradient_math.py)
- CSS 보정 규칙 R1~R16 (RULES.md)
- 작업 규율 7개 규칙 (PROCESS-CONTROL.md)
- 8개 Figma 프레임 1:1 HTML 변환물 (block-tests/)
- 8개 Jinja2 템플릿 staging (templates_staging/)
- 변환 완료 도서관 + 디자인 인사이트 (blocks_index.md)
- 사용법 가이드 (README.md)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:16:33 +09:00

411 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 변환 절차 (10 STEP)
Figma 프레임 1개를 HTML+template으로 변환할 때 매번 동일하게 따르는 운영 핸드북.
> **원칙: 같은 세션에서 여러 프레임 연속 작업 OK.** 컨텍스트가 무거워지면 `/compact` 로 정리하고 계속 진행. 핵심 결정/규칙/산출물은 모두 파일에 박혀있어 compact 후에도 보존됨. 이렇게 해야 누적 학습(R13 등 sub-pattern)이 즉시 적용됨. CLAUDE.md 원칙 7 참조.
> **원칙: 1 프레임 변환 = 1:1 reference + 템플릿 동시 작성.** 1번째 등장이라도 templates_staging/{pattern}.html.j2 + meta.yaml + example.yaml 까지 작성한다. 정적 HTML만 두는 것은 work-creating-work. 사용자가 final 검수 후 design_agent/templates/ 로 직접 프로모션.
---
## STEP 0 — 준비
### 0-A. 에이전트: blocks_index.md 한 번 읽기 (필수)
새 세션은 메모리가 없다. 패턴 발견 트리거(2번째 등장)가 작동하려면 **세션 시작 직후** [blocks_index.md](blocks_index.md)를 한 번 통으로 읽어야 한다.
```
Read figma_to_html_agent/blocks_index.md
```
확인할 것:
- "변환 완료 (현행 방법론)" 섹션의 패턴 목록
- "패턴 카탈로그" 섹션의 등록 패턴 (등장 횟수)
- "templates_staging 대기열" 의 진행 중 패턴
### 0-B. 사용자: 프레임 선택
1. Figma desktop에서 변환할 프레임을 **선택** (클릭) 한다
2. 에이전트에게 "이 프레임 변환해줘"라고 알린다 (프레임 ID/이름 명시 권장)
### 0-C. 에이전트: 패턴 비교
STEP 1~3로 metadata + screenshot 받은 직후, 0-A에서 본 인덱스와 비교:
- 비슷한 구조 발견 → "이거 X 패턴과 비슷합니다. 두 번째 등장이면 templates_staging/ 로 Jinja2 추출 진행할까요?" 사용자에게 확인
- 비슷한 게 없음 → 일반 STEP 4 이하 진행
**확인사항:**
- Figma desktop 앱이 활성 탭에 올바른 파일이 떠 있는가
- `.mcp.json`에 figma-desktop SSE 서버가 등록돼있는가 (`http://127.0.0.1:3845/sse`)
---
## STEP 1~3 — 데이터 수집 (병렬)
세 도구를 **단일 메시지에 multiple tool_use 블록**으로 동시 호출한다 (도구 호출 단위 병렬). 순차 호출하면 같은 노드 ID를 두 번 추출하느라 토큰만 낭비됨.
```
[single message, multiple tool_use blocks]
1. mcp__figma-desktop__get_metadata nodeId="" (현재 선택 노드)
2. mcp__figma-desktop__get_design_context nodeId="" (현재 선택 노드)
3. mcp__figma-desktop__get_screenshot nodeId="" (현재 선택 노드)
```
**주의:** nodeId를 비우면 현재 선택 노드를 사용하므로 metadata 응답을 기다릴 필요 없음. 셋 다 동시에 갈 수 있다.
| 도구 | 얻는 것 | 사용처 |
|------|--------|-------|
| get_metadata | 모든 leaf 노드의 `id, type, name, x, y, width, height` (XML) | bottom-up 플래튼 |
| get_design_context | gradient/filter/font/color (React+Tailwind 코드) | CSS 변환 |
| get_screenshot | Figma가 렌더한 PNG | STEP 8 사람 눈 검증 |
**주의:**
- get_metadata 응답이 100KB+ 면 frame이 너무 커서 자르지 않은 상태. 사용자에게 더 작은 단위 선택 요청
- get_design_context는 응답이 매우 크므로 한 프레임당 1회만 호출
---
## STEP 4 — 자산 정리 (block-tests/assets/shared/ 캐시)
design_context에서 `localhost:3845/assets/{hash}.png|svg` 패턴의 자산 URL 추출.
각 자산에 대해:
1. URL 끝의 hash를 파일명으로 사용
2. `block-tests/assets/shared/{hash}.{ext}`**이미 있으면 다운로드 스킵**
3. 없으면 curl로 다운로드
```bash
cd block-tests/assets/shared
for url in $URLS; do
hash=$(basename "$url")
[ -f "$hash" ] || curl -sSo "$hash" "$url"
done
```
HTML에서 참조 시:
```html
<img src="assets/shared/{hash}.png">
```
(`block-tests/{slug}.html` 기준으로 상대 경로 `assets/shared/`)
**효과:**
- 동일 자산이 여러 프레임에서 등장해도 한 번만 다운로드 (해시 파일명이라 자동 dedup)
- 후속 프레임 변환 시간 단축
- 토큰 절약 (이미 있는지 확인만)
**프레임 매핑 메모:** `block-tests/{slug}_assets.txt`에 사용한 hash 목록 + 의미 라벨 기록 → 추후 재추출 시 빠른 매핑
```
# bim-goals-3circles_assets.txt
84965807....png bg_texture
f05ebf15....png arc_top
2f0f1750....png arc_side
```
**legacy:** 이전에 다운로드한 자산이 `block-tests/assets/frame_{id}/` 에 있다면 그대로 두되, 새 변환부터는 `shared/` 만 사용한다.
---
## STEP 5 — flat.md 작성 (분석 + 이상 탐지)
`block-tests/{slug}_flat.md` 파일 생성. 다음 섹션을 반드시 포함:
### 섹션 1. 메타
```markdown
# Frame {ID} — {이름}
> 원본: {W} × {H} px (node {ID})
> Scale: × {S} → {1280} × {H×S} px
> 슬라이드 16:9 안 배치
```
### 섹션 2. 계층 경로 (bottom-up)
모든 leaf 노드를 들여쓰기 트리로 표현. 그룹별 누적 offset 표시.
```
Frame {root} ({W}×{H})
├─ Group "X" (offset → 누적)
│ ├─ TEXT "..." (abs_x, abs_y) {w}×{h}
│ └─ ...
```
### 섹션 3. 이상 탐지 결과
| 검사 | 결과 |
|------|------|
| 회전 단일문자 (bbox 가로 > 세로 × 1.5) | 발견 노드 ID 또는 "없음" |
| 좁은 박스 세로 텍스트 (width < fontSize × 0.8) | ... |
| 중복 노드 (동일 좌표 + 동일 내용) | ... |
| Vector 좌표 metadata vs design_context 불일치 | ... (있으면 어느 쪽 신뢰) |
### 섹션 4. 변형 가능 축 메모 + 슬롯 옵션
이 블록을 템플릿화한다면 무엇이 파라미터가 될지 1~5줄로. **각 슬롯이 required인지 optional인지 표시**:
```markdown
## 변형 가능 축
- columns[N=2~4] (required)
- badge (required)
- bullet_items[1~12] (required)
- bg_image (required)
- bottom_photo (optional) ← 사진 없는 mdx도 이 블록 매칭 가능
- color_palette[N] (required, N과 일치)
```
이 메모가 STEP 10의 `blocks_index.md` 요약 + 향후 templates_staging meta.yaml 의 초안.
### 섹션 5. Sub-pattern 식별 (재사용 가능한 atomic 단위)
이 블록 안에 **다른 블록과 공유 가능한 sub-pattern**이 있는가? RULES.md R13~ 참조.
```markdown
## Sub-patterns
- `bullet-list-with-marker` (R13) — 각 텍스트 앞에 장식 마커
- 위치: 각 컬럼 본문 영역
- 마커: checkbox PNG
- 적용 구조: .bullet-list / .bullet-row / .bullet-icon / .bullet-text
```
Sub-pattern을 **즉시 RULES.md에 등록할 필요는 없다**. 동일 sub-pattern이 2번째 등장하면 그때 R번호 부여해서 정식 등록.
---
## STEP 6 — 그라데이션 수학 변환
각 SVG `<linearGradient>` 데이터를 [scripts/gradient_math.py](scripts/gradient_math.py)로 CSS로 변환.
```bash
python scripts/gradient_math.py \
--w 350 --h 350 \
--x1 110.833 --y1 18.2292 --x2 219.479 --y2 175 \
--stops "0:#FDC69E,1:#E0782C"
```
출력:
```
linear-gradient(145.28deg, #FDC69E 16.04%, #E0782C 55.20%)
```
수학 원리는 [MATH.md §2 참조](MATH.md).
**여러 그라데이션을 한 번에 변환할 땐 Python 인라인 스크립트 사용:**
```python
import sys, os
# scripts/ 디렉토리를 sys.path에 명시 추가 (작업 디렉토리 무관)
sys.path.insert(0, os.path.join('figma_to_html_agent', 'scripts'))
from gradient_math import svg_to_css
svg_to_css(W=350, H=350, x1=110.833, y1=18.2292, x2=219.479, y2=175,
stops=[(0, '#FDC69E'), (1, '#E0782C')])
```
**작업 디렉토리가 `figma_to_html_agent/` 인 경우:**
```python
import sys; sys.path.insert(0, 'scripts')
from gradient_math import svg_to_css
```
**또는 정식 패키지로 사용** (`scripts/__init__.py` 가 있으므로):
```python
# 작업 디렉토리가 figma_to_html_agent/ 일 때
from scripts.gradient_math import svg_to_css
```
⚠️ **금지: 함수 코드를 인라인 Python에 복사 붙여넣기**. 한 번 만든 `gradient_math.py`를 항상 import해서 쓴다. 복사하면 버그 수정 시 여러 곳을 동시에 고쳐야 하고 수식이 미세하게 어긋날 위험.
---
## STEP 7 — HTML 작성
### 7-A. 기본 구조
```html
<div class="slide"> <!-- 1280×720 흰색 -->
<div class="block"> <!-- 1280 × (H×S) -->
<div class="inner"> <!-- 원본 W×H, transform: scale(S) -->
... 모든 요소 (Figma 원본 좌표 사용) ...
</div>
</div>
</div>
```
```css
.inner {
position: absolute;
left: 0; top: 0;
width: {W}px; height: {H}px;
transform: scale({S});
transform-origin: top left;
}
```
**왜 transform: scale을 쓰는가:** 모든 위치/크기/폰트/그림자/스트로크가 한 번의 transform으로 균일하게 축소됨. 매 값을 수동으로 ×S 곱하는 것보다 안전하고 검증 가능. ([MATH.md §1](MATH.md))
### 7-B. 요소 변환 우선순위
| 요소 종류 | 구현 방법 | 이유 |
|---------|---------|-----|
| 원/사각형 + gradient + blend | **HTML div** + `border-radius` + `linear-gradient` + `mix-blend-mode: multiply` | 동적 재구성 위해 |
| Stroke (경계선) | `border: Npx solid color` + `box-sizing: border-box` | gradient와 함께 사용 가능 |
| Drop shadow blur | `box-shadow: 0 0 {2×stdDev}px {color}` | SVG feGaussianBlur 근사 |
| 곡선 (아크, 비원형) | **SVG `<path>`** 또는 미리 export된 PNG | CSS 불가능 |
| 텍스트 | HTML `<div>` 절대 배치 | 선택 가능, 접근성 |
| 실사 이미지 | `<img>` PNG | 재현 불가 |
| 회전된 도형 | 래퍼 div + `transform: rotate()` ([INSIGHT-GRADIENT.md](INSIGHT-GRADIENT.md)) | gradient 동시 회전 |
### 7-C. 보정 규칙
[RULES.md](RULES.md) R1~R16 모두 적용:
- R1: descender padding-bottom
- R2~R3: 회전/세로 텍스트
- R4: 그라데이션 텍스트
- R5: 다중 fills
- R6: 중복 노드
- R7: 흰 배경
- R8: 스케일 팩터
- R9: 순수 CSS 우선
- R10: blend mode 호환
- R11: stroke 정렬 (inside/outside)
- R12: viewBox padding
---
## STEP 8 — Selenium 렌더링 + 사람 눈 검증
```python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
import os, time
# _renders/ 폴더 없으면 생성
os.makedirs('block-tests/_renders', exist_ok=True)
opts = Options()
opts.add_argument('--headless=new')
opts.add_argument('--hide-scrollbars')
opts.add_argument('--force-device-scale-factor=1')
opts.add_argument('--window-size=1600,900')
d = webdriver.Chrome(options=opts)
p = os.path.abspath('block-tests/{slug}.html').replace('\\','/')
d.get('file:///' + p)
time.sleep(1.5)
d.save_screenshot('block-tests/_renders/{slug}_full.png')
r = d.execute_script(
'const r=document.querySelector(".slide").getBoundingClientRect();'
'return [r.x,r.y,r.width,r.height];'
)
Image.open('block-tests/_renders/{slug}_full.png').crop(
(int(r[0]), int(r[1]), int(r[0]+r[2]), int(r[1]+r[3]))
).save('block-tests/_renders/{slug}.png')
d.quit()
```
**검증 방식:**
- 자동 픽셀 diff는 하지 않음 (font 렌더 차이로 노이즈만 많음)
- Figma `get_screenshot` 응답과 Selenium 결과를 **사람 눈**으로 비교
- 차이 발견 시 STEP 5~7로 돌아가서 원인 파악 (값 수정 금지)
---
## STEP 9 — 결과물 저장
```
block-tests/
├── {slug}.html ← 변환물
├── {slug}_flat.md ← 플래튼/이상/변형 축 메모
└── _renders/
└── {slug}.png ← 검증 스크린샷
```
`{slug}` 명명 규칙: 의미 기반 kebab-case (예: `bim-goals-3circles`, `cards-3col-icon`).
프레임 ID는 metadata로 추적 가능하므로 파일명에 넣지 않음.
---
## STEP 10 — blocks_index.md 1줄 업데이트
`blocks_index.md` 끝에 한 줄 추가:
```markdown
| {slug} | {프레임 ID} | {1줄 변형 축 요약} | {날짜} |
```
이 인덱스가 패턴 발견의 단서가 된다. 다음 변환 시작 전에 이 인덱스를 한 번 훑어서 "이미 비슷한 거 했나?" 확인.
---
## 패턴 → 템플릿화 (1번째부터 즉시)
**규칙: 1번째 등장부터 templates_staging 작성. 정적 HTML만 두는 것 금지.**
| 등장 횟수 | 처리 |
|---------|------|
| **1번째** | `block-tests/{slug}.html` (1:1 reference) + `templates_staging/{pattern_id}.html.j2` (Jinja2 + meta.yaml + example.yaml) **함께 작성** |
| 2번째 | 기존 staging 템플릿이 새 데이터로 잘 렌더되는지 확인. 안 되면 템플릿 수정. example 추가. |
| 3번째 이후 | 동일 |
**왜 1번째부터 템플릿화하나?**
- 변환의 목적은 **블록 라이브러리 구축**, 단순 HTML 복제가 아님
- 1:1 단계에서 발견한 인사이트(R13 등)를 즉시 템플릿에 반영해야 잊지 않음
- 사용자가 검수할 때 "이게 블록으로 어떻게 작동할지" 즉시 확인 가능
- 2번째 등장을 기다리면 사용자 수동 복제 작업이 누적됨 (work-creating-work)
**Stage 2 산출물:**
```
templates_staging/
├── {pattern_id}.html.j2 ← Jinja2 템플릿 본체
└── {pattern_id}.meta.yaml ← when / slots / min_size_px / 변형 축 초안
```
여기까지가 **에이전트 책임의 끝.**
---
## 🚧 프로모션 게이트 (사용자 전용)
> 이 게이트 이후 작업은 **에이전트가 절대 수행하지 않는다.** 모든 design_agent/templates/ 변경은 사용자 본인이 직접 한다.
### 사용자가 수행할 작업
1. **검수**: `templates_staging/{pattern_id}.html.j2` 를 다양한 파라미터로 렌더 테스트
2. **품질 게이트 통과 확인**:
- [ ] 1:1 변환물과 시각적으로 동일한가
- [ ] 슬롯 파라미터를 바꿔도 깨지지 않는가 (원 4개, 라벨 0개 등 극단 케이스)
- [ ] meta.yaml의 when/slots가 design_agent의 다른 블록과 충돌 없는가
3. **이동**: `templates_staging/{pattern_id}.html.j2``design_agent/templates/blocks/{category}/`
4. **등록**: `design_agent/templates/catalog.yaml` 에 when/slots/min_size_px 추가
5. **상태 업데이트**: `blocks_index.md` 의 해당 행 상태 → `promoted`
### 에이전트의 역할
- staging 작성까지만
- 사용자 요청 없이 `design_agent/templates/` 를 절대 읽거나 쓰지 않음
- "templates/ 에 옮겨드릴까요?" 같은 제안 금지 (월권)
- 사용자가 명시적으로 "이 staging 결과 검토해줘"라고 요청하면 → staging 폴더 내에서만 검토
---
## 안티 패턴 (하지 말 것)
| ❌ 하지 말 것 | 이유 |
|------------|-----|
| 사전에 인벤토리/지문/군집 단계 | work-creating-work, 패턴은 변환하면서 발견됨 |
| 1번째 등장은 정적 HTML로만 두기 (templates_staging 미작성) | work-creating-work, 인사이트 잊혀짐. 1번째부터 템플릿 작성 |
| 컨텍스트 차면 강제 새 세션 | compact 사용. 핵심 결정은 모두 파일에 박혀있어 손실 없음 |
| Figma 데이터 안 보고 멀티모달 이미지로 추측 | 미묘한 alpha/blend에서 틀림 |
| "여기 1px 어색하니 다른 곳도 같이 바꾸자" | 사용자 피드백만 정확히 반영 |
| 같은 자산을 매번 새로 다운로드 | `block-tests/assets/shared/` 캐시 활용 |
| 그라데이션 각도/색을 눈대중으로 | gradient_math.py로 수학 도출 |
| gradient_math.py 함수 코드 인라인 복사 | import만 한다. 복사하면 수식 어긋남 |
| 세션 시작에 blocks_index.md 안 읽음 | 패턴 발견 트리거 영영 작동 안 함 |
| `design_agent/templates/` 직접 수정 | 프로모션은 사용자 전용. 에이전트는 staging까지만 |
| "templates/ 옮겨드릴까요?" 제안 | 월권. 사용자가 알아서 함 |
| `prerequisites-3col.html` 을 신규 변환 레퍼런스로 사용 | 구 방법론 (R8/R9 미적용). legacy 표시됨 |