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>
This commit is contained in:
410
PROCESS.md
Normal file
410
PROCESS.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# 변환 절차 (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 표시됨 |
|
||||
Reference in New Issue
Block a user