figma_to_html_agent 추가 + MCP/Claude 설정

figma_to_html_agent/:
- Figma MCP 기반 블록 추출 에이전트 (CLAUDE.md, PLAN.md, PROCESS.md 등)
- block-tests/: Figma→HTML 변환 결과물 (bim-3roles-cards 등)
- templates_staging/: Jinja2 템플릿 + meta.yaml + example.yaml
- figma-analysis/, figma-assets/: Figma 분석 데이터 + 에셋
- scripts/: gradient_math.py 등 유틸리티

설정:
- .mcp.json: Figma MCP 서버 연결 설정
- .claude/settings.json: Claude Code 프로젝트 설정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 11:00:31 +09:00
parent 360cd8e44c
commit 51548fdc41
467 changed files with 25280 additions and 10 deletions

View File

@@ -0,0 +1,123 @@
# Figma → HTML Agent
Figma 프레임을 **수학적으로 정확하게** HTML/CSS로 변환하고, 변환물을 **재사용 가능한 블록 라이브러리**로 축적하는 에이전트.
## 목적
사용자가 Figma 파일에서 프레임을 선택하면:
1. 그 프레임을 16:9 슬라이드(1280×720) 안의 HTML 블록으로 100% 동일하게 변환한다
2. 변환물의 "변형 가능 축"을 기록한다 (원 개수, 색상, 라벨 등)
3. 같은 패턴이 반복되면 Jinja2 템플릿으로 추상화하여 design_agent의 블록 라이브러리에 편입한다
## 핵심 원칙 (절대 어기지 않음)
1. **수학적 계산만 허용** — 시행착오 px 조정 금지. Figma 좌표 → 스케일 → CSS 값 수학적 도출
2. **Bottom-up 프로세스** — leaf 노드 플래튼 → 2개씩 묶기 → 계층 쌓기. top-down하면 누락
3. **이상 탐지 필수** — 모든 노드에 bbox 비율 검사, 회전 감지, 중복 감지 수행
4. **AI가 먼저 발견** — 디테일(1px, 1° 차이)을 사용자 피드백 전에 AI가 스스로 찾음
5. **하드코딩 금지** — 결과물을 수동으로 고치지 말고 프로세스를 고친다
6. **AI 역할 분담** — AI는 분류(고르기)만, 구성(만들기)은 코드. LLM은 px을 못 본다
7. **컨텍스트 관리는 compact로** — 한 세션에서 여러 프레임을 연속 작업할 수 있다. 컨텍스트가 무거워지면 `/compact` 로 핵심만 요약하고 계속 진행. 이유: 핵심 결정/구조/규칙은 모두 파일(CLAUDE.md, PROCESS.md, RULES.md, blocks_index.md, 산출물)에 박혀있어 compact 후에도 보존됨. 손실되는 건 시행착오/디버깅 과정 뿐이며, 이건 잃어도 OK. 매 프레임마다 새 세션을 강제하면 누적 학습이 silo되어 R13 같은 sub-pattern 발견의 즉시 적용이 불가능해짐.
8. **순수 CSS 우선, SVG는 곡선/필터에만** — 동적 재구성 위해 가능한 한 HTML div + linear-gradient 사용
9. **프로모션 게이트는 사용자 전용** — 에이전트는 절대 `design_agent/templates/` 에 직접 쓰지 않는다. 모든 작업은 `figma_to_html_agent/` 안에서 끝나며, 본체 라이브러리 이전은 사용자 수동 검수 후 사용자 본인이 수행한다.
10. **시맨틱 우선, Figma 평면 레이어 그대로 옮기지 말 것** — Figma의 평면 레이어 구조는 디자인 도구의 한계일 뿐, 의미 구조가 아니다. 마커+텍스트는 list item, 카드 묶음은 column unit, 등 시맨틱하게 재그룹핑하여 작성한다. RULES.md R13 (Custom-marker bullet list) 참조. 새로 발견되는 sub-pattern은 [blocks_index.md](blocks_index.md) "디자인 인사이트" 섹션에 누적한다.
11. **모든 슬롯은 기본 optional** — 1:1 단계에서 모든 슬롯이 채워져 있다고 해서 "이 블록은 필수" 로 해석하지 않는다. 같은 블록이 사진 없는/짧은/긴 mdx에 모두 매칭되어야 한다는 가정으로 설계한다.
## 변환 프로세스 (10단계)
전체 절차는 [PROCESS.md](PROCESS.md) 참조.
```
0-A. 에이전트: blocks_index.md 한 번 읽기 (지난 변환 패턴 확인)
0-B. 사용자: Figma에서 프레임 선택
1. get_metadata ← 구조 + bbox
2. get_design_context ← gradient/filter/text 정보
3. get_screenshot ← Figma 원본 (검증 비교용)
4. 자산 → block-tests/assets/shared/{hash} 캐시
5. flat.md 작성 ← bottom-up + 이상 탐지 + 변형 축 메모
6. 그라데이션 수학 변환 ← scripts/gradient_math.py 호출
7. HTML 작성 ← 순수 CSS 우선, transform: scale() 균일 축소
8. Selenium 스크린샷 ← Figma 프리뷰와 사람 눈 비교
9. block-tests/{slug}.html + flat.md 저장
10. blocks_index.md 1줄 업데이트
```
**패턴 발견 트리거:** 동일 구조의 프레임이 **2번째** 등장하는 순간 → `templates_staging/{pattern_id}.html.j2` 로 Jinja2화. 이게 staging 종착점.
**프로모션 게이트:** staging까지가 에이전트 책임. 그 다음은 사용자가 직접 검수하고 [design_agent/templates/blocks/](../templates/blocks/) 로 이전 + [catalog.yaml](../templates/catalog.yaml) 등록. **에이전트는 design_agent/templates/ 를 절대 건드리지 않는다.**
## 도구
| 도구 | 용도 |
|------|------|
| Figma MCP `get_metadata` | 프레임 구조 + 절대 좌표 |
| Figma MCP `get_design_context` | gradient/filter/font 등 stylable 데이터 |
| Figma MCP `get_screenshot` | Figma 원본 PNG (눈 검증용) |
| `scripts/gradient_math.py` | SVG `<linearGradient>` → CSS `linear-gradient(...)` 수학 변환 |
| Selenium (headless Chrome) | HTML 렌더링 + 검증 스크린샷 |
| Pillow | 스크린샷 자르기/비교 |
## 입출력
**입력:** Figma 파일 + 노드 ID (또는 현재 선택 노드)
**출력:**
- `block-tests/{slug}.html` — 변환 결과
- `block-tests/{slug}_flat.md` — 플래튼/이상 탐지/변형 축 메모
- `assets/shared/...` — 공유 자산 캐시
- `blocks_index.md` 한 줄 추가
## 폴더 구조
```
figma_to_html_agent/ ← 에이전트 작업 영역 (staging)
├── CLAUDE.md ← 이 파일 (에이전트 명세)
├── PROCESS.md ← 10단계 운영 절차 (변환 핸드북)
├── MATH.md ← 수학 공식 레퍼런스
├── RULES.md ← CSS 보정 규칙 (R1~R12)
├── PROCESS-CONTROL.md ← "찍어맞추기 금지" 규칙
├── PLAN.md ← 현재 진행 현황
├── blocks_index.md ← 변환 완료 도서관
├── scripts/
│ ├── __init__.py ← 빈 파일 (패키지 인식용)
│ └── gradient_math.py ← SVG→CSS 그라데이션 변환 함수
├── block-tests/ ← Stage 1: 정적 1:1 변환물
│ ├── {slug}.html
│ ├── {slug}_flat.md
│ ├── _renders/ ← Selenium 검증 스크린샷
│ └── assets/
│ ├── shared/ ← 해시 기반 자산 캐시 (재사용)
│ └── frame_{id}/ ← 프레임 전용 자산 (legacy)
└── templates_staging/ ← Stage 2: Jinja2 추상화
├── {pattern_id}.html.j2
└── {pattern_id}.meta.yaml ← when/slots/min_size_px 초안
────────────────────────────────────────────────────────
🚧 프로모션 게이트 (사용자 수동 작업) 🚧
────────────────────────────────────────────────────────
design_agent/ ← 본체 라이브러리 (에이전트 접근 금지)
└── templates/
├── blocks/{category}/
│ └── {pattern_id}.html.j2 ← 사용자가 staging에서 이전
└── catalog.yaml ← 사용자가 when/slots 등록
```
**중요:** 에이전트는 위 구분선 아래(`design_agent/templates/`)를 **절대 수정하지 않는다.** 그 영역은 사용자가 staging 결과물을 검수한 뒤 본인이 직접 프로모션한다.
## 금지 사항
- 시행착오 px 조정 (1씩 늘려보기 등)
- 사용자에게 "맞나요?" 반복 질문 (스스로 검증)
- line-height 등 CSS 속성을 감으로 보정 (폰트 메트릭에서 수학적 도출)
- 흰 텍스트 스트로크 (`-webkit-text-stroke: white`) 사용
- 블록 배경을 검정으로 표시 (미리보기는 항상 흰색 배경)
- **이미지 해석으로 gradient 방향 판단** (멀티모달 금지, 데이터로만 판단 — PROCESS-CONTROL.md 참조)
- **한 번에 여러 값 동시 수정** (gradient 각도와 border-radius 동시 변경 금지)
- **여러 프레임을 한 세션에 변환** (1세션 1프레임 원칙)
- **plus-darker 블렌드 사용** (Safari 전용 → multiply로 교체, RULES.md R10)
- **Figma 인벤토리/지문/군집 같은 사전 분류** (work-creating-work, 패턴은 bottom-up으로 발견)
- **`design_agent/templates/` 직접 수정** (프로모션 게이트는 사용자 전용. 에이전트는 staging까지만)
- **사용자에게 "templates/ 에 옮겨드릴까요?" 같은 제안** (월권. 사용자가 알아서 함)

View File

@@ -0,0 +1,582 @@
# Figma → 컴포넌트 추출 + 카탈로그 구축 계획
## 목적
Figma 디자인(바론컨설턴트 홈페이지 기획팀 공유)에서 재사용 가능한 **콘텐츠 블록**을 추출하고, 디자인 팀장(Sonnet)이 선택할 수 있는 카탈로그로 체계화한다.
**핵심 원칙: 블록은 모드 독립적이다.**
- 블록 자체는 "슬라이드 전용"이 아니라 **HTML/CSS 콘텐츠 블록**
- 슬라이드 모드(100vh, overflow:hidden)와 웹/스크롤 모드(auto, overflow:visible)는 **컨테이너(base 템플릿)**가 결정
- 블록은 높이를 고정하지 않음 → 어떤 컨테이너에도 들어갈 수 있음
- 현재는 `slide-base.html`(슬라이드)에 집중하되, 향후 `page-base.html`(웹) 추가 가능
```
블록 (카드, 표, 인용 등) — 모드와 무관, 재사용 가능
slide-base.html → height:100vh, overflow:hidden (슬라이드 모드)
page-base.html → height:auto, overflow:visible (웹/스크롤 모드, 향후)
```
---
## 현재 상태
### 보유 자산
| 항목 | 상태 | 위치 |
|------|------|------|
| Figma API 접근 | ✅ 가능 | Token: `.env` |
| 기존 블록 템플릿 7종 | ✅ 완성 | `templates/blocks/` |
| 디자인 토큰 | ✅ 완성 | `static/tokens.css` |
| 슬라이드 렌더러 | ✅ 완성 | `src/renderer.py` |
| 디자인 팀장 (DA-13) | ❌ todo | `src/design_director.py` |
| 블록 카탈로그 | ❌ 없음 | - |
### Figma 파일 구조
```
바론 공유 2025.05.13 (node: 1574-6254)
├── 1장 바론컨설턴트
├── 2장 디지털전환
│ ├── 2-1 건설산업에서의 디지털전환 (1920x8538, 스크롤형)
│ ├── 2-2 디지털전환과 소프트웨어 (1920x9123, 스크롤형)
│ └── 건설산업에서의 디지털전환 (1920x8536, 스크롤형)
│ [자세히보기]
│ ├── 2-1장 자세히보기 (4프레임: 건설산업/BIM/GIS/디지털트윈)
│ ├── 2-2장 자세히보기
│ └── 2-3장 자세히보기
├── 3장 제공서비스
│ ├── 3-1장 솔루션프로그램 자세히보기
│ └── 3-3장 빅룸 자세히보기
└── 모션작업
```
### 기존 블록 vs Figma에서 발견된 패턴
| 패턴 | 기존 블록 | Figma에서 발견 | 갭 |
|------|----------|--------------|-----|
| 2단 비교 | ✅ comparison | ✅ | - |
| 카드 그리드 | ✅ card-grid | ✅ (변형 다수) | 변형 추가 필요 |
| 벤 다이어그램 | ✅ relationship | ✅ | - |
| 단계 흐름 | ✅ process | ✅ | - |
| 강조 인용 | ✅ quote-block | ✅ (큰따옴표 장식) | 변형 추가 필요 |
| 결론 바 | ✅ conclusion-bar | ✅ | - |
| 비교 테이블 | ✅ comparison-table | ✅ | - |
| **이미지 갤러리** | ❌ | ✅ (2열, 3열, 2x2) | **신규** |
| **타임라인** | ❌ | ✅ (세로 원형 4단계) | **신규** |
| **섹션 타이틀** | ❌ | ✅ (영문+한글 공통 헤더) | **신규** |
| **사례 카드** | ❌ | ✅ (출처+불릿 카드) | **신규** |
| **핵심 지표** | ❌ (정의만) | ✅ (큰 숫자+보조) | **신규** |
| **아이콘 리스트** | ❌ | ✅ (아이콘+제목+설명) | **신규** |
| **Hero 섹션** | ❌ | ✅ (배경+원형이미지+텍스트) | **신규** |
| **CTA 버튼 바** | ❌ | ✅ (자세히보기 버튼) | **필요 시** |
| **이미지 블록** | ❌ | ✅ (도표, 참고자료) | **신규** (3변형: full/side/thumb) |
| **자세히보기 블록** | ❌ | ✅ (상세 콘텐츠 접기/펼치기) | **신규** (`<details>/<summary>`) |
---
## 작업 계획
### Phase A: Figma 분석 + 패턴 추출
#### A-1: Figma 전체 섹션 이미지 렌더링
- **작업:** 각 섹션/프레임을 이미지로 렌더링하여 시각적으로 패턴 식별
- **방법:**
- **1차:** Framelink MCP `get_figma_data` — CSS-ready 데이터로 노드 구조 + 스타일 동시 추출
- **2차:** Figma 공식 MCP `get_screenshot` — 시각 참고용 스크린샷
- **fallback:** Figma REST API `/v1/images/{file_key}?ids={node_ids}` (MCP 미설치 시)
- **산출물:** `docs/figma-screenshots/` 폴더에 PNG 저장
- **완료 기준:** 모든 자세히보기 프레임(8개)의 스크린샷 확보
#### A-2: Figma 노드 구조 심층 분석
- **작업:** 각 프레임의 상세 스타일 + 레이아웃 정보 추출
- **방법:**
- **1차:** Framelink MCP `get_figma_data` (nodeId 지정, depth 조절)
- 자동 CSS 변환: 색상→hex/rgba, 레이아웃→flex 용어, 그림자→box-shadow, 그라데이션→linear-gradient()
- 스타일 중복 제거 (글로벌 변수로 추출)
- 토큰 효율적 (raw API 대비 1/5 크기)
- **2차:** Figma 공식 MCP `get_variable_defs` — 디자인 토큰/변수 추출
- **fallback:** Figma REST API `/v1/files/{key}/nodes?ids={ids}&depth=5` (MCP 미설치 시)
- **추출 정보:**
- TEXT 노드: fontFamily, fontSize, fontWeight, lineHeight, letterSpacing, color, 텍스트 내용
- FRAME/GROUP: auto-layout (direction, gap, padding, alignItems, justifyContent), constraints
- RECTANGLE: fills (solid/gradient/image), strokes, cornerRadius, effects
- INSTANCE: componentId (재사용 컴포넌트 식별)
- **Figma → CSS 매핑 (Framelink MCP가 자동 처리, REST API 시 수동):**
- `layoutMode: "VERTICAL"``flex-direction: column`
- `primaryAxisAlignItems: "CENTER"``justify-content: center`
- `itemSpacing: 20``gap: 20px`
- `paddingLeft/Right/Top/Bottom``padding`
- `fills[].color {r,g,b,a}``rgba()` 또는 `#hex`
- `fills[].type: "GRADIENT_LINEAR"``linear-gradient(...)`
- `cornerRadius``border-radius`
- `strokes + strokeWeight``border`
- `effects[].type: "DROP_SHADOW"``box-shadow`
- `fontSize``font-size` (px 단위)
- `lineHeightPercentFontSize: 170``line-height: 1.7`
- **산출물:** `docs/figma-analysis/` 폴더에 구조 문서
- **주의:** Figma API rate limit 심함 — depth 깊은 요청은 30분 차단 가능. 얕게 요청 후 필요한 노드만 상세 조회
#### A-3: 디자인 패턴 분류 + 명명
- **작업:** 추출된 시각 요소를 재사용 가능한 블록 단위로 분류
- **기준:**
- 2회 이상 반복되는 패턴 → 블록 후보
- 슬롯(교체 가능한 위치)이 명확한 것 → 우선 순위 높음
- 콘텐츠 유형과 매칭되는 것 → 우선 순위 높음
- **산출물:** 패턴 목록 + 각 패턴의 Figma 원본 노드 ID
### Phase B: HTML/CSS 컴포넌트 제작
#### B-1: 신규 블록 템플릿 제작 (8~10종)
- **파일:** `templates/blocks/{name}/` 폴더별 정리 (변형별 파일 + preview.png)
- **제작 순서 (우선순위):**
1. `section-title/default.html` — 공통 헤더 (모든 슬라이드에서 사용)
2. `example-card/2col.html` — 사례 카드 (출처+불릿, 정책 문서 인용)
3. `image-block/full.html`, `side.html`, `thumb.html` — 이미지 블록 3변형
- full: 전체 너비 (핵심 도표, 가로형)
- side: 텍스트 옆 (보조 이미지, 세로형)
- thumb: 썸네일 (참고 문서 표지)
- CSS: `object-fit: contain` (비율 유지, 잘리지 않음)
- 원본 이미지 그대로 사용, 크기만 조절 (crop 안 함)
4. `image-gallery/2col.html`, `3col.html`, `2x2.html` — 이미지 갤러리
5. `timeline/vertical.html`, `horizontal.html` — 타임라인 (연혁/로드맵)
6. `big-number/3col.html`, `4col.html` — 핵심 지표 (큰 숫자 + 보조 텍스트)
7. `icon-list/vertical.html`, `grid.html` — 아이콘 리스트 (기능 나열)
8. `details-block/default.html` — 자세히보기 (`<details>/<summary>`)
- 슬라이드 표면: 요약만 표시
- 펼치면: 전체 상세 내용
- 인쇄 시: `beforeprint` 이벤트로 자동 펼침
- **규칙:**
- 디자인 토큰(`var(--color-*)`) 사용 (하드코딩 색상 금지)
- Jinja2 슬롯 (`{{ variable }}`) 형식
- `<style>` 태그를 블록 HTML 안에 포함 (자체 완결)
- Figma 원본과 시각적으로 유사하되 1:1 복제 아님 (디자인 토큰 기준)
- **높이를 고정하지 않음** (auto 또는 비율 사용) — 모드 독립적
- 각 블록에 사람용 주석 포함 (용도, 적합/부적합, Figma 원본 위치)
#### B-1a: Figma 웹 → 블록 변환 규칙
Figma는 웹사이트 디자인(920px 너비, 세로 스크롤)이므로, 블록으로 변환할 때 아래 규칙을 적용한다:
| Figma 원본 | 블록 변환 | 이유 |
|------------|----------|------|
| 높이: 고정 px (예: 200px) | `height: auto` 또는 비율(%) | 720px 안에 들어가야 하고, 웹 모드에서도 동작해야 함 |
| 너비: 고정 px (예: 920px) | `fr` 또는 `%` (grid 컬럼에 맞춤) | 컨테이너 너비에 적응해야 함 |
| 색상: Figma 값 (예: #1565c0) | `var(--color-accent)` 등 디자인 토큰 | 토큰 체계 통일. 필요 시 `tokens.css`에 토큰 추가 |
| 폰트: Figma 값 (예: Noto Sans CJK KR 24px) | `var(--font-subtitle)` 등 디자인 토큰 | Pretendard 기준. 크기 비율만 참고 |
| 간격: Figma 값 (예: padding 20px, gap 80px) | `var(--spacing-inner)`, `var(--spacing-block)` 등 | 토큰 체계. 비율 참고 |
| 그라데이션 배경 | **제거** | CLAUDE.md 디자인 규칙: 그라데이션 금지 |
| 호버/클릭 효과 | **제거** | CLAUDE.md 디자인 규칙: 호버 금지 |
| 애니메이션/트랜지션 | **제거** | CLAUDE.md 디자인 규칙: 애니메이션 금지 |
| 원형 이미지 마스크 | **제거** (필요 시 별도 변형으로) | 장식 요소는 기본에서 제외 |
| 텍스트 위치 | `{{ variable }}` Jinja2 슬롯으로 표시 | 교체 가능한 부분 |
**원칙:** Figma에서 가져오는 것은 **정보 구조(카드 배치, 타임라인 흐름, 비교 레이아웃)**이지, 장식 요소가 아니다.
#### B-2: 기존 블록 변형 추가
- **대상:**
- `quote-block.html``quote-block-decorated.html` (큰따옴표 ::before/::after 장식)
- `card-grid.html``card-grid-icon.html` (아이콘 강조 변형)
- `comparison.html``comparison-visual.html` (이미지 포함 비교)
- **규칙:** 기존 슬롯 구조 유지, CSS만 변형
#### B-3: slide-base.html 업데이트
- **내용:** 신규 블록의 grid-area 지원, 디자인 토큰 추가 (필요 시)
- **주의:** 기존 7개 블록의 렌더링이 깨지지 않는지 반드시 검증
### Phase C: 카탈로그 구축
#### C-1: catalog.yaml 생성
- **파일:** `templates/catalog.yaml`
- **구조:**
```yaml
version: "1.0"
blocks:
# --- 기존 블록 ---
- id: quote-block
name: 강조 인용
visual: "좌측 컬러 라인 + 배경색 + 인용 텍스트"
when: "문제 제기, 핵심 주장, 정의 강조할 때"
not_for: "일반 설명문, 사례 나열"
slots:
required: [quote_text]
optional: [source]
character_limits:
quote_text: 150
source: 50
variations: [default, decorated]
figma_ref: null # 기존 블록은 Figma 이전에 제작됨
# --- 신규 블록 ---
- id: example-card
name: 사례 카드
visual: "제목 + 출처(기관/연도) + 불릿 목록, 테두리 박스. 2~3장 나란히."
when: "정책 문서 인용, 법령/지침 사례, 출처가 명확한 근거 제시"
not_for: "용어 정의, 일반 설명, 비교"
slots:
required: [items[].title, items[].bullets[]]
optional: [items[].source_org, items[].source_year]
character_limits:
title: 30
bullet: 60
variations: [single, 2col, 3col]
figma_ref: "1574:54586 > 2-1_01 > examples-row"
- id: image-gallery
name: 이미지 갤러리
visual: "이미지 2~4장 나란히 + 캡션, 중앙 정렬"
when: "근거 자료 사진, 문서 표지, 현장 사진, 참고 이미지"
not_for: "텍스트 콘텐츠, 다이어그램 (다이어그램은 relationship 사용)"
slots:
required: [images[].src, images[].alt]
optional: [images[].caption]
variations: [2col, 3col, 2x2]
figma_ref: "1574:54586 > 2-1_03 > image grid"
- id: timeline
name: 타임라인
visual: "세로/가로 축 위에 원형 마커 + 연도/제목/설명"
when: "연혁, 로드맵, 정책 시행 일정, 단계별 계획"
not_for: "프로세스 흐름 (순서는 있지만 시간이 아닌 것은 process 사용)"
slots:
required: [events[].year, events[].title]
optional: [events[].description]
character_limits:
title: 25
description: 60
variations: [vertical, horizontal]
figma_ref: "1574:54586 > 2-1_01 > timeline"
- id: big-number
name: 핵심 지표
visual: "큰 숫자(2rem+) + 단위 + 보조 설명. 2~4개 나란히."
when: "KPI, 통계, 목표 수치, 성과 지표"
not_for: "텍스트 설명, 정의"
slots:
required: [metrics[].number, metrics[].label]
optional: [metrics[].unit, metrics[].description]
variations: [2col, 3col, 4col]
figma_ref: null
- id: section-title
name: 섹션 타이틀
visual: "영문 소제목(작은 글씨) + 한글 대제목(큰 글씨), 하단 구분선"
when: "모든 슬라이드 상단, 섹션 시작"
not_for: "본문 콘텐츠 영역"
slots:
required: [title_ko]
optional: [title_en, subtitle]
figma_ref: "공통 > section_title 컴포넌트"
- id: icon-list
name: 아이콘 리스트
visual: "아이콘 + 제목 + 설명이 세로로 나열, 좌측 아이콘 정렬"
when: "기능 나열, 특성 목록, 장점 리스트"
not_for: "비교 (comparison 사용), 순서 (process 사용)"
slots:
required: [items[].icon, items[].title, items[].description]
character_limits:
title: 20
description: 80
variations: [vertical, horizontal, grid]
figma_ref: null
layouts:
- id: "65-35"
name: "6.5:3.5 좌우 분할"
grid_columns: "6.5fr 3.5fr"
when: "좌측 메인 콘텐츠 + 우측 보조/정의"
- id: "50-50"
name: "5:5 균등 분할"
grid_columns: "1fr 1fr"
when: "대등한 비교, 병렬 콘텐츠"
- id: "single"
name: "단일 컬럼"
grid_columns: "1fr"
when: "프로세스 흐름, 타임라인, 단순 구조"
- id: "35-65"
name: "3.5:6.5 좌우 분할"
grid_columns: "3.5fr 6.5fr"
when: "좌측 요약/네비게이션 + 우측 메인 콘텐츠"
```
#### C-2: 카탈로그 → 디자인 팀장 프롬프트 연결
- **파일:** `src/design_director.py` 수정
- **방법:** `catalog.yaml` 로드 → 블록 목록을 시스템 프롬프트에 삽입
- **프롬프트 구조:**
```
사용 가능한 블록:
- quote-block: 좌측 컬러 라인 + 인용 텍스트. 문제 제기할 때 사용. 일반 설명문에는 부적합.
- example-card: 제목+출처+불릿. 정책 사례 인용할 때 사용. 2~3장 나란히.
- card-grid: 2~4열 카드. 용어 정의 여러 개 나열할 때 사용.
...
사용 가능한 레이아웃:
- 65-35: 좌측 메인 + 우측 보조. 메인 콘텐츠가 많을 때.
- 50-50: 균등 비교. 대등한 내용일 때.
...
위 블록과 레이아웃만 사용하여 배치해라. 목록에 없는 블록은 만들지 마라.
```
#### C-3: renderer.py 업데이트
- **내용:** 신규 블록 템플릿 로드 지원
- **주의:** 기존 `BLOCK_SLOTS` dict에 신규 블록 추가
---
## 충돌 지점 + 리스크 검토
### 1. Figma 디자인 ≠ 디자인 토큰
| 리스크 | 설명 | 대응 |
|--------|------|------|
| **색상 불일치** | Figma는 파란 그라데이션 배경 사용, 디자인 토큰은 `#2563eb` 단색 | Figma 색상을 참고하되 토큰 체계 우선. 필요 시 토큰 추가 (`--color-accent-light` 등) |
| **폰트 불일치** | Figma는 Noto Sans CJK KR 사용, 토큰은 Pretendard Variable | Pretendard 유지. Figma 폰트 크기 비율만 참고 |
| **여백 불일치** | Figma 920px 프레임 vs 슬라이드 1280px | 비율 기반으로 변환. 고정 px 대신 토큰 사용 |
| **웹 vs 슬라이드** | Figma는 웹사이트(세로 스크롤, 920x1231~2208px), 슬라이드는 고정(1280x720px) | 높이 고정하지 않음(auto). 컨테이너가 모드를 결정 |
**원칙:** Figma 디자인을 1:1 복제하지 않는다. 패턴(정보 구조)만 추출하고, 스타일은 디자인 토큰으로 통일한다.
**디자인 토큰 매핑 테이블 (Figma 추출 시 작성):**
Figma에서 추출한 색상/폰트/간격을 기존 `tokens.css`와 매핑하고, 누락된 토큰이 있으면 추가한다.
| Figma 속성 | 추출값 (예시) | 기존 토큰 매핑 | 신규 토큰 필요? |
|------------|-------------|--------------|--------------|
| 배경색 (SOLID fill) | `#f8fafc` | `--color-bg-subtle` | - |
| 포인트 색상 | `#1565c0` | `--color-accent` (현재 `#2563eb`) | 검토 필요 |
| 텍스트 색상 | `#1e293b` | `--color-primary` | - |
| 보조 텍스트 | `#64748b` | `--color-neutral` | - |
| 경고/강조 | `#dc2626` | `--color-danger` | - |
| 본문 폰트 크기 | `16px` | `--font-body` (현재 `0.95rem ≈ 15.2px`) | 비율 확인 |
| 제목 폰트 크기 | `24px` | `--font-subtitle` (현재 `1.25rem = 20px`) | 비율 확인 |
| 블록 간 간격 | `20px` | `--spacing-block` | - |
| 내부 패딩 | `16px` | `--spacing-inner` | - |
| 카드 상단 액센트 | `3px solid` | `--accent-border` | - |
이 매핑 테이블은 Phase A-2에서 실제 Figma 값을 추출한 후 정확한 값으로 채운다. 기존 토큰과 차이가 크면 토큰을 추가하되, 기존 토큰의 값을 변경하지 않는다 (기존 7개 블록이 깨질 수 있음).
### 2. 블록 개수 증가 → 디자인 팀장 혼란
| 리스크 | 설명 | 대응 |
|--------|------|------|
| **선택지 과다** | 7개 → 13~15개로 증가 시 Sonnet이 부적절한 블록 선택 가능 | `when` + `not_for` 필드로 선택 기준 명확화. 프롬프트에 "이런 콘텐츠에는 이 블록을 쓰지 마라" 명시 |
| **유사 블록 혼동** | card-grid vs example-card vs icon-list 구분 | 카탈로그에 각 블록의 차이점 명시. 예: "card-grid는 정의, example-card는 출처 있는 사례, icon-list는 기능 나열" |
**대응:** catalog.yaml의 `not_for` 필드가 핵심. "이 블록은 이것에 쓰지 마라"를 명시해야 혼동 감소.
### 3. 기존 파이프라인 깨짐
| 리스크 | 설명 | 대응 |
|--------|------|------|
| **renderer.py 호환성** | 신규 블록 추가 시 기존 렌더링 깨짐 | 기존 7개 블록 테스트 먼저 통과 확인 후 신규 추가 |
| **BLOCK_SLOTS 누락** | design_director.py의 BLOCK_SLOTS에 신규 블록 미등록 | catalog.yaml에서 자동 로드하는 방식으로 전환 |
| **slide-base.html** | grid-template-areas에 신규 area명 미지원 | 동적 생성이므로 문제 없음 (Sonnet이 area명을 직접 지정) |
**대응:** Phase B 완료 후 기존 테스트 케이스(DA-16) 반드시 재실행.
### 4. Figma API 제약
| 리스크 | 설명 | 대응 |
|--------|------|------|
| **CSS 미제공** | Figma REST API는 CSS를 직접 제공하지 않음. 스타일 속성만 제공 | **Framelink MCP 사용 시 자동 CSS 변환.** REST API만 사용 시 수동 변환 필요 |
| **이미지 에셋** | 벡터(VECTOR, ELLIPSE)는 PNG로 렌더링 가능하나 CSS 재현 필요 | **Framelink MCP `download_figma_images`로 일괄 export.** 단순 도형은 CSS, 복잡한 것은 PNG |
| **INSTANCE 참조** | Figma 컴포넌트(Instance)의 master 확인 필요 | `GET /v1/files/{key}/components`로 마스터 컴포넌트 조회 |
| **Rate Limit** | REST API depth 깊은 요청 시 30분 차단 가능 | **Framelink MCP가 토큰 효율적 (raw API 대비 1/5).** 얕게 요청 후 필요한 노드만 상세 조회. 결과 캐싱 |
| **디자인 토큰 접근** | REST API로는 Figma 변수/토큰 추출 불가 (별도 scope 필요) | **Figma 공식 MCP `get_variable_defs` 사용.** 또는 노드 속성에서 수동 추출 |
| **의미적 HTML 추론 불가** | "버튼"은 FRAME+TEXT, "카드"는 FRAME+FRAME — 태그 구분 없음 | 노드 이름(naming convention)으로 추론. 예: `section_title`, `example-card` |
#### MCP 도구 설정
**Framelink MCP (권장, CSS-ready 추출):**
```json
// Claude Code MCP 설정
{
"mcpServers": {
"Framelink MCP for Figma": {
"command": "cmd",
"args": ["/c", "npx", "-y", "figma-developer-mcp", "--figma-api-key=YOUR-KEY", "--stdio"]
}
}
}
```
- `get_figma_data`: 노드 스타일을 CSS-ready YAML로 변환 (색상→hex, 레이아웃→flex, 그림자→box-shadow)
- `download_figma_images`: 이미지 일괄 다운로드 (크롭, 중복 제거 자동)
**Figma 공식 MCP (디자인 토큰 + 스크린샷):**
```
claude mcp add --transport http figma https://mcp.figma.com/mcp
```
- `get_variable_defs`: 디자인 토큰(색상, 간격, 타이포) 추출
- `get_screenshot`: 시각 참고용 스크린샷
- 주의: Free/Starter 플랜 = 월 6회 호출 제한
### 5. Starlight(.astro) 연결 시 충돌
| 리스크 | 설명 | 대응 |
|--------|------|------|
| **CSS 변수 충돌** | Starlight 자체 CSS 변수(`--sl-*`)와 디자인 토큰(`--color-*`) 충돌 | 네임스페이스 분리: `--da-color-*` 접두사 사용 검토 |
| **폰트 로딩** | .astro에서 Pretendard CDN 로드 필요 | `<style is:global>`에 @import 포함 |
| **`<style>` 인라인** | 현재 렌더러가 CSS를 인라인하는데, .astro에서 Starlight CSS와 섞임 | `.astro` 출력 시 scoped style 또는 `is:global` + 높은 specificity |
**대응:** .astro 출력은 Phase C 이후 별도 태스크로. 현재는 독립 HTML 출력에 집중.
### 6. 유사 프로젝트 사례에서 발견된 문제
| 사례 | 문제 | 교훈 |
|------|------|------|
| **SlideSpeak** | 16개 레이아웃으로 시작했으나 실제 콘텐츠 다양성 커버 못함 | 블록을 조합하는 방식이 고정 레이아웃보다 유연 (현재 방식 유지) |
| **PPTAgent (EMNLP 2025)** | 레퍼런스 슬라이드 클러스터링 시 과소/과다 분류 문제 | 블록 수를 10~15개로 제한. 너무 세분화하면 AI 선택이 어려워짐 |
| **Beautiful.ai** | 300개 템플릿 중 실제 사용은 20개 | 처음부터 많이 만들지 않기. 실제 사용 빈도 보고 추가 |
| **InfoDesignLM** | 텍스트만으로 레이아웃 생성 시 콘텐츠 양 ↔ 공간 불일치 | character_limits를 카탈로그에 명시, 텍스트 편집자가 강제 준수 |
---
## 작업 순서 (의존 관계)
```
A-1 (Figma 스크린샷) ──┐
├→ A-3 (패턴 분류) → B-1 (신규 블록 제작) → B-3 (base 업데이트)
A-2 (노드 구조 분석) ──┘ ↓
C-1 (catalog.yaml)
B-2 (변형 추가) → C-2 (팀장 프롬프트 연결)
C-3 (renderer 업데이트)
기존 테스트 재실행 (DA-16)
```
### 예상 소요
| Phase | 작업 | 규모 |
|-------|------|------|
| A (분석) | Figma 스크린샷 + 노드 분석 + 패턴 분류 | 탐색/조사 |
| B (제작) | 신규 블록 6~8종 + 변형 3종 + base 업데이트 | 구현 (핵심) |
| C (카탈로그) | catalog.yaml + 팀장 프롬프트 + renderer 업데이트 | 연결/통합 |
**핵심 원칙:** Phase A에서 패턴을 정확히 분류하지 않으면 Phase B에서 쓸모없는 블록을 만들게 된다. 분석 먼저, 제작은 그 다음.
---
## 산출물 목록
```
docs/
├── figma-screenshots/ # A-1: 각 프레임 PNG
├── figma-analysis/ # A-2: 노드 구조 문서 + 디자인 토큰 매핑 테이블
└── FIGMA-COMPONENT-EXTRACTION-PLAN.md # 이 파일
templates/
├── catalog.yaml # C-1: 블록 카탈로그 (AI용 메뉴판 + 사람용 참고)
├── blocks/
│ ├── comparison/ # 기존 (폴더 구조로 재편)
│ │ ├── default.html
│ │ └── preview.png
│ ├── card-grid/ # 기존
│ │ ├── default.html
│ │ ├── icon.html # B-2: 변형
│ │ └── preview.png
│ ├── relationship/ # 기존
│ ├── process/ # 기존
│ ├── quote-block/ # 기존
│ │ ├── default.html
│ │ ├── decorated.html # B-2: 변형
│ │ └── preview.png
│ ├── conclusion-bar/ # 기존
│ ├── comparison-table/ # 기존
│ ├── section-title/ # B-1: 신규
│ │ ├── default.html
│ │ └── preview.png
│ ├── example-card/ # B-1: 신규
│ │ ├── single.html
│ │ ├── 2col.html
│ │ ├── 3col.html
│ │ └── preview.png
│ ├── image-block/ # B-1: 신규 (3변형)
│ │ ├── full.html # 전체 너비 (핵심 도표)
│ │ ├── side.html # 텍스트 옆 (보조 이미지)
│ │ ├── thumb.html # 썸네일 (참고 문서)
│ │ └── preview.png
│ ├── image-gallery/ # B-1: 신규
│ │ ├── 2col.html
│ │ ├── 3col.html
│ │ ├── 2x2.html
│ │ └── preview.png
│ ├── timeline/ # B-1: 신규
│ │ ├── vertical.html
│ │ ├── horizontal.html
│ │ └── preview.png
│ ├── big-number/ # B-1: 신규
│ │ ├── 3col.html
│ │ ├── 4col.html
│ │ └── preview.png
│ ├── icon-list/ # B-1: 신규
│ │ ├── vertical.html
│ │ ├── grid.html
│ │ └── preview.png
│ └── details-block/ # B-1: 신규
│ ├── default.html
│ └── preview.png
├── slide-base.html # B-3: 업데이트 (슬라이드 모드)
└── page-base.html # 향후: 웹/스크롤 모드
samples/ # 완성 슬라이드 레시피
├── dx-bim-comparison/
│ ├── slide.html # 완성 HTML
│ ├── preview.png # 스크린샷
│ └── meta.yaml # 사용된 블록 조합 + Figma 원본 참조
└── ...
src/
├── design_director.py # C-2: catalog.yaml 연동
└── renderer.py # C-3: 신규 블록 지원
```
---
## PROGRESS.md 연동
이 계획의 태스크들은 PROGRESS.md에 아래와 같이 등록한다:
### Phase F (Figma 컴포넌트 추출) — PROGRESS.md 등록 항목
| 태스크 | 상태 | 의존성 | 메모 |
|--------|------|--------|------|
| F-A1: Figma 스크린샷 확보 | todo | MCP 설치 후 | Framelink MCP 또는 REST API |
| F-A2: 노드 구조 심층 분석 | todo | F-A1 | CSS-ready 데이터 + 토큰 매핑 테이블 |
| F-A3: 패턴 분류 + 명명 | todo | F-A1, F-A2 | 블록 후보 목록 확정 |
| F-B1: 신규 블록 템플릿 제작 (8~10종) | todo | F-A3 | 폴더 구조, 변환 규칙 적용 |
| F-B1a: Figma 웹→블록 변환 규칙 검증 | todo | F-B1 | 토큰 매핑, 높이 auto 확인 |
| F-B2: 기존 블록 변형 추가 | todo | F-B1 | CSS만 변형, 슬롯 유지 |
| F-B3: slide-base.html 업데이트 | todo | F-B1 | 기존 7개 블록 깨지지 않는지 검증 |
| F-C1: catalog.yaml 생성 | todo | F-B1 | when/not_for/slots/char_limits |
| F-C2: 팀장 프롬프트 연결 | todo | F-C1 | catalog.yaml → 시스템 프롬프트 |
| F-C3: renderer.py 업데이트 | todo | F-C1 | 신규 블록 로드 + BLOCK_SLOTS 동기화 |
| F-T1: 기존 테스트 재실행 | todo | F-B3, F-C3 | DA-16 기존 케이스 전체 통과 확인 |
**사전 세팅 (Figma 작업 시작 전 필요):**
| 세팅 | 상태 | 설명 |
|------|------|------|
| Framelink MCP 설치 | todo | `npx figma-developer-mcp --figma-api-key=...` |
| Figma 공식 MCP 설치 | todo | `claude mcp add --transport http figma https://mcp.figma.com/mcp` |
| 기존 블록 폴더 구조 재편 | todo | 플랫 파일 → 블록별 폴더 (`templates/blocks/{name}/`) |
| preview.png 생성 방법 확보 | todo | HTML을 브라우저로 열어 스크린샷 (수동 또는 Playwright) |
---
## 금지 사항
1. Figma 디자인을 1:1 복제하지 않는다 (정보 구조만 추출, 장식은 제거, 스타일은 토큰 기준)
2. 기존 7개 블록 템플릿을 수정하지 않는다 (신규/변형은 별도 파일)
3. 한 번에 모든 블록을 만들지 않는다 (A-3 분류 결과를 보고 우선순위 재조정)
4. catalog.yaml 없이 블록을 추가하지 않는다 (카탈로그 미등록 = 디자인 팀장이 모름)
5. Kei Persona Agent 코드를 수정하지 않는다
6. 블록의 높이를 고정 px로 하드코딩하지 않는다 (모드 독립적이어야 함)
7. 기존 `tokens.css`의 값을 변경하지 않는다 (신규 토큰 추가는 가능, 기존 값 변경은 7개 블록 깨짐 위험)
8. 이미지를 crop하지 않는다 (원본 그대로, 크기만 조절)
9. 그라데이션, 호버, 애니메이션 등 Figma 장식 요소를 가져오지 않는다

View File

@@ -0,0 +1,150 @@
# Figma → HTML 변환 프로세스 리뷰
> 2026-04-08~09 테스트 세션 결과. 기존 FIGMA-EXTRACTION.md / FIGMA-DESIGN-LANGUAGE.md는 그대로 유지.
---
## 1. 테스트 경과
### 1.1 테스트 대상
| 순서 | 프레임 | 노드수 | 성격 | 결과 |
|------|--------|-------|------|------|
| 1 | Frame 1171281214 (37:231) | ~15 | 단일 카드 (H/W 탭+라벨+본문) | 부분 성공 (둥근 모서리 누락) |
| 2 | Frame 1171281215 (39:239) | 149 | 시스템 구성 (H/W 7항목 + 중앙원 + S/W 6항목) | 부분 성공 (색상 차이 다수 누락) |
| 3 | Frame 1171280278 (17:3403) | 43 | 실제 디자인 (사진, 3D지형, 자유배치) | 미시도 (구조 분석 한계) |
### 1.2 발견된 누락 사항 (시간순)
| # | 누락 | 원인 | Figma 필드 |
|---|------|------|-----------|
| 1 | 평행사변형 우측 상단 Bezier curve | `vectorNetwork.vertices`만 봄, `fillGeometry.path``C` 명령어 미확인 | `fillGeometry[].path` |
| 2 | 그라디언트 바 우측 pill-shape | `cornerRadius` 단일값만 체크 | `rectangleCornerRadii` (예: `[0,40,40,0]`) |
| 3 | S/W 그라디언트 바 색상 (크림→주황) | H/W 바 색상을 S/W에도 일괄 적용 | `fills[].gradientStops` — 인스턴스별 확인 필요 |
| 4 | S/W 아이콘 색상 (주황 vs H/W 올리브) | 같은 imageRef라고 동일 취급 | `fills[].filters` (tint, highlights, shadows) |
| 5 | 텍스트 위치 오류, 겹침 | 149노드를 플랫하게 absolute 배치 | 트리 계층 무시가 근본 원인 |
---
## 2. 근본 원인 분석
### 2.1 작업 방식의 문제
```
현재 방식:
curl API → 3.6MB JSON 덤프 → 임시 Python 스크립트로 파싱
→ 필요해 보이는 필드만 선택적으로 읽음
→ 좌표를 눈으로 읽고 HTML에 하드코딩
→ 사용자 지적 → 수정 → 또 지적 → 또 수정...
문제:
1. 임시 스크립트가 매번 다르고, 추출 범위가 일정하지 않음
2. "중요해 보이는" 필드만 골라 읽으니 형상/필터/개별반지름 등을 놓침
3. 같은 패턴 반복 요소의 속성 차이를 대조하지 않음
4. 트리 계층을 무시하고 플랫하게 절대좌표 배치
5. 전체를 한번에 만들어서 오류 발견이 늦음
```
### 2.2 "배치 우선" 편향
```
AI의 파싱 우선순위 (잘못됨):
1. 어디에 있나 (x, y, width, height) ← 먼저 봄
2. 무슨 색이나 (fills, color) ← 그다음
3. 무슨 글자나 (characters, fontSize) ← 그다음
4. 어떤 모양이나 (path, cornerRadius) ← 마지막... 놓침
5. 인스턴스 간 차이 (filters, gradient) ← 아예 안 봄
디자인에서는 4, 5가 핵심임
```
---
## 3. FIGMA-EXTRACTION.md 보강 필요 사항
> 기존 MD는 유지. 아래 항목들은 방향 확정 후 반영.
### 3.1 섹션 2.4 "추출해야 하는 핵심 데이터" 추가 필드
| 추가 필드 | 용도 |
|----------|------|
| `rectangleCornerRadii` | 꼭짓점별 다른 반지름 (예: `[0,40,40,0]` = 우측만 둥글게) |
| `fillGeometry[].path` | SVG path에 `C`/`Q` 곡선 명령어가 있으면 직선이 아닌 형상 |
| `arcData` | 호/부채꼴 형상 |
| `fills[].filters` | 같은 이미지라도 노드별 필터(tint, highlights, shadows)로 색상 변경 |
### 3.2 워크플로우 개선 방향 (미확정)
**소분 → 단계적 조립**:
```
한번에 전체를 만들지 않는다.
트리 leaf부터 올라감:
→ leaf 변환 (개별 확인)
→ 부모 그룹으로 조립
→ 다음 레벨로 조립
→ 최종 프레임
각 단계에서 반드시:
- 같은 패턴의 인스턴스끼리 속성 대조
- 이미지 노드의 filters 확인
- 텍스트 겹침 없는지 좌표 간격 확인
```
**단, Figma 트리가 깔끔한 건 사용자가 수작업으로 정리했기 때문.**
원본 Figma는 자동 이름 + 의미 없는 중첩이 겹겹이 있는 상태.
→ AI가 뒤죽박죽인 구조를 정확히 읽으려면 **Figma MCP 활용이 필수**.
---
## 4. 다음 세션에서 이어갈 것
### 4.1 Figma MCP 테스트
- **작업 디렉토리를 `D:\ad-hoc\kei\design_agent\`로 열어야** `.mcp.json`의 Figma MCP가 인식됨
- 현재 세션은 `D:\`에서 열려서 MCP 미인식
- API 키는 업데이트 완료: `figd_-eLtFZz5itRec7N60iJFB1njw1nKH8T_X_PM205T`
### 4.2 테스트 대상
```
Figma URL: https://www.figma.com/design/9S6LsQyO6zlRxtiqZccOUM/Untitled?node-id=18-8204
대상: Frame 1171281172
목표: MCP로 구조를 읽고 → HTML로 변환 → 정확도 확인
```
### 4.3 검증 포인트
- MCP가 트리 구조를 계층적으로 탐색할 수 있는지
- 노드별 시각 속성 (fills, filters, gradientStops, cornerRadii 등)을 빠짐없이 읽는지
- 읽은 결과를 기반으로 소분→조립 방식으로 HTML 변환이 가능한지
### 4.4 성공 기준
MCP 기반으로 변환했을 때, 사용자가 지적하기 전에 다음을 스스로 잡아낼 수 있어야 함:
- 둥근 모서리 / 곡선 형상
- 인스턴스별 색상 차이 (그라디언트, 필터)
- 텍스트 겹침 / 위치 오류
---
## 5. 사용자가 공유한 참고 자료
| 자료 | URL | 비고 |
|------|-----|------|
| Figma→HTML 플러그인 | https://www.figma.com/community/plugin/1421932899298722297 | Convert Figma Design to HTML CSS |
| Hubannero 플러그인 | https://www.figma.com/community/plugin/1527963216001787676 | Figma to HTML/MP4/GIFs |
| GitHub Copilot MCP | https://github.com/webmaxru/figma-to-webpage-github-copilot-mcp | Figma→Webpage via MCP |
| SKT UX MCP | https://github.com/banil-la/figma-mcp-skt-ux | Figma MCP for UX |
| MCP Market | https://mcpmarket.com/ko/server/figma-to-ai-html-converter | Figma→AI HTML Converter |
| LobeHub Skills | https://lobehub.com/skills/skill.md | Skill definitions |
---
## 6. 기존 MD와의 관계
| 문서 | 역할 | 상태 |
|------|------|------|
| `FIGMA-DESIGN-LANGUAGE.md` | 디자인 토큰, 색상, 타이포, 레이아웃 패턴 | 유지 (변경 없음) |
| `FIGMA-EXTRACTION.md` | 추출 워크플로우, 수학적 계산, 체크리스트 | 유지 (보강 예정, 미반영) |
| `FIGMA-CONVERSION-REVIEW.md` | **이 문서**. 테스트 결과, 인사이트, 다음 세션 이어갈 지점 | 신규 |

View File

@@ -0,0 +1,164 @@
# Figma Design Language Analysis
> Phase 1 결과 문서 (2026-04-07)
> Figma Source: `9S6LsQyO6zlRxtiqZccOUM` / Page 1
## 1. 스코프
| 프레임 | 역할 | 판정 |
|--------|------|------|
| Frame 1 (1:3) | 3D 수렴 화살표 | 서브 컴포넌트 (장식 이미지) |
| Frame 2 (1:5) | Solution 제작 목표 | **블록화** → hero-icon-cards |
| Frame 3 (1:35) | 정책 달성 (Engn.Solution vs DfMA) | **블록화** → compare-2col-badge |
| Frame 4 (1:49) | 과정 vs 결과의 혁신 | **블록화** → compare-detail-gradient |
| Frame 5 (1:74) | 상세보기 버튼 | 서브 컴포넌트 (CTA) |
| Frame 6 (1:80) | 정책방향 (세로 문서) | **제외** (1280×720 부적합) |
## 2. 스케일 변환
Figma 캔버스 → 슬라이드(1280px) 변환 비율:
- Frame 2, 3: ×0.71 (1808px → 1280px)
- Frame 4: ×0.33 (3848px, 양쪽 합쳐서 2패널)
| Figma | 슬라이드 환산 | 역할 |
|-------|-------------|------|
| 70px | 28-35px | 대섹션 헤더 |
| 60px | 24-28px | Hero 메시지 |
| 50px | 22-26px | 섹션 제목, 배지 |
| 45px | 20-22px | 카드 타이틀 (EN) |
| 40px | 16-20px | 본문 |
| 35px | 14-18px | 부제, 한국어 서브 |
| 32px | 12-14px | 버튼 |
## 3. 색상 팔레트 (Warm Theme)
기존 블루/슬레이트 테마와 **병존**하는 새 팔레트:
| 토큰 | Hex | Figma 원본 | 용도 |
|------|-----|-----------|------|
| `--color-warm-brown` | `#5C3714` | rgba(92,55,20) | 과정/프로세스 섹션 제목 |
| `--color-dark-teal` | `#084C56` | rgba(8,76,86) | 결과/디지털 섹션 제목 |
| `--color-teal` | `#227582` | rgba(34,117,130) | 설명 텍스트 |
| `--color-forest` | `#548235` | rgba(84,130,53) | 배경 그라디언트 |
| `--color-beige` | `#E4D9C0` | rgba(228,217,192) | 서브틀 배경/버튼 |
| `--color-warm-yellow` | `#FAEDCB` | rgba(250,237,203) | 하이라이트 바 |
### 그라디언트 패턴
- 왼쪽(과정): `rgba(165,161,150,0.10) → rgba(57,50,30,1.00)` (베이지→브라운)
- 오른쪽(결과): `rgba(41,107,85,0.10) → rgba(3,33,24,1.00)` (틸→다크)
- 버튼: `rgba(255,255,255,0.00) → rgba(228,217,192,1.00)` (투명→베이지)
- 배경: `rgba(84,130,53,1.00) → rgba(37,62,31,0.00)` (그린→투명)
## 4. 타이포그래피
- **폰트**: Pretendard Variable 유지 (Noto Sans KR은 이미 fallback)
- **핵심은 크기/굵기 위계**
| 레벨 | 크기 (슬라이드) | Weight | 스트로크 | 정렬 |
|------|---------------|--------|---------|------|
| Hero Statement | 24-28px | 700 | white 1.5px | center |
| Section Header | 28-35px | 900 | white 5px | center/left |
| Badge Title | 22-26px | 700 | 없음 | center |
| Card Title (EN) | 20-22px | 900 | white 5px | center |
| Card Subtitle (KR) | 14-18px | 500 | white 1.5px | center |
| Body Text | 16-20px | 700 | white 1px | left |
| Section Sub-title | 22-26px | 900 | 없음 | left |
### 텍스트 스트로크 기법
Figma 디자인의 특징: 다양한 배경 위에서 가독성 확보를 위해 **흰색 스트로크** 사용
```css
-webkit-text-stroke: 1.5px white; /* 일반 텍스트 */
-webkit-text-stroke: 5px white; /* 강조 텍스트 */
paint-order: stroke fill; /* 스트로크가 텍스트 뒤로 */
```
## 5. 레이아웃 패턴
### A. Badge Header
- 이미지/그라디언트 배경 위 `border-radius: 20px`
- 중앙 흰색 텍스트 (50px/700 → 22-26px/700)
- 높이: ~88px (Figma) → ~44-50px (슬라이드)
### B. Hero Statement
- 전체 폭 중앙 정렬
- 큰 텍스트 (60px/700) + 흰색 스트로크
- 키워드 **굵은 강조** 가능
### C. Icon Card Row
- N개 카드 수평 배치, 세로 구분선
- 각 카드: 아이콘 이미지 + 영문 제목(900) + 한국어 부제(500)
- 흰색 둥근 컨테이너 (borderRadius: 20)
### D. Two-Col Comparison
- 좌/우 그라디언트 배경
- 각 열: 헤더 바 + (섹션 제목 + 본문) × N개
- 색상으로 좌/우 구분 (브라운 vs 틸)
### E. CTA Button
- 그라디언트 바 (투명→베이지) + 둥근 버튼 (r:7)
- 흰색 텍스트
## 6. 디자인 시스템 vs 콘텐츠 전용 경계
### 디자인 시스템 (블록에 포함)
- 색상 팔레트, 그라디언트 패턴
- 타이포그래피 위계, 텍스트 스트로크
- 둥근 모서리 컨테이너 (r:20)
- Badge Header, 2열 비교, N열 카드 레이아웃 구조
### 콘텐츠 전용 (블록에 포함하지 않음)
- 3D 화살표 이미지 (Frame 1) → 콘텐츠가 제공
- 특정 아이콘 이미지들 (brain, thunder 등) → 콘텐츠가 제공
- 도메인 텍스트 → 슬롯으로 처리
---
## 7. 추가 블록 (Page 2, 3, 4)
> 2026-04-08 추가
### Page 2 (15:2) — 프레젠테이션 슬라이드
| 블록 | 출처 | 설명 |
|------|------|------|
| `category-strip-table` | 001_개요 우측 하단 | 컬러 스트립 N열 테이블 (기술/사람/자연) |
- 다크 배경, 좌측 색상 바(세로 라벨) + 제목/본문 M행 반복
- N열 동적 (2~5), 색상 바 색상은 열마다 지정
- scale = 1200/2123 = 0.5652
### Page 3 (18:8204) — 컴포넌트
| 블록 | 출처 | 설명 |
|------|------|------|
| `checklist-dark` | f5 (1770×553) | 체크 아이콘 + 제목:설명 N행 리스트 |
| `system-2col-center` | f8 (2446×1943) | 좌/우 항목 + 중앙 원형 라벨 |
- checklist-dark: 다크 배경, 주황 체크(☑), 제목:설명 한 줄 구조
- system-2col-center: 3열 Grid (좌 항목 + 중앙 원 + 우 항목), 색상 탭
### Page 4 (29:439) — 순환 다이어그램
| 블록 | 출처 | 설명 |
|------|------|------|
| `cycle-orbit` | Frame 1 (1076×292) | 3D 원 투영 순환 궤도 다이어그램 |
핵심 수학:
- **3D 원 → Z축 기울임(80°) → 2D 투영** (토성 고리 원리)
- `project(α) = (cx + R×cos(α), cy + R×sin(α)×cos(80°))`
- N개 노드: `360°/N` 간격, 사이각 2/3로 축소 (앞쪽 가까워짐)
- 하단 중심(90°) 기준 좌/우 대칭 배치
- 설명 텍스트: 좌측 노드 → 이름 좌측에, 우측/상단 노드 → 이름 우측에
- 화살표: 호 위 1/3, 2/3 지점에 접선 방향 회전
## 8. 전체 블록 목록 (7개)
| # | 블록 ID | 카테고리 | 출처 | 핵심 특징 |
|---|--------|---------|------|----------|
| 1 | `hero-icon-cards` | cards | Page 1 | 3D 리본 배지 + 빨간 테두리 박스 + N열 카드 |
| 2 | `compare-2col-badge` | cards | Page 1 | 3D 리본 탭 + 틸 테두리 2열 비교 |
| 3 | `compare-detail-gradient` | cards | Page 1 | 비대칭 라운드 헤더 + Grid 행 정렬 + As-Is/To-Be |
| 4 | `category-strip-table` | cards | Page 2 | 컬러 스트립 바 + 다크 배경 N열 테이블 |
| 5 | `checklist-dark` | emphasis | Page 3 | 체크 아이콘 + 제목:설명 다크 리스트 |
| 6 | `system-2col-center` | cards | Page 3 | 중앙 원형 라벨 + 좌/우 항목 Grid |
| 7 | `cycle-orbit` | visuals | Page 4 | 3D 원 투영 SVG 순환 궤도 |

View File

@@ -0,0 +1,674 @@
# Figma → HTML 블록 변환 프로세스
> 2026-04-07 확립. Figma 디자인을 design_agent 블록으로 변환하는 정확한 방법론.
---
## 1. 전체 워크플로우
```
[Step 1] Figma API로 파일 구조 추출
[Step 2] 프레임별 렌더링 이미지(PNG) 다운로드
[Step 3] 노드별 상세 데이터 추출 (좌표, 색상, 폰트, 크기)
[Step 4] 디자인 언어 분석 (공통 패턴 vs 콘텐츠 전용 구분)
[Step 5] 블록 설계 (슬롯, 동적 규칙, schema)
[Step 6] 수학적 계산 (Figma 좌표 → 스케일 → CSS값)
[Step 7] HTML/CSS 구현
[Step 8] 비교 리뷰 (Figma PNG vs HTML, 같은 폭으로 위/아래 배치)
[Step 9] 피드백 반영 → Step 6~8 반복
[Step 10] Jinja2 템플릿화 + catalog.yaml 등록
```
---
## 2. Figma API 사용법
### 2.1 파일 구조 가져오기
```bash
curl -s -H "X-Figma-Token: {TOKEN}" \
"https://api.figma.com/v1/files/{FILE_KEY}" \
| python -m json.tool
```
### 2.2 특정 노드 상세 데이터
```bash
curl -s -H "X-Figma-Token: {TOKEN}" \
"https://api.figma.com/v1/files/{FILE_KEY}/nodes?ids={NODE_IDS}&geometry=paths"
```
### 2.3 노드 이미지 렌더링 (PNG)
```bash
curl -s -H "X-Figma-Token: {TOKEN}" \
"https://api.figma.com/v1/images/{FILE_KEY}?ids={NODE_IDS}&format=png&scale=2"
```
- `scale=2`: 2배 해상도로 다운로드 (선명도 확보)
- 응답의 `images` 객체에 각 노드 ID별 S3 URL 제공
### 2.4 추출해야 하는 핵심 데이터
| 데이터 | API 필드 | 용도 |
|-------|---------|------|
| 위치 | `absoluteBoundingBox.x, .y` | 요소 간 관계 계산 |
| 크기 | `absoluteBoundingBox.width, .height` | 스케일 계산 |
| 텍스트 | `characters` | 콘텐츠 확인 |
| 폰트 | `style.fontFamily, .fontSize, .fontWeight` | 타이포그래피 |
| 색상 | `fills[].color` | 색상 팔레트 |
| 테두리 | `strokes[], strokeWeight` | 박스 스타일 |
| 라운드 (단일) | `cornerRadius` | 4꼭짓점 동일 border-radius |
| **라운드 (개별)** | **`rectangleCornerRadii`** | **[TL, TR, BR, BL] 꼭짓점별 다른 반지름. pill-shape 등 비대칭 라운드 감지** |
| **형상 경로** | **`fillGeometry[].path`** | **SVG path 문자열. `C`/`Q`/`A` 명령어 존재 시 곡선 형상 → `clip-path: path()` 또는 SVG로 구현** |
| **호/부채꼴** | **`arcData`** | **원호, 부채꼴 등 호 형상 파라미터** |
| 이미지 | `fills[].imageRef` | 이미지 자산 식별 |
---
## 3. 수학적 계산 (핵심)
### 3.1 스케일 팩터
```
슬라이드 콘텐츠 폭 = 1280px - padding(40px × 2) = 1200px
scale = 1200 / figma_frame_width
```
| Figma 프레임 | 폭 | 스케일 |
|-------------|-----|--------|
| Frame 2 | 1808px | 0.6637 |
| Frame 3 | 1807px | 0.6641 |
| Frame 4 | 3848px | 0.3118 |
### 3.2 요소 간 정렬 계산
**절대 원칙: Figma 좌표 차이값 → 스케일 적용 → CSS값**
```python
# 예: 리본 접힘선과 박스 테두리 정렬
badge_y = 1431 # Figma에서 badge 이미지 top Y
box_y = 1449 # Figma에서 box top Y
fold_offset = box_y - badge_y # = 18px (Figma 기준)
# 스케일 적용
fold_offset_css = round(fold_offset * scale) # = 12px (CSS)
```
**금지: "좀 더 올려볼게요" 식의 시행착오 px 조정**
### 3.3 이미지 자산 크기 계산
```python
# Figma 원본 크기에 스케일 적용
ribbon_width_css = round(badge_img_width * scale)
ribbon_height_css = round(badge_img_height * scale)
# 비율 계산 (CSS에서 width만 지정하면 height는 자동)
aspect_ratio = badge_img_width / badge_img_height
```
### 3.4 패딩/여백 계산
```python
# 리본이 박스 안에 들어오는 높이 = 리본 전체 높이 - 접힘선 오프셋
ribbon_inside_box = ribbon_height_css - fold_offset_css
# 박스 상단 패딩 = 리본 침입 높이 + 여유
box_padding_top = ribbon_inside_box + 6 # 6px 여유
```
### 3.5 실제 계산 예시 (Frame 2)
```
입력 (Figma 원본):
badge 이미지: 508×94px, y=1431
box: y=1449
frame width: 1808px
계산:
scale = 1200/1808 = 0.6637
ribbon_w = 508 × 0.6637 = 337px
ribbon_h = 94 × 0.6637 = 62px
fold_offset = (1449-1431) × 0.6637 = 12px
ribbon_below_fold = 62 - 12 = 50px
box_padding_top = 50 + 6 = 56px
CSS 출력:
.ribbon { width: 337px; top: -12px; }
.box { padding-top: 56px; }
```
---
## 4. 이미지 자산 처리
### 4.1 CSS로 만들면 안 되는 것
| 요소 | 이유 | 처리 |
|------|------|------|
| 3D 리본/두루마리 | 입체감, 그림자, 곡면 → CSS 불가 | Figma에서 PNG 추출 |
| 복잡한 그라디언트 배경 | 다중 정지점, 비선형 → CSS 근사 불가 | 이미지 사용 |
| 아이콘 이미지 | 디자이너가 만든 고유 자산 | 원본 이미지 사용 |
### 4.2 CSS로 만들 수 있는 것
| 요소 | CSS 구현 |
|------|---------|
| 단색/2색 그라디언트 배경 | `linear-gradient()` |
| 둥근 모서리 테두리 박스 | `border + border-radius` |
| 텍스트 스타일 | `font-size, font-weight, color` |
| 그리드/플렉스 레이아웃 | `display: grid / flex` |
| 구분선 | `border` or `background` |
### 4.3 이미지 추출 및 저장
```bash
# Figma API로 특정 노드 이미지 추출
curl -s -H "X-Figma-Token: {TOKEN}" \
"https://api.figma.com/v1/images/{FILE_KEY}?ids={NODE_ID}&format=png&scale=2"
# 다운로드 → static/figma-assets/ 에 저장
curl -s -o static/figma-assets/{name}.png "{S3_URL}"
```
저장 위치: `static/figma-assets/`
---
## 5. 비교 리뷰 페이지 작성법
### 5.1 레이아웃
```
같은 폭으로 위/아래 배치 (좌/우 아님 — 크기 차이 문제)
┌─ 빨간 테두리 ──────────────┐
│ Figma Original (PNG) │
└─────────────────────────────┘
─ 구분선 ─
┌─ 초록 테두리 ──────────────┐
│ HTML Block │
└─────────────────────────────┘
```
### 5.2 HTML 스케일링
```css
.html-inner {
width: 1280px; /* 슬라이드 원본 크기 */
transform-origin: top left;
transform: scale(0.74); /* 960px 컨테이너에 맞춤: 960/1280 */
}
```
### 5.3 비교 리뷰 파일 위치
`data/figma_ref/comparison.html`
---
## 6. Jinja2 템플릿 변환 규칙
### 6.1 고정값 → 변수
```html
<!-- Figma 원본의 텍스트 → Jinja2 변수 -->
<span>정책 달성</span><span>{{ badge_title }}</span>
<span>Engn. Solution</span><span>{{ left_title }}</span>
```
### 6.2 반복 요소 → 루프
```html
<!-- N개 카드 → for loop -->
{% for card in cards %}
<div class="card">{{ card.title }}</div>
{% endfor %}
```
### 6.3 이미지 자산 → 슬롯
```html
<!-- 리본 이미지: 색상에 따라 다른 자산 사용 가능 -->
<img src="{{ ribbon_image | default('figma-assets/badge_solution.png') }}">
```
### 6.4 계산된 CSS → CSS 변수
```html
<!-- 수학적 계산 결과를 CSS 변수로 -->
<div style="--ribbon-width: {{ ribbon_width }}px; --fold-offset: {{ fold_offset }}px;">
```
---
## 7. 디자인 언어 vs 콘텐츠 전용 구분
### 디자인 언어 (블록에 포함, 재사용 가능)
- 색상 팔레트 (warm 테마: 브라운, 틸, 베이지)
- 타이포그래피 위계 (크기, 굵기 단계)
- 레이아웃 구조 (2열 비교, N열 카드 등)
- 장식 요소 (3D 리본, 둥근 컨테이너)
### 콘텐츠 전용 (블록에 포함하지 않음)
- 특정 텍스트 ("디지털전환은 사용자...")
- 특정 아이콘 이미지 (brain, thunder 등)
- 도메인 전문 용어 (DfMA, Engn. Solution)
---
## 8. 파일 구조
```
design_agent/
├── static/figma-assets/ ← Figma에서 추출한 이미지 자산
│ ├── badge_policy.png (틸 3D 리본)
│ ├── badge_solution.png (빨간 3D 리본)
│ ├── box_policy_container.png
│ ├── box_solution_cards.png
│ └── arrow_asis_tobe.png (As-Is→To-Be 화살표)
├── data/figma_ref/ ← 비교 리뷰용
│ ├── comparison.html (Figma vs HTML 비교 페이지 — 전체 블록)
│ ├── frame2_1-5.png (Page 1 Figma 원본)
│ ├── frame3_1-35.png
│ ├── frame4_1-49.png
│ ├── strip_table.png (Page 2 필수조건 테이블)
│ ├── checklist_dark.png (Page 3 체크리스트)
│ ├── system_2col.png (Page 3 시스템 구성)
│ └── cycle_orbit.png (Page 4 순환 궤도)
├── templates/blocks/cards/ ← 카드 블록 (Figma 신규 5개)
│ ├── hero-icon-cards.html (Page 1 — 히어로 + N열 아이콘 카드)
│ ├── compare-2col-badge.html (Page 1 — 3D 리본 배지 + 2열 비교)
│ ├── compare-detail-gradient.html (Page 1 — 그라디언트 상세 2열 비교)
│ ├── category-strip-table.html (Page 2 — 컬러 스트립 N열 테이블)
│ └── system-2col-center.html (Page 3 — 중앙 라벨 + 좌/우 항목)
├── templates/blocks/emphasis/ ← 강조 블록 (Figma 신규 1개)
│ └── checklist-dark.html (Page 3 — 체크 아이콘 + 제목:설명 리스트)
├── templates/blocks/visuals/ ← 비주얼 블록 (Figma 신규 1개)
│ └── cycle-orbit.html (Page 4 — 3D 원 투영 순환 궤도 다이어그램)
├── FIGMA-DESIGN-LANGUAGE.md ← 디자인 언어 분석 결과
├── FIGMA-EXTRACTION.md ← 이 문서
└── PHASE-FIGMA-BLOCKS.md ← 블록 설계 명세
```
---
## 9. 고급 레이아웃 패턴
### 9.1 좌/우 열 섹션 Y선 정렬 (CSS Grid 행 공유)
2열 비교에서 좌/우 섹션 제목이 같은 Y선에 있어야 할 때:
**문제**: 각 열을 독립 flex-column으로 만들면, 좌측 섹션 본문이 길면 우측 다음 섹션이 밀림.
```
flex-column (잘못):
좌: [제목1] [긴본문] [제목2]
우: [제목1] [짧은본문] [제목2] ← 제목2가 좌측과 Y가 다름
```
**해결**: CSS Grid 2열 × N행으로 행을 공유하면 자동 정렬.
```css
.block {
display: grid;
grid-template-columns: 1fr 1fr; /* 2열 */
grid-template-rows: auto auto auto auto; /* 헤더 + N행 */
}
```
```
Grid (올바름):
[좌 헤더] [우 헤더] ← Row 0
[좌 섹션1] [우 섹션1] ← Row 1 (행 높이 = max(좌,우))
[좌 섹션2] [우 섹션2] ← Row 2 (Y선 자동 정렬!)
```
**실제 계산 (Frame 4)**:
```
Figma Y좌표:
Row 1: 좌 1166, 우 1166 → 0px 차이 (이미 정렬)
Row 2: 좌 1529, 우 1467 → 62px 차이 (Grid가 해결)
Row 3: 좌 1845, 우 1845 → 0px 차이 (이미 정렬)
원인: Row 1 좌측에 As-Is→To-Be 구조가 있어서 본문이 62px 더 높음
```
### 9.2 As-Is → To-Be 수평 서브 레이아웃
한 섹션 안에서 변환 전/후를 수평 배치할 때:
```html
<div class="asis-tobe">
<div class="asis">
<div class="bullet">이전 상태 1</div>
<div class="bullet">이전 상태 2</div>
</div>
<img src="arrow.png" class="arrow" alt="→">
<div class="tobe">
<div class="bullet">변환 후 1</div>
<div class="bullet">변환 후 2</div>
</div>
</div>
```
```css
.asis-tobe { display: flex; align-items: center; gap: 8px; }
.asis, .tobe { flex: 1; }
.arrow { width: 60px; height: auto; flex-shrink: 0; }
```
**Figma 좌표로 검증**:
```
As-Is: x=2737, w=539
Arrow: x=3375, w=252
To-Be: x=3687, w=672
→ 세 요소가 같은 Y(1269)에 수평 배치됨을 좌표로 확인
```
### 9.3 3D 리본/두루마리 배지 정렬 공식
리본 이미지의 접힘선(fold-back)이 박스 테두리와 정확히 일치해야 할 때:
```
┌── 리본 이미지 ──────────────┐
│ 접힘 삼각형 (fold) │ ← fold_offset (이미지 top에서)
│ 리본 본체 │
│ │
└──────────────────────────────┘
════════════════════════════════ ← 박스 top border (여기에 fold가 일치해야 함)
┌── 박스 ──────────────────────┐
│ padding-top = ribbon_below │
│ 콘텐츠 시작 │
계산:
fold_offset = (box_y - badge_y) × scale → CSS: top 값
ribbon_below = ribbon_height - fold_offset → 박스 안 침입 높이
box_padding_top = ribbon_below + 여유(6px) → 콘텐츠 겹침 방지
```
**핵심**: 리본을 올리거나 내리는 게 아니라, **박스의 위치를 계산**하는 것.
- `top: -fold_offset` → 리본 접힘선 = 박스 top border
- 리본은 그대로, 박스와의 관계만 수학적으로 결정
---
## 10. 순환 궤도 다이어그램 (cycle-orbit) 수학 공식
### 10.1 핵심 개념: 3D 원 → Z축 기울임 → 2D 투영
타원이 아니라 **3D 원을 Z축으로 기울인 것**. 토성의 고리와 같은 원리.
```
3D 공간: 2D 투영 (화면):
y y
| / z |
| / |
| / θ=80° (기울임) |
|/______ x |______ x
원 (R=400) 타원 (rx=400, ry=69)
→ Z축으로 80° 뒤로 눕힘 → y축이 cos(80°)=0.1736으로 압축
```
### 10.2 기본 공식
```python
import math
R = 400 # 3D 원 반지름
cx, cy = 500, 200 # 원 중심 (SVG 좌표)
theta = 80 # Z축 기울임 각도 (°)
tilt = math.radians(theta)
# 투영된 타원 파라미터
rx = R # x축은 변하지 않음
ry = R * math.cos(tilt) # y축만 cos(θ)로 압축
# 원 위의 한 점 (각도 α)을 2D로 투영
def project(alpha_deg):
a = math.radians(alpha_deg)
x = cx + R * math.cos(a)
y = cy + R * math.sin(a) * math.cos(tilt)
return round(x), round(y)
```
### 10.3 N개 노드 배치
```python
N = 3 # 노드 개수 (3, 4, 5, 6 모두 가능)
start_angle = 270 # 상단부터 시작
# 기본 간격: 360°/N
base_gap = 360 / N # 3개→120°, 4개→90°, 5개→72°
# 사이각 축소 (2/3): 양끝이 너무 벌어지는 것 방지
gap = base_gap * 2 / 3
# 각 노드 각도 계산 (상단 노드 고정, 나머지가 원 위에서 이동)
angles = []
for i in range(N):
if i == 0:
angles.append(start_angle) # 상단 고정
else:
# 상단 기준 좌/우 대칭 배치
# 홀수 인덱스: 좌측 (반시계)
# 짝수 인덱스: 우측 (시계)
if i % 2 == 1: # 좌측
step = (i + 1) // 2
angles.append(start_angle - gap * step)
else: # 우측
step = i // 2
angles.append(start_angle + gap * step)
# 각 노드의 2D 좌표
for i, angle in enumerate(angles):
x, y = project(angle)
print(f'Node {i}: angle={angle:.0f}°, pos=({x}, {y})')
```
### 10.4 N별 계산 예시
| N | 기본 간격 | 축소 간격 (2/3) | 노드 각도 |
|---|---------|--------------|---------|
| 3 | 120° | 80° | 270°, 190°, 350° |
| 4 | 90° | 60° | 270°, 210°, 330°, 150° |
| 5 | 72° | 48° | 270°, 222°, 318°, 174°, 366° |
| 6 | 60° | 40° | 270°, 230°, 310°, 190°, 350°, 150° |
### 10.5 화살표 >> 위치 계산
화살표는 **두 노드 사이 호의 1/3, 2/3 지점**에 배치. 방향은 **접선 방향**.
```python
def arrow_positions(angle1, angle2):
"""두 노드 사이 호에 화살표 2개 배치"""
mid1 = angle1 + (angle2 - angle1) * 0.35
mid2 = angle1 + (angle2 - angle1) * 0.65
pos1 = project(mid1)
pos2 = project(mid2)
# 접선 방향 (화살표 회전각)
def tangent_angle(alpha):
a = math.radians(alpha)
tx = -math.sin(a)
ty = math.cos(a) * math.cos(tilt)
return math.degrees(math.atan2(ty, tx))
rot1 = tangent_angle(mid1)
rot2 = tangent_angle(mid2)
return (pos1, rot1), (pos2, rot2)
```
### 10.6 설명 텍스트 배치 규칙
**Figma 원본 분석 결과**: 설명은 원 바깥 방향(Q꼬리)이 아니라, **노드 이름 옆에 수평으로** 배치.
```
규칙:
원 중심 기준 좌측 노드 (angle > 90° and < 270°):
→ 설명이 노드 이름의 좌측에 (text-anchor: end)
원 중심 기준 우측 또는 상단 노드 (나머지):
→ 설명이 노드 이름의 우측에 (text-anchor: start)
텍스트 구조:
[설명 제목] ← 이름과 같은 Y선, 옆에 수평
[노드 아이콘 원] • 불릿 1 ← 제목 아래 들여쓰기
[라벨] (원 아래) • 불릿 2
[서브라벨] (라벨 아래)
```
**실제 배치 (3노드 예시)**:
```
👥 사람(역량) 혁신적 사고방식 ← 우측
• 창의적 문제 해결
• 사용자 중심 접근
Digital 기술과... 🖥 기술(디지털) 자연(여건) 📋 지속적 투자 의지 ← 우측
• 건설 전문 지식 ↑ • 실행 추진력
• 최신 기술 좌측 • 변화를 통한 가치 창출
```
```python
def desc_position(node_x, node_y, node_angle, cx):
"""설명 텍스트 위치 계산"""
if node_x < cx: # 왼쪽 노드
desc_x = node_x - 36 # 노드 좌측
anchor = 'end'
else: # 오른쪽 또는 상단 노드
desc_x = node_x + 36 # 노드 우측
anchor = 'start'
desc_y = node_y + 37 # 라벨과 같은 높이 (아이콘 아래)
return desc_x, desc_y, anchor
```
### 10.7 SVG 구조
```xml
<svg viewBox="0 0 1000 380">
<!-- 1. 타원 궤도 -->
<ellipse cx="500" cy="200" rx="400" ry="69"/>
<!-- 2. 화살표 >> (N개 구간 × 2개씩) -->
<text transform="rotate(각도)">»</text>
<!-- 3. 노드 (원 + 아이콘 + 라벨) -->
<circle cx="x" cy="y" r="26"/>
<text>아이콘</text>
<text>라벨</text>
<!-- 4. 설명 텍스트 -->
<text>설명 제목</text>
<text>• 불릿</text>
</svg>
```
### 10.8 중요: 파이프라인에서 좌표 계산
이 블록은 **Jinja2 템플릿에 좌표를 하드코딩하면 안 됨**.
파이프라인(Python)에서 N, R, θ를 받아 좌표를 계산한 뒤 템플릿에 전달해야 함.
```python
# pipeline에서 호출
def calculate_orbit(n_nodes, radius=400, tilt_deg=80):
"""N개 노드의 SVG 좌표와 화살표 위치를 계산"""
cx, cy = 500, 200
tilt = math.radians(tilt_deg)
gap = (360 / n_nodes) * 2 / 3
nodes = []
arrows = []
# ... 위 공식 적용
return {
'ellipse': {'cx': cx, 'cy': cy, 'rx': radius, 'ry': round(radius * math.cos(tilt))},
'nodes': nodes, # [{x, y, angle}, ...]
'arrows': arrows, # [{x, y, rotation}, ...]
}
```
---
## 11. 실수 방지 (Anti-patterns)
### 11.1 절대 하면 안 되는 것
| Anti-pattern | 왜 안 되는지 | 올바른 방법 |
|-------------|------------|-----------|
| px 시행착오 조정 ("좀 더 올려볼게") | 3번 이상 실패, 시간 낭비 | Figma 좌표에서 수학적 계산 |
| 3D 효과를 CSS로 재현 | 평면적이라 품질 차이 심각 | Figma에서 PNG 추출 |
| 비교 리뷰를 좌/우 배치 | 크기 차이로 비교 불가 | 위/아래 같은 폭으로 배치 |
| Jinja2 템플릿을 브라우저에서 직접 열기 | 변수 미렌더, 이미지 경로 깨짐 | comparison.html 또는 FastAPI로 확인 |
| 독립 flex-column으로 2열 비교 | 행 정렬 안 됨 | CSS Grid 행 공유 |
| 느낌으로 폰트/색상 설정 | Figma와 다른 결과물 | Figma API에서 정확한 값 추출 |
### 11.2 반드시 해야 하는 것
| 원칙 | 이유 |
|------|------|
| CSS 주석에 계산 근거 기록 | 나중에 왜 이 값인지 추적 가능 |
| 비교 리뷰 후 진행 | 디자인 차이를 사전에 발견 |
| 이미지 자산은 `static/figma-assets/`에 저장 | FastAPI가 서빙, 경로 일관성 |
| `comparison.html`에 모든 프레임 포함 | 한 페이지에서 전체 리뷰 가능 |
| Figma 노드 ID 기록 | 나중에 업데이트된 디자인 재추출 가능 |
---
## 12. Figma 소스 정보
### 현재 등록된 Figma 파일
| 항목 | 값 |
|------|---|
| File Key | `9S6LsQyO6zlRxtiqZccOUM` |
**Page 1 (0:1)** — 기본 디자인
| 블록 | Node ID | 설명 |
|------|---------|------|
| hero-icon-cards | `1:5` | Frame 2 (Solution 제작 목표) |
| compare-2col-badge | `1:35` | Frame 3 (정책 달성) |
| compare-detail-gradient | `1:49` | Frame 4 (과정 vs 결과 혁신) |
| Badge 빨간 리본 | `1:33` | image 4019 |
| Badge 틸 리본 | `1:43` | image 2197 |
| Arrow As-Is→To-Be | `1:67` | image 2645 |
| Box 빨간 테두리 | `1:12` | Rectangle 42894 |
| Box 틸 테두리 | `1:37` | Rectangle 42598 |
**Page 2 (15:2)** — 프레젠테이션 슬라이드
| 블록 | Node ID | 설명 |
|------|---------|------|
| category-strip-table | `17:1264` | 001_개요 우측 하단 (필수조건 3열) |
**Page 3 (18:8204)** — 컴포넌트
| 블록 | Node ID | 설명 |
|------|---------|------|
| checklist-dark | `18:8351` | f5 (체크리스트 6행) |
| system-2col-center | `18:8405` | f8 (System 구성 H/W vs S/W) |
**Page 4 (29:439)** — 순환 다이어그램
| 블록 | Node ID | 설명 |
|------|---------|------|
| cycle-orbit | `29:439` | DX 시행 필수 요건 (3노드 순환) |
---
## 13. 체크리스트
새 Figma 프레임을 블록으로 변환할 때:
- [ ] Figma API로 노드 데이터 추출 (좌표, 크기, 색상, 폰트)
- [ ] PNG 렌더링 다운로드 (scale=2)
- [ ] 복잡한 비주얼 요소 식별 → 이미지로 추출 (CSS로 만들지 않음)
- [ ] 스케일 팩터 계산 (1200 / frame_width)
- [ ] 핵심 정렬 포인트 수학적 계산 (좌표 차이 × 스케일)
- [ ] CSS 값 도출 (계산 근거를 주석으로 기록)
- [ ] 비교 리뷰 페이지에 추가 (위/아래 같은 폭)
- [ ] 사용자 피드백 확인
- [ ] Jinja2 템플릿 변환 (고정값→변수, 반복→루프)
- [ ] catalog.yaml 등록

View File

@@ -0,0 +1,97 @@
# Figma 도형 + 그라데이션 처리
## 핵심 원리
Figma에서 도형 작업 방식:
1. 박스(프레임/컨테이너) 안에 도형을 만든다 (border-radius + gradient)
2. **박스를 회전**시킨다 (도형 자체가 아니라)
3. 박스가 돌면 그 안의 도형도 같이 돌아간다 → border-radius와 gradient가 함께 회전
## CSS 구현 구조
**도형 자체에 transform 적용 금지. 반드시 래퍼(wrapper) 컨테이너에 적용한다.**
```html
<div class="wrapper"> <!-- 이게 회전한다 -->
<div class="shape"></div> <!-- 이건 건드리지 않는다 -->
</div>
```
```css
.wrapper {
position: absolute;
/* pre-rotation position/size */
transform: rotate(X deg); /* Figma gradient 각도의 부호 반대 */
}
.shape {
width: 100%; height: 100%;
border-radius: <Figma >;
background: linear-gradient(90deg, <Figma >);
/* CSS 90deg = Figma 0deg = 왼→오, 이것이 "기본 상태" */
}
```
## 프로세스
```
1. Figma gradient 각도 확인 (예: 90°)
2. 기본 상태(Figma 0°) 정의
- shape: border-radius = Figma 값, gradient = CSS 90deg (left→right) + Figma 색상
3. 래퍼의 pre-rotation 위치/크기 계산
- 90° 회전이면 width/height 교환
- 래퍼 중심 = 최종 중심 (Figma position + size/2)
4. 래퍼에 transform: rotate() 적용
- Figma +90° → CSS rotate(-90deg) (부호 반대, CSS는 CW 기준)
```
## 예시: 42335
Figma 데이터:
- 위치: (574, 45), 크기: 205×424 (tall, 최종 상태)
- border-radius: 102 0 0 102 (왼쪽 둥근, 기본 상태 기준)
- gradient: 90deg (시계 반대 방향 90도 회전됨)
Pre-rotation 계산:
- 최종 중심: (676.5, 257)
- Pre-rotation 크기: 424×205 (swap)
- Pre-rotation top-left: (464.5, 154.5)
CSS:
```html
<div class="wrapper-42335">
<div class="shape-42335"></div>
</div>
```
```css
.wrapper-42335 {
position: absolute;
left: 464.5px; top: 154.5px;
width: 424px; height: 205px;
transform: rotate(-90deg);
}
.shape-42335 {
width: 100%; height: 100%;
border-radius: 102px 0 0 102px;
background: linear-gradient(90deg, rgba(217,162,104,1) 37%, rgba(220,103,14,0) 89%);
}
```
## Figma gradient 각도 → CSS transform 변환
Figma는 시계방향이 양수, CSS transform은 시계방향이 양수이지만:
- Figma gradient 각도는 **도형 내부 방향** 기준
- 박스를 회전시키는 관점에서는 부호가 반대
```
Figma gradient 0° → CSS transform: rotate(0deg) (회전 없음)
Figma gradient +90° → CSS transform: rotate(-90deg) (반시계)
Figma gradient -90° → CSS transform: rotate(+90deg) (시계)
Figma gradient ±180° → CSS transform: rotate(180deg)
```
## 주의사항
- Figma 데이터가 유일한 소스. PNG는 픽셀 분석으로만 교차 검증.
- 이미지를 눈으로 보고 방향 판단 금지 (멀티모달 해석 불안정).
- border-radius와 gradient를 각각 수동 계산하지 않는다. **래퍼를 회전**시킨다.
- 작동하는 값은 건드리지 않는다. 사용자가 지적한 것만 수정한다.

362
figma_to_html_agent/MATH.md Normal file
View File

@@ -0,0 +1,362 @@
# 수학 공식 레퍼런스
Figma → HTML 변환에서 사용하는 모든 수학 공식. 이 문서의 공식만 사용하고, 직관/감으로 보정하지 않는다.
---
## §1. 스케일 팩터
### 정의
```
S = 1280 / W_원본_프레임
```
`1280`은 16:9 슬라이드 가로 폭. 모든 프레임은 가로 1280에 맞춰 축소된다.
### 적용 방법: CSS transform scale (권장)
```html
<div class="block">
<div class="inner"> <!-- 원본 W × H 좌표계 그대로 -->
... 모든 요소 (Figma 원본 px) ...
</div>
</div>
```
```css
.block {
width: 1280px;
height: {H × S}px;
overflow: hidden;
position: relative;
}
.inner {
position: absolute;
left: 0; top: 0;
width: {W}px; /* 원본 그대로 */
height: {H}px;
transform: scale({S});
transform-origin: top left;
}
```
**왜 transform이 좋은가:**
- 위치/크기/폰트/그림자/스트로크/blur radius 모두 한 번에 균일 축소
- 매 값 수동 곱셈하면 누적 오차 + 검증 어려움
- transform은 GPU 가속, 계산 정확
### 적용 대상
| 적용 | 미적용 |
|------|------|
| 위치 (x, y) | 색상 |
| 크기 (width, height) | 그라데이션 방향 (각도 그대로) |
| 폰트 크기 | 그라데이션 stop 퍼센트 (그대로) |
| 스트로크 너비 | 폰트 굵기 |
| 간격 (gap, padding) | line-height 비율 (1.5 등) |
| 그림자 (blur, offset) | border-radius 비율 (50% 등) |
| border-radius (px) | |
---
## §2. SVG `<linearGradient>` → CSS `linear-gradient()`
### 입력
SVG에서:
```xml
<linearGradient id="..." gradientUnits="userSpaceOnUse"
x1="..." y1="..." x2="..." y2="...">
<stop offset="0" stop-color="..."/>
<stop offset="1" stop-color="..."/>
</linearGradient>
```
### 변환 공식
```
1. dx = x2 - x1
dy = y2 - y1
L_svg = √(dx² + dy²)
2. SVG 벡터 각도 (y-down 좌표계, 0°=오른쪽, +CW):
svg_angle = atan2(dy, dx) (단위: 라디안)
3. CSS 각도 (12시 방향=0°, +CW):
css_angle = degrees(svg_angle) + 90
css_angle = css_angle mod 360
4. CSS 그라데이션 선 길이 (W×H 박스 안):
α = radians(css_angle)
L_css = |W × sin(α)| + |H × cos(α)|
5. 박스 중심의 t 파라미터 (SVG 벡터 위, 0=시작, 1=끝):
t_center = ((W/2 - x1)·dx + (H/2 - y1)·dy) / L_svg²
6. CSS 0% / 100%가 SVG t-space의 어디에 매핑되는지:
half = (L_css / 2) / L_svg
t0 = t_center - half ← CSS 0%
t1 = t_center + half ← CSS 100%
7. SVG 각 stop offset (0~1)을 CSS percent로:
pct = (offset - t0) / (t1 - t0) × 100
```
### 예시
SVG:
```xml
<linearGradient x1="110.833" y1="18.2292" x2="219.479" y2="175"
gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FDC69E"/>
<stop offset="1" stop-color="#E0782C"/>
</linearGradient>
```
박스 W=H=350일 때:
```
dx = 108.65, dy = 156.77
L_svg = √(108.65² + 156.77²) = 190.74
svg_angle = atan2(156.77, 108.65) = 0.9646 rad = 55.27°
css_angle = 55.27 + 90 = 145.27°
α = 2.535 rad
L_css = 350 × |sin 145.27°| + 350 × |cos 145.27°|
= 350 × 0.5696 + 350 × 0.8220
= 487.06
t_center = ((175 - 110.833)·108.65 + (175 - 18.229)·156.77) / 190.74²
= (6971.7 + 24577.3) / 36382
= 0.8672
half = (487.06 / 2) / 190.74 = 1.2767
t0 = 0.8672 - 1.2767 = -0.4095
t1 = 0.8672 + 1.2767 = 2.1439
SVG offset 0 → pct = (0 - (-0.4095)) / 2.5534 × 100 = 16.04%
SVG offset 1 → pct = (1 - (-0.4095)) / 2.5534 × 100 = 55.20%
```
CSS:
```css
background: linear-gradient(145.27deg, #FDC69E 16.04%, #E0782C 55.20%);
```
### 코드: `scripts/gradient_math.py`
```python
from scripts.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')])
# → "linear-gradient(145.27deg, #FDC69E 16.04%, #E0782C 55.20%)"
```
---
## §3. 회전 감지 (bbox 비율 검사)
Figma MCP는 `rotation` 속성을 출력하지 않으므로 bbox 비율로 추론:
```
단일 문자 텍스트:
width > height × 1.5 → 90° 회전 (가로로 누움)
일반 텍스트:
width < fontSize × 0.8 → 좁은 박스 세로 배치 (writing-mode 아님, <br>로 줄바꿈)
```
CSS 적용:
```css
.rotated {
transform: rotate(90deg); /* 또는 -90deg */
}
```
---
## §4. Descender 보정 (padding-bottom)
CSS `line-height: 1`이거나 `< font_content_area_ratio`이면 글리프 하강부(g, y, p, 쉼표)가 잘림.
### 폰트별 메트릭
| 폰트 | UPM | typoAscender | typoDescender | content_area_ratio |
|------|-----|------|------|------|
| Noto Sans KR | 1000 | 1160 | 288 | 1.448 |
| Pretendard | 1000 | 1100 | 300 | 1.400 |
### 공식
```
content_area_ratio = (typoAscender + |typoDescender|) / UPM
half_leading = (line_height_ratio - content_area_ratio) / 2
↑ 음수면 잘림 발생
clipped_px = |half_leading| × font_size
padding-bottom = ceil(clipped_px)
```
### 예시 (Noto Sans KR, font 27.1px, lh 1)
```
half_leading = (1.0 - 1.448) / 2 = -0.224
clipped = 0.224 × 27.1 = 6.07 px
→ padding-bottom: 7px
```
### 예시 (Noto Sans KR, font 30px, lh 35px → ratio 1.167)
```
half_leading = (1.167 - 1.448) / 2 = -0.1405
clipped = 0.1405 × 30 = 4.215 px
→ padding-bottom: 5px
```
---
## §5. SVG viewBox padding → CSS box-sizing 매핑
SVG가 drop-shadow blur 여백을 위해 viewBox를 확장해놓은 경우 (예: 280×280 fill을 310 viewBox에 넣음):
### 케이스 A — Stroke가 fill 외부 (안전과 품질 ring 같은 케이스)
```
SVG: viewBox 310, fill r=140 (d=280), stroke r=142.5 width=5 (extends r=140 to 145)
visible: 290×290 (fill 280 + 5px stroke 외부 확장)
viewBox padding: 310 - 290 = 20 (각 변 10이 drop-shadow blur 패딩, 추가 5는 stroke)
CSS:
div W=H=290
border: 5px solid white
box-sizing: border-box
→ border-box 290, padding-box 280 ← fill 영역
position: Figma fill 위치 - (5, 5) ← stroke 외부 확장 보정
```
### 케이스 B — Stroke가 fill 내부 (생산성/소통 ring 같은 케이스)
```
SVG: viewBox 300, fill r=140, stroke r=137.5 width=5 (extends r=135 to 140 — fill 외곽 5px overlap)
visible: 280×280 (stroke가 fill 외곽 5px를 덮음)
viewBox padding: 300 - 280 = 20 (전부 drop-shadow blur)
CSS:
div W=H=280
border: 5px solid white
box-sizing: border-box
background-origin: border-box ← gradient를 border-box 280에 매핑
background-clip: border-box
→ border 5가 외곽 fill을 덮어 그라데이션 가시 영역은 270
position: Figma 위치 그대로 (offset 없음)
```
### 그라데이션 좌표 remap
SVG `<linearGradient>` 좌표는 viewBox 공간 기준. CSS box로 매핑할 때:
```
viewBox padding이 P (예: 15 또는 10)이라면:
CSS_x = SVG_x - P
CSS_y = SVG_y - P
```
이렇게 보정한 좌표를 §2의 svg_to_css 공식에 W=H=fill_size로 넣는다.
---
## §6. Drop shadow: SVG `feGaussianBlur` ↔ CSS `box-shadow`
SVG:
```xml
<filter>
<feGaussianBlur stdDeviation="5"/>
<feColorMatrix .../>
</filter>
```
CSS 근사:
```css
box-shadow: 0 0 {2 × stdDeviation}px {color};
```
`stdDeviation=5` → CSS `box-shadow: 0 0 10px black`
**주의:** 정확한 픽셀 일치는 아님. 시각적으로 매우 유사하지만 SVG 가우시안과 CSS 블러 알고리즘이 다름. ±2px 차이는 허용.
---
## §7. Blend mode 호환
### Figma가 사용하는 blend mode → CSS 호환 매핑
| Figma | CSS 정확 | CSS 호환 (Chrome/Firefox) | 비고 |
|-------|---------|----------------------|------|
| Normal | normal | normal | 기본 |
| Multiply | multiply | multiply | OK |
| **Plus darker** | plus-darker | **multiply** | plus-darker는 Safari 전용 |
| Darken | darken | darken | OK |
| Screen | screen | screen | OK |
| Overlay | overlay | overlay | OK |
### Plus-darker vs Multiply 차이
```
plus-darker(src, dst) = max(0, src + dst - 1)
multiply(src, dst) = src × dst
```
- 흰 배경: 둘 다 동일 (효과 없음)
- 어두운 배경: multiply가 plus-darker보다 강하게 어두워짐
- 밝은 그라데이션 + 흰 배경 조합: 시각적 차이 거의 없음 (이 프로젝트 디자인 대부분 해당)
**Chrome/Firefox 호환 위해 multiply로 통일.** RULES.md R10 참조.
---
## §8. CSS `border-radius` 비율 변환
Figma `cornerRadius`는 px 단위. CSS도 px 단위 그대로 사용 + scale 적용.
특수 케이스:
- 완전 원: `border-radius: 50%`
- 캡슐: `border-radius: {height/2}px`
- 한쪽만 둥근 사각: `border-radius: {tl} {tr} {br} {bl}` (개별 4값)
스케일링 시: scale transform이 자동으로 px 값을 비율 유지하며 축소함. 별도 계산 불필요.
---
## §9. 글자 수 추정 (블록 안에 들어갈 텍스트 양)
블록 너비/높이에서 들어갈 수 있는 한글 글자 수를 미리 계산:
```
한 줄 글자 수 = 블록 너비(px) / (font_size × 한글_글자_너비_계수)
줄 수 = 블록 높이(px) / (font_size × line_height_ratio)
총 글자 수 = 한 줄 × 줄 수 × 안전계수(0.85)
```
### Pretendard / Noto Sans KR 한글 글자 너비 계수 = 0.97
| font-size | 한글 글자 너비 | line_height 1.6 줄 높이 |
|-----------|-------------|---------------------|
| 12px | 11.6px | 19.2px |
| 16px | 15.5px | 25.6px |
| 20px | 19.4px | 32.0px |
| 24px | 23.3px | 38.4px |
| 30px | 29.1px | 48.0px |
이는 design_agent 텍스트 편집 단계에서 사용. 변환 단계에서는 직접 사용하지 않음.
---
## 검증 체크리스트
변환 후 매번 확인:
- [ ] §1 스케일 — `transform: scale(S)` 한 번만 사용했는가, 매 값 수동 곱셈은 없는가
- [ ] §2 그라데이션 — gradient_math.py로 도출한 값을 그대로 사용했는가, 눈대중 각도/stop은 없는가
- [ ] §3 회전 — bbox 비율로 회전 감지했는가
- [ ] §4 descender — `line_height < content_area_ratio`인 텍스트에 padding-bottom 추가했는가
- [ ] §5 viewBox — stroke 정렬 확인 (외부/내부)에 따라 box-sizing 적용했는가
- [ ] §6 shadow — `box-shadow blur = 2 × stdDeviation`인가
- [ ] §7 blend — `plus-darker``multiply`로 교체했는가

View File

@@ -0,0 +1,478 @@
# Phase 2: Figma Block Design Specification
> 7개 블록 + 2개 서브 컴포넌트 상세 설계
> 기준: FIGMA-DESIGN-LANGUAGE.md 분석 결과
> 최종 업데이트: 2026-04-08
---
## Block 1: `hero-icon-cards`
### 1.1 시각적 구조
```
┌──────────────────────────────────────────────┐
│ [Hero Statement - 큰 텍스트, 중앙] │ ← zone: header or full-width
│ │
│ ┌─[Badge Title]─┐ │
│──────────┤ ├───────────────────│
│ ┌─────┐ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │icon │ │ │icon │ │icon │ │icon │ │icon │ │ ← N개 카드 (2~6)
│ │ │ │ │ │ │ │ │ │ │ │ │
│ │Title│ ╎ │Title│ │Title│ │Title│ │Title│ │ ← 세로 구분선
│ │(sub)│ │ │(sub)│ │(sub)│ │(sub)│ │(sub)│ │
│ └─────┘ │ └─────┘ └─────┘ └─────┘ └─────┘ │
│ └───────────────────────────────────│
└──────────────────────────────────────────────┘
```
### 1.2 슬롯 정의
| 슬롯 | 필수 | 타입 | 설명 |
|------|------|------|------|
| `statement` | O | string | Hero 메시지 (1-2줄) |
| `badge_title` | X | string | 배지 바 텍스트 |
| `cards[]` | O | array | 카드 배열 |
| `cards[].icon` | X | string | 아이콘 이미지 URL 또는 이모지 |
| `cards[].title` | O | string | 영문 또는 주제목 |
| `cards[].subtitle` | X | string | 한국어 부제 |
| `cards[].color` | X | string | 개별 카드 강조색 |
### 1.3 동적 재구성 규칙
#### 그리드 계산
```
입력: N = cards.length, W = container_width_px
N ≤ 5: 1행 N열
col_count = N
card_width = (W - padding*2 - gap*(N-1)) / N
N = 6: 1행 6열 (gap 축소)
col_count = 6
gap = 8px (기본 16px에서 축소)
N > 6: 2행
col_count = ceil(N / 2)
row_count = 2
```
#### 폰트 스케일링
```
card_width ≥ 200px → title: 20px, subtitle: 14px
card_width ≥ 150px → title: 16px, subtitle: 12px
card_width < 150px → title: 14px, subtitle: 11px
```
#### 높이 계산
```
hero_height = statement_lines * line_height + padding
badge_height = 44px (고정)
card_area_height = icon_height + title_lines * title_lh + subtitle_lh + padding
- 1행: card_area_height
- 2행: card_area_height * 2 + gap
total_min_height = hero_height + badge_height + card_area_height + gaps
```
### 1.4 catalog.yaml schema
```yaml
- id: hero-icon-cards
name: 히어로 문구 + 아이콘 카드
category: cards
template: blocks/cards/hero-icon-cards.html
height_cost: xlarge
min_height_px: 280
relation_types: [definition, flow]
min_items: 2
max_items: 6
visual: >
상단에 큰 Hero 메시지(24px bold, 중앙) + 배지 바 +
하단에 N열 아이콘 카드(둥근 흰색 컨테이너, 세로 구분선).
각 카드는 아이콘 이미지 + 영문 제목(20px/900) + 한국어 부제(14px/500).
when: >
핵심 목표나 가치를 N개 키워드로 선언할 때.
각 키워드에 아이콘이나 이미지가 있을 때.
"우리가 추구하는 5가지 가치" 같은 구조.
not_for: >
비교/대조 구조 → compare-2col-badge.
상세 설명이 길 때 → card-icon-desc.
순서/단계 → card-step-vertical 또는 process-horizontal.
purpose_fit: [핵심전달, 가치선언]
zone: full-width-only
slots:
required: [statement, cards[]]
optional: [badge_title, cards[].icon, cards[].subtitle, cards[].color]
schema:
statement:
max_lines: 2
font_size: 24
ref_chars:
body: 60
note: "24px bold, 중앙정렬, 흰색 스트로크"
badge_title:
max_lines: 1
font_size: 18
ref_chars:
body: 20
note: "18px bold white, 배지 바 위"
card_title:
max_lines: 2
font_size: 20
ref_chars:
body: 15
note: "20px black/900, 중앙정렬"
card_subtitle:
max_lines: 1
font_size: 14
ref_chars:
body: 10
note: "14px medium, 한국어 부제"
padding_overhead_px: 60
padding_h_px: 32
```
---
## Block 2: `compare-2col-badge`
### 2.1 시각적 구조
```
┌──────────────────────────────────────────────┐
│ ┌─[Badge Title]─┐ │
│────────────┤ ├─────────────────│
│ │
│ ┌── Left Column ──┐ ╎ ┌── Right Column ──┐ │
│ │ │ ╎ │ │ │
│ │ [Big Title] │ ╎ │ [Big Title] │ │
│ │ │ ╎ │ │ │
│ │ body text... │ ╎ │ body text... │ │
│ │ body text... │ ╎ │ body text... │ │
│ │ │ ╎ │ │ │
│ └──────────────────┘ ╎ └──────────────────┘ │
│ │
│ [Optional: Hero Statement] │
└──────────────────────────────────────────────┘
```
### 2.2 슬롯 정의
| 슬롯 | 필수 | 타입 | 설명 |
|------|------|------|------|
| `badge_title` | O | string | 배지 바 텍스트 |
| `left_title` | O | string | 좌측 열 대제목 |
| `left_body` | O | string | 좌측 열 본문 |
| `right_title` | O | string | 우측 열 대제목 |
| `right_body` | O | string | 우측 열 본문 |
| `statement` | X | string | 하단 Hero 메시지 |
| `left_color` | X | string | 좌측 강조색 (기본: --color-teal) |
| `right_color` | X | string | 우측 강조색 (기본: --color-teal) |
### 2.3 동적 재구성 규칙
#### 레이아웃 계산
```
container_width = 컨테이너 전체 폭
padding_h = 32px * 2
2열 모드 (기본):
col_width = (container_width - padding_h - divider_gap) / 2
divider_gap = 32px
1열 모드 (sidebar zone, 폭 < 500px):
좌/우가 세로 스택
col_width = container_width - padding_h
```
#### 높이 계산
```
badge_height = 44px
left_height = title_height + body_lines * line_height + padding
right_height = title_height + body_lines * line_height + padding
content_height = max(left_height, right_height)
statement_height = statement ? (statement_lines * 28 + 16) : 0
total = badge_height + content_height + statement_height + gaps
```
#### 텍스트 피팅
```
col_width에 따른 body 글자수 제한:
col_width ≥ 500px → ~40자/줄, font: 16px
col_width ≥ 350px → ~28자/줄, font: 14px
col_width < 350px → ~20자/줄, font: 13px
```
### 2.4 catalog.yaml schema
```yaml
- id: compare-2col-badge
name: 배지 헤더 2열 비교
category: cards
template: blocks/cards/compare-2col-badge.html
height_cost: large
min_height_px: 200
relation_types: [comparison, contrast]
visual: >
상단 배지 바(이미지/그라디언트 배경 + 흰색 텍스트) 아래
2열 비교 레이아웃. 좌/우 각각 대제목(24px/900) + 본문(16px/700).
중앙 세로 구분선. 둥근 흰색 컨테이너(r:20).
선택적 하단 Hero 메시지.
when: >
두 개념/방법/전략을 나란히 비교할 때.
배지 헤더로 상위 주제를 명시.
예: "Engn. Solution vs DfMA", "현재 vs 미래"
not_for: >
3개 이상 항목 비교 → compare-3col-badge.
장/단점 목록 → comparison-2col.
상세 내용이 길고 섹션이 많을 때 → compare-detail-gradient.
purpose_fit: [비교대조, 개념정의]
zone: full-width-only
slots:
required: [badge_title, left_title, left_body, right_title, right_body]
optional: [statement, left_color, right_color]
schema:
badge_title:
max_lines: 1
font_size: 18
ref_chars:
body: 15
note: "18px bold white, 배지 바"
left_title:
max_lines: 1
font_size: 24
ref_chars:
body: 15
note: "24px black/900, 흰색 스트로크"
left_body:
max_lines: 6
font_size: 16
ref_chars:
body: 200
note: "16px/700, 틸 색상"
right_title:
max_lines: 1
font_size: 24
ref_chars:
body: 15
note: "24px black/900, 흰색 스트로크"
right_body:
max_lines: 6
font_size: 16
ref_chars:
body: 200
note: "16px/700, 틸 색상"
statement:
max_lines: 2
font_size: 20
ref_chars:
body: 50
note: "20px bold, 중앙정렬"
padding_overhead_px: 56
padding_h_px: 32
```
---
## Block 3: `compare-detail-gradient`
### 3.1 시각적 구조
```
┌──────────────────────────────────────────────────────────┐
│ ┌───── Left Header Bar (gradient) ─────┐┌── Right ─────┐│
│ │ [Left Column Title] ││ [Right Title] ││
│ └──────────────────────────────────────┘└───────────────┘│
│ ┌─────── Left BG (warm) ──────┐┌──── Right BG (teal) ──┐│
│ │ ││ ││
│ │ [Section 1 Title] ││ [Section 1 Title] ││
│ │ • body text ││ • body text ││
│ │ • body text ││ • body text ││
│ │ ││ ││
│ │ [Section 2 Title] ││ [Section 2 Title] ││
│ │ • body text ││ • body text ││
│ │ • body text ││ • body text ││
│ │ ││ ││
│ │ [Section N Title] ││ [Section M Title] ││
│ │ • body text ││ • body text ││
│ └──────────────────────────────┘└───────────────────────┘│
└──────────────────────────────────────────────────────────┘
```
### 3.2 슬롯 정의
| 슬롯 | 필수 | 타입 | 설명 |
|------|------|------|------|
| `left_header` | O | string | 좌측 열 헤더 타이틀 |
| `right_header` | O | string | 우측 열 헤더 타이틀 |
| `left_sections[]` | O | array | 좌측 섹션 배열 |
| `left_sections[].title` | O | string | 섹션 소제목 |
| `left_sections[].body` | O | string | 섹션 본문 (줄바꿈 허용) |
| `right_sections[]` | O | array | 우측 섹션 배열 |
| `right_sections[].title` | O | string | 섹션 소제목 |
| `right_sections[].body` | O | string | 섹션 본문 |
| `left_color_theme` | X | string | 좌측 테마 (기본: warm) |
| `right_color_theme` | X | string | 우측 테마 (기본: teal) |
### 3.3 동적 재구성 규칙 (★ 가장 수학적으로 복잡)
#### 그리드 계산
```
container_width에서 2열 분할:
col_width = (container_width - gap) / 2
gap = 0px (그라디언트가 맞닿음)
```
#### 섹션 높이 계산 (핵심)
```
header_bar_height = 48px (고정)
각 섹션의 높이:
section_height(s) =
title_height(s.title, title_font_size, col_width) +
body_height(s.body, body_font_size, col_width) +
section_padding
title_height = ceil(char_count / chars_per_line) * title_line_height
body_height = line_count * body_line_height
chars_per_line = floor(col_width / (font_size * 0.55)) // 한글 평균 0.55em
좌측 전체:
left_total = header_bar + sum(section_height for s in left_sections) + gaps
우측 전체:
right_total = header_bar + sum(section_height for s in right_sections) + gaps
content_height = max(left_total, right_total)
```
#### 오버플로 방지 — Fit 검증
```
if content_height > container_available_height:
전략 1: 폰트 축소
body_font_size -= 1px (최소 12px)
재계산
전략 2: 섹션 본문 줄 수 제한
max_body_lines = floor(
(available_per_section - title_height) / body_line_height
)
available_per_section = (container_height - header*2 - gaps) / max(N_left, N_right)
전략 3: Kei 에스컬레이션 (기존 파이프라인)
content 요약 요청
```
#### 색상 테마 매핑
```
warm (좌측 기본):
header_gradient: rgba(165,161,150,0.10) → rgba(57,50,30,1.00)
section_title_color: var(--color-warm-brown)
bg: rgba(255,255,255,0.30) → rgba(57,50,30,0.30)
teal (우측 기본):
header_gradient: rgba(41,107,85,0.10) → rgba(3,33,24,1.00)
section_title_color: var(--color-dark-teal)
bg: rgba(41,107,85,0.30) → rgba(255,255,255,0.30)
```
### 3.4 catalog.yaml schema
```yaml
- id: compare-detail-gradient
name: 그라디언트 상세 2열 비교
category: cards
template: blocks/cards/compare-detail-gradient.html
height_cost: xlarge
min_height_px: 300
relation_types: [comparison, contrast, process]
min_items: 2 # 좌/우 최소 1섹션씩
max_items: 10 # 좌+우 합계
visual: >
좌우 그라디언트 배경(워 브라운 vs 다크틸)으로 나뉜 2열 비교.
각 열 상단에 그라디언트 헤더 바 + 큰 제목(28px/900).
하단에 N개 섹션(소제목 22px/900 + 본문 16px/700) 반복.
좌측은 따뜻한 톤(과정/As-Is), 우측은 차가운 톤(결과/To-Be).
when: >
두 카테고리를 상세하게 비교할 때.
각 카테고리에 여러 하위 항목이 있을 때.
과정 vs 결과, As-Is vs To-Be, 문제 vs 해결 구조.
not_for: >
간단한 2항목 비교(본문 짧을 때) → compare-2col-badge.
3열 비교 → compare-3col-badge.
비교가 아닌 단독 리스트 → dark-bullet-list.
purpose_fit: [비교대조, 구조시각화, 근거사례]
zone: full-width-only
slots:
required: [left_header, right_header, left_sections[], right_sections[]]
optional: [left_color_theme, right_color_theme]
schema:
left_header:
max_lines: 1
font_size: 28
ref_chars:
body: 20
note: "28px black/900, 그라디언트 바 위"
right_header:
max_lines: 1
font_size: 28
ref_chars:
body: 20
note: "28px black/900, 그라디언트 바 위"
section_title:
max_lines: 2
font_size: 22
ref_chars:
body: 30
note: "22px/900, 색상 테마별 (브라운 or 틸)"
section_body:
max_lines: 4
font_size: 16
ref_chars:
body: 120
note: "16px/700, black"
padding_overhead_px: 48
padding_h_px: 0
```
---
## 서브 컴포넌트
### S1. 장식 이미지 (3D 화살표 등)
- 블록이 아닌 **콘텐츠 이미지**로 처리
- `cards[].icon` 또는 별도 `decoration_image` 슬롯으로 전달
- 블록은 `<img>` 태그로 렌더링, 크기는 CSS로 컨테이너에 맞춤
### S2. CTA 버튼
- 독립 블록이 아닌 **다른 블록 내 선택적 요소**
- `cta_text` 슬롯으로 전달 (없으면 미표시)
- CSS: 그라디언트 바 + 둥근 버튼 (r:7)
---
## 구현 결과 (전체 7개 블록)
| # | 블록 | 카테고리 | 출처 | 상태 | 핵심 수학 |
|---|------|---------|------|------|----------|
| 1 | `hero-icon-cards` | cards | Page 1/Frame 2 | ✅ 완료 | 3D 리본 fold_offset 계산 (badge_y→box_y×scale) |
| 2 | `compare-2col-badge` | cards | Page 1/Frame 3 | ✅ 완료 | 3D 리본 fold_offset + 틸 테두리 |
| 3 | `compare-detail-gradient` | cards | Page 1/Frame 4 | ✅ 완료 | CSS Grid 행 공유 + As-Is/To-Be + 연속 그라디언트 |
| 4 | `category-strip-table` | cards | Page 2/001_개요 | ✅ 완료 | scale=1200/2123, N열 동적 Grid |
| 5 | `checklist-dark` | emphasis | Page 3/f5 | ✅ 완료 | scale=1200/1770, 행 간격 계산 |
| 6 | `system-2col-center` | cards | Page 3/f8 | ✅ 완료 | scale=1200/2446, 3열 Grid |
| 7 | `cycle-orbit` | visuals | Page 4/Frame 1 | ✅ 완료 | **3D 원 Z축 기울임(80°) → 2D 투영**, 사이각 축소, 접선 회전 |
### 핵심 교훈
1. **수학적 계산 필수**: Figma 좌표 → 스케일 → CSS값. 시행착오 금지.
2. **3D 리본은 이미지 추출**: CSS로 재현 불가, Figma에서 PNG 추출.
3. **CSS Grid 행 공유**: 좌/우 섹션 Y선 정렬 문제 해결.
4. **연속 그라디언트**: 셀별 배경 → Grid 전체 배경으로 끊김 방지.
5. **3D 원 투영**: `project(α) = (cx+R×cos(α), cy+R×sin(α)×cos(θ))` — N개 노드 자동 배치.
6. **텍스트 배치**: 좌측 노드→이름 좌측에, 우측/상단 노드→이름 우측에.
7. **비교 리뷰 필수**: Figma PNG vs HTML을 같은 폭으로 위/아래 비교.

View File

@@ -0,0 +1,98 @@
# Figma → HTML 변환 파이프라인
> 핵심 운영 절차는 [PROCESS.md](PROCESS.md), 수학 공식은 [MATH.md](MATH.md), CSS 보정 규칙은 [RULES.md](RULES.md), 변환 완료 목록은 [blocks_index.md](blocks_index.md).
## 현재 방향 (2026-04 확정)
**프로세스 우선, 인벤토리 후순위.** 35개 프레임을 사전 분류하지 않고, 1세션 1프레임씩 변환하면서 패턴이 발견되면 그때 템플릿화한다.
### 폐기된 접근
| 단계 | 폐기 사유 |
|------|---------|
| ~~Stage 1: Figma 인벤토리 일괄 추출~~ | work-creating-work. 35개는 사람이 5분이면 훑음 |
| ~~Stage 2: 노드 수 기반 지문~~ | leaf 카운트는 약한 시그널. 패턴 분류에 부정확 |
| ~~Stage 3: 자동 군집~~ | 약한 지문으로 자동 군집 시 잘못 묶임. 사람 눈이 빠름 |
**대체:** 매 변환 직후 [blocks_index.md](blocks_index.md)에 1줄 메모. 패턴은 bottom-up으로 발견된다.
## 활성 단계
```
[루프, 1세션 1프레임]
A. 1:1 변환 ← PROCESS.md 10단계 실행
B. 변형 축 메모 ← flat.md에 1~5줄 작성
C. blocks_index.md 1줄 추가
D. 패턴 2번째 등장? → 템플릿화 (Jinja2 + catalog.yaml 등록)
design_agent/templates/blocks/{category}/
[다음 프레임은 새 세션에서]
```
## 현황 (2026-04-10)
| 항목 | 상태 |
|------|------|
| 핵심 문서 (CLAUDE/PROCESS/MATH/RULES/PROCESS-CONTROL) | ✅ 정리 완료 |
| 재사용 스크립트 (scripts/gradient_math.py) | ✅ 자체 회귀 테스트 통과 |
| 변환 완료 블록 (정적 HTML) | 2 / N |
| 템플릿화 (Jinja2) | 0 |
| catalog.yaml 등록 (새 패턴) | 0 |
| design_agent 본체 통합 | 0 |
### 변환 완료 블록
| # | slug | frame | pattern | 상태 |
|---|------|-------|---------|------|
| 1 | prerequisites-3col | 45:15 | 3-column-comparison | static (이전 작업) |
| 2 | bim-goals-3circles | 66:310 | cycle-3way-intersect | static (Pure CSS, 검증 ✓) |
자세한 내역: [blocks_index.md](blocks_index.md)
## 대상 Figma 파일
- 파일키: `9S6LsQyO6zlRxtiqZccOUM` ("Untitled")
- 페이지: Page 2
- 추정 프레임 수: ~35개 (확정 안 됨, 사용자가 매번 선택)
## 다음 액션
1. 사용자가 Figma desktop에서 다음 변환할 프레임 **선택**
2. 에이전트가 PROCESS.md의 10단계 그대로 실행
3. 변환 후 blocks_index.md 업데이트
4. 다음 프레임은 **새 세션에서**
## 학습된 규칙 (이 프로젝트에서 발견)
> [RULES.md](RULES.md) R1~R12에 정리됨
1. Figma MCP는 rotation 미제공 → bbox 비율로 감지 (R2)
2. CSS line-height:1 → descender 잘림 → padding-bottom 보정 (R1)
3. Figma 세로 텍스트 = 좁은 박스 + 가로 텍스트 → HTML `<br>` 방식 (R3)
4. 흰 텍스트 stroke → HTML에서 비주얼 안 좋음 → 제거
5. 미리보기 배경은 항상 흰색 (R7)
6. 다중 fills → 최상단만 사용 (R5)
7. 동일 좌표 중복 노드 → 1개만 렌더링 (R6)
8. **순수 CSS 우선, SVG는 곡선/필터에만** (R9, 1171281211 변환에서 확정)
9. **plus-darker → multiply 교체** (R10, Safari 외 호환)
10. **Stroke inside/outside 구분** → box-sizing 결정 (R11)
11. **viewBox padding 그라데이션 좌표 remap** 필수 (R12)
12. **Vector 노드 metadata bbox는 회전된 좌표** → React wrapper 좌표 신뢰
## 블록 라이브러리 통합 경로 (Stage E~G, 미래)
```
figma_to_html_agent/templates/ ← Jinja2 템플릿 임시 저장소
design_agent/templates/blocks/{category}/{pattern_id}.html.j2
design_agent/templates/catalog.yaml ← when/slots/min_size_px 등록
design_agent Phase Q 블록 선택 단계와 자동 연결
사용자 콘텐츠 → 존 크기 → 패턴 매칭 → 블록 선택 → 슬롯 채우기 → 렌더
```

View File

@@ -0,0 +1,66 @@
# Figma → HTML 프로세스 제어
## 변경 전 반드시 확인
### 1. 소스는 Figma 데이터다
- gradient 방향: Figma 데이터의 각도에서 CSS 변환 (CSS = 90 - Figma)
- border-radius: Figma 데이터 그대로 (스케일만)
- PNG를 보고 방향을 판단하지 않는다
- PNG는 픽셀 데이터 분석으로만 교차 검증에 사용
### 2. 이미지 해석 금지
- 멀티모달 이미지 해석으로 gradient 방향 판단 불가 (미묘한 alpha에서 틀림)
- 방향 확인이 필요하면 픽셀 데이터를 숫자로 분석
- "보니까 ~인 것 같다" 금지. 데이터로 확인
### 3. 작동하는 것은 건드리지 않는다
- 사용자가 A만 문제라고 하면 A만 수정
- B, C가 "같은 이유로 틀릴 것 같다"고 추측해서 함께 바꾸지 않는다
- 변경 전: 현재 값이 뭔지 기록
- 변경 후: 변경한 값이 뭔지 기록
- 되돌려야 할 때 정확히 어디로 돌아가는지 알아야 한다
### 4. 한 번에 하나만 바꾼다
- gradient 각도와 border-radius를 동시에 바꾸지 않는다
- 하나 바꾸고 확인, 맞으면 다음 하나
### 5. 사용자가 말한 것만 한다
- 사용자의 피드백을 자의적으로 해석하지 않는다
- "주황색 gradient가 안 맞다" → 주황색 gradient만 수정
- 초록, 다른 요소는 건드리지 않는다
### 6. 찍어맞추기 금지
- 0deg 안 되면 180deg, 그것도 안 되면 90deg... 이런 식 금지
- 값을 바꾸기 전에 WHY를 먼저 설명할 수 있어야 한다
- 설명 못하면 바꾸지 않는다
### 7. "쉬운 전면 재작성" 절대 금지
- 80점 결과물에서 2가지 문제를 고칠 때, 구조를 flex/grid 등으로 **전면 재작성하지 않는다**
- 기존에 맞춘 수십 가지(pill 크기, 위치, 비율, border 걸침)가 전부 깨진다
- **기존 구조 유지 + 문제만 정확히 수정**이 원칙
- 보완이 안 되면 그 방식을 오답노트로 두고 **다른 방식으로 접근**
- 점점 나빠지면 **즉시 멈추고 마지막 OK 상태로 복원**
- 구조 변경이 불가피하면 **사전에 영향 범위 분석 + 사용자 확인 후** 진행
## Figma 도형 gradient 처리 프로세스
```
1. Figma gradient 각도 확인
2. gradient를 0으로 돌린 기본 상태 파악
- border-radius: Figma 값 그대로
- gradient: CSS 90deg (Figma 0 = 왼→오 = CSS 90deg)
3. CSS로 기본 상태 구현
4. Figma gradient 각도 적용: CSS = 90 - Figma각도
5. 위치(left, top)와 크기(width, height) 배치
```
## Figma gradient 각도 체계
```
Figma 0° = 왼쪽 진 → 오른쪽 옅 = CSS 90°
Figma -90° = 위 진 → 아래 옅 = CSS 180°
Figma -180° = 오른쪽 진 → 왼쪽 옅 = CSS 270°
Figma 90° = 아래 진 → 위 옅 = CSS 0°
```
변환: **CSS = 90 - Figma**

View 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 표시됨 |

View File

@@ -0,0 +1,686 @@
# Design Agent - Technology Research Report
Date: 2026-03-24
---
## 1. CSS Grid for Slide Layouts
### 1.1 Fixed-Viewport Approach (16:9, 1280x720)
**Recommended technique: Fixed container + CSS transform scaling.**
The slide container should be authored at a fixed "normal" size (1280x720), then scaled to fit any viewport using `transform: scale()`. This is the same approach used by reveal.js, the dominant HTML presentation framework.
```css
.slide {
width: 1280px;
height: 720px;
aspect-ratio: 16 / 9;
overflow: hidden;
position: relative;
}
```
For preview/embedding, wrap in a container that calculates a scale factor:
```css
.slide-wrapper {
width: 100%;
max-width: 1280px;
aspect-ratio: 16 / 9;
overflow: hidden;
}
.slide-wrapper > .slide {
transform-origin: top left;
transform: scale(var(--slide-scale, 1));
}
```
The scale factor can be computed with minimal JS: `containerWidth / 1280`.
**Key insight from reveal.js:** All presentations have a "normal" size at which they are authored. The framework automatically scales uniformly to fit different resolutions without changing aspect ratio or layout. Default is 960x700; for our use case, 1280x720 is the standard 16:9 HD dimension.
**Why this works for Design Agent:** The renderer produces HTML at exactly 1280x720. It never needs to be "responsive" -- it's a fixed-format document like a PDF page. Scaling is only for preview purposes.
### 1.2 Grid-Template-Areas for Block Combinations
`grid-template-areas` provides named regions that map directly to the block composition concept in the CLAUDE.md:
```css
.layout-quote-compare-cards-conclusion {
display: grid;
grid-template-areas:
"quote quote"
"compare cards"
"diagram diagram"
"conclusion conclusion";
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr auto auto;
gap: var(--spacing-block);
padding: var(--spacing-page);
}
```
**Best practice:** Define each layout as a separate CSS class with its own `grid-template-areas`. The Sonnet agent selects which layout class to apply based on the block combination it decides. This keeps the renderer deterministic -- it just applies the class.
**Reusability:** CSS variables allow the same grid template to adapt:
- Column count: `grid-template-columns: repeat(var(--cols, 3), 1fr)`
- Gap: `gap: var(--spacing-block)`
- Row sizing: `grid-template-rows` can mix `auto` (content-sized) and `1fr` (fill remaining)
### 1.3 Design Tokens
**Naming convention:** `--{category}-{property}-{variant}`
The CLAUDE.md already defines a good token set. The industry standard approach (from EightShapes, Nord Design System) uses kebab-case with semantic naming:
```
--color-primary, --color-accent, --color-neutral
--font-title, --font-subtitle, --font-body, --font-caption
--spacing-page, --spacing-block, --spacing-inner
--radius, --border-width, --accent-border
```
This matches what's already in the project's CLAUDE.md. No changes needed.
**For slide-specific tokens, add:**
```css
--slide-width: 1280px;
--slide-height: 720px;
--slide-aspect: 16 / 9;
```
### 1.4 Overflow Handling in Fixed Pages
Three techniques for ensuring content fits within fixed dimensions:
1. **Single-line truncation:**
```css
.truncate {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
```
2. **Multi-line truncation (line clamping):**
```css
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
```
3. **Container overflow hidden (safety net):**
```css
.block { overflow: hidden; }
.slide { overflow: hidden; }
```
**Korean-specific consideration:** `word-break: keep-all` affects how text wraps, which impacts line count. Content fitting calculations must account for this. The Sonnet agent should be instructed with character limits per slot, not word limits.
---
## 2. Content-to-Layout Classification
### 2.1 How to Prompt an LLM for Reliable Classification
**Claude Structured Output (recommended):**
Anthropic launched Structured Outputs in November 2025, supporting Claude Sonnet 4.5 and Opus 4.1. This guarantees JSON schema conformance at the token generation level -- the model literally cannot produce tokens that violate the schema.
Implementation:
```python
from pydantic import BaseModel, Field
from anthropic import Anthropic
class ContentBlock(BaseModel):
content_type: str = Field(description="One of: comparison, process, relationship, big-number, definition, list, timeline, emphasis, problem")
evidence: str = Field(description="Which text patterns led to this classification")
suggested_block: str = Field(description="Block type from the template library")
class ContentAnalysis(BaseModel):
blocks: list[ContentBlock]
layout_direction: str = Field(description="How blocks should be arranged on the page")
primary_message: str = Field(description="The single key takeaway for this slide")
```
**Reliability strategies:**
- Set temperature to 0.0-0.1 for classification tasks (reduces format drift)
- Use the `output_format` parameter with JSON schema (not just prompting)
- Include one perfect example in the system prompt
- Add explicit validation instructions
### 2.2 Information Type Taxonomy for Presentations
Based on research from SlideSpeak (16 layout types), PPTAgent (EMNLP 2025), Beautiful.ai (300 templates), and Dr. Andrew Abela's Chart Chooser:
**The CLAUDE.md already defines 9 excellent content types.** Here is how they map to industry precedent:
| CLAUDE.md Type | SlideSpeak Equivalent | PPTAgent Category | Common Slide Type |
|---|---|---|---|
| comparison | SS_ITEMS_*_A/B | Content slide (multi-column) | Comparison slide |
| process | SS_STEPS_3/4/5 | Content slide (sequential) | Process/workflow slide |
| relationship | (custom) | Content slide (diagram) | Venn/tree diagram slide |
| big-number | SS_BIGNUMBER_1/3 | Content slide (metric) | KPI/statistics slide |
| definition (card-grid) | SS_ITEMS_3/4/5/6 | Content slide (grid) | Definition/feature slide |
| list | SS_CONTENT | Content slide (list) | Bullet point slide |
| timeline | (custom) | Content slide (sequential) | Timeline slide |
| emphasis (quote-block) | (custom) | Structural slide | Quote/callout slide |
| problem | (custom) | Structural slide | Problem statement slide |
**Additional types to consider (from industry):**
- **SWOT** (SlideSpeak has SS_SWOT) -- 4-quadrant grid
- **Matrix/Table** (already covered by comparison-table)
- **Cover/Title** -- for when the content is just a single title/subtitle
### 2.3 Structured Output Schema for Layout Decisions
The PPTAgent paper (EMNLP 2025) uses a two-stage approach that aligns perfectly with the Design Agent architecture:
- **Stage 1 (Opus):** Analyze content, classify into functional types, extract content schemas
- **Stage 2 (Sonnet):** Select reference layouts, fill content into slots, apply editing actions
PPTAgent represents all parsed outputs in JSON format for LLM compatibility. The Design Agent should do the same.
---
## 3. Slot-Based Template Systems
### 3.1 SlideSpeak's Named Slot System
SlideSpeak uses a comprehensive naming convention for template placeholders:
**Layout names:** SS_COVER, SS_CONTENT, SS_TABLE_OF_CONTENT, SS_BIGNUMBER_1_A, SS_BIGNUMBER_3_A, SS_ITEMS_3_A through SS_ITEMS_6_B, SS_STEPS_3 through SS_STEPS_5_ICONS, SS_SWOT
**Universal slot names:**
- `SS_TITLE` -- slide title
- `SS_SUBTITLE` -- subtitle
- `SS_LOGO` -- logo placeholder
- `SS_IMAGE` -- general image
- `SS_PAGE` -- page number
- `SS_PRESENTATION_TITLE` -- footer title
**Multi-item slot naming pattern:**
- `SS_ITEM_{N}_TITLE` -- title for item N
- `SS_ITEM_{N}_CONTENT` -- content for item N
- `SS_ITEM_{N}_NUMBER` -- number for item N (big-number layouts)
- `SS_ICON_{N}` -- icon for item N
**Key insight for Design Agent:** The `{{SLOT_NAME}}` convention in CLAUDE.md maps well. Adopt a similar systematic naming: `{{BLOCK_TITLE}}`, `{{ITEM_1_TITLE}}`, `{{ITEM_1_CONTENT}}`, etc.
### 3.2 Jinja2 for Template Rendering
Jinja2 is the recommended engine. It integrates natively with FastAPI and Python.
**Block inheritance for base layout:**
```jinja2
{# base_slide.html #}
<!DOCTYPE html>
<html lang="ko">
<head>
<link rel="stylesheet" href="design-tokens.css">
<style>{% block extra_style %}{% endblock %}</style>
</head>
<body>
<div class="slide">
{% block content %}{% endblock %}
</div>
</body>
</html>
```
**Block-level templates:**
```jinja2
{# blocks/comparison.html #}
<div class="block-comparison">
<div class="col-left">
<h3>{{ left_title }}</h3>
<p>{{ left_content }}</p>
</div>
<div class="col-right">
<h3>{{ right_title }}</h3>
<p>{{ right_content }}</p>
</div>
</div>
```
**Composition via includes:**
```jinja2
{# Generated by renderer based on Sonnet's layout decision #}
{% extends "base_slide.html" %}
{% block content %}
{% include "blocks/quote-block.html" %}
<div class="grid-row-2">
{% include "blocks/comparison.html" %}
{% include "blocks/card-grid.html" %}
</div>
{% include "blocks/conclusion-bar.html" %}
{% endblock %}
```
### 3.3 Slot Constraints
Each slot should have defined constraints that Sonnet respects:
| Slot Type | Max Characters (Korean) | Required | Notes |
|---|---|---|---|
| slide_title | 30 | Yes | Single line |
| block_title | 20 | Yes | Single line |
| item_title | 15 | Yes | Single line |
| item_content | 80 | No | 2-3 lines |
| quote_text | 120 | Yes | 3-4 lines |
| big_number | 8 | Yes | Number + unit |
| conclusion | 60 | Yes | Single line |
| caption | 40 | No | Single line |
**Korean consideration:** Korean characters are roughly 2x the width of Latin characters at the same font size. Character limits should be specified in characters, not words, since Korean doesn't use spaces the same way as English.
---
## 4. HTML to PDF Conversion
### 4.1 Playwright (Recommended)
**Why Playwright over Puppeteer:**
- Native Python SDK (no Node.js dependency for a Python project)
- Multiple browser support (Chromium, Firefox, WebKit), though PDF only works in Chromium
- Growing community, active maintenance, better CI/CD integration
- Full CSS Grid support via real Chromium rendering engine
**Python implementation:**
```python
from playwright.async_api import async_playwright
async def html_to_pdf(html_content: str, output_path: str) -> None:
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.set_content(html_content, wait_until="networkidle")
await page.pdf(
path=output_path,
width="1280px",
height="720px",
print_background=True,
prefer_css_page_size=True,
)
await browser.close()
```
**Key options:**
- `print_background=True` -- required for background colors/images
- `prefer_css_page_size=True` -- lets CSS `@page` rules control dimensions
- `width`/`height` -- custom page dimensions (accepts px, in, mm, cm units)
### 4.2 Print CSS for Slide Format
```css
@media print {
@page {
size: 1280px 720px;
margin: 0;
}
body {
margin: 0;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.slide {
width: 1280px;
height: 720px;
page-break-after: always;
overflow: hidden;
}
}
```
**`-webkit-print-color-adjust: exact`** is critical -- without it, background colors and images may be stripped in PDF output.
### 4.3 Quality Comparison
Both Puppeteer and Playwright use Chromium's print-to-PDF engine, so output quality is identical. The choice comes down to:
| Factor | Playwright | Puppeteer |
|---|---|---|
| Language | Python, JS, C#, Java | JS/Node.js only |
| PDF engine | Chromium only | Chromium only |
| CSS Grid quality | Excellent (Chromium) | Excellent (Chromium) |
| Korean font rendering | Excellent | Excellent |
| Install size | ~400MB (browser binary) | ~300MB |
| API ergonomics | Better async patterns | More established |
**Recommendation:** Playwright, because the Design Agent backend is Python. No need to bridge to Node.js.
### 4.4 Korean-Specific Considerations
- Fonts must be available on the server. Self-host Pretendard/Noto Sans KR WOFF2 files or use CDN.
- Set `lang="ko"` on the HTML element for proper line-breaking algorithms.
- Ensure `@font-face` declarations are loaded before PDF generation (`wait_until="networkidle"`).
---
## 5. Pure CSS Diagrams
### 5.1 Venn Diagrams (Pure CSS)
**Technique:** Overlapping circles with opacity and negative margins.
```css
.venn-container { display: flex; align-items: center; justify-content: center; }
.venn-circle {
width: 200px; height: 200px;
border-radius: 50%;
opacity: 0.7;
display: flex; align-items: center; justify-content: center;
padding: 20px;
text-align: center;
}
.venn-a { background: var(--color-accent); }
.venn-b { background: var(--color-neutral); margin-left: -60px; }
```
**Advanced approach (Adrian Roselli):** CSS Grid + `shape-outside` for text wrapping within overlapping regions. More complex but better for text-heavy Venn diagrams.
**Limitation:** Pure CSS Venn diagrams work well for 2-3 circles. Beyond that, SVG is more practical.
### 5.2 Flowcharts / Process Arrows (Pure CSS)
**Technique:** Flexbox/Grid layout + pseudo-elements for arrows.
```css
.process-steps { display: flex; align-items: center; gap: 0; }
.process-step {
background: var(--color-bg-subtle);
padding: var(--spacing-inner);
position: relative;
flex: 1;
}
.process-step + .process-step::before {
content: '';
position: absolute;
left: -12px; top: 50%;
transform: translateY(-50%);
border: 8px solid transparent;
border-left-color: var(--color-accent);
}
```
**CSS Anchor Positioning (2025-2026):** A new CSS feature for connecting elements with lines. Supported in Chrome 125+, Safari 26+, not yet in Firefox. Since we target Chromium (for PDF generation), this is usable but adds complexity. For the Design Agent, pseudo-element arrows are simpler and more reliable.
### 5.3 Tree/Hierarchy Diagrams (Pure CSS)
**Technique:** Nested `<ul>/<li>` + pseudo-elements for connector lines.
Libraries:
- **Treeflex** (https://dumptyd.github.io/treeflex/) -- CSS-only library for hierarchy trees, no JS
- Custom implementation using `border-left` on `<li>` for vertical lines, `::before` for horizontal connectors, `::after` for node circles
### 5.4 When to Use SVG Instead
| Use Case | CSS | SVG | Recommendation |
|---|---|---|---|
| Venn (2-3 circles) | Good | Better | CSS for simplicity |
| Process arrows (linear) | Excellent | Overkill | CSS |
| Tree (2-3 levels) | Good | Better | CSS (Treeflex) |
| Complex flowchart (branches) | Difficult | Much better | SVG |
| Curved connectors | Impossible | Easy | SVG |
| Data-driven charts | Impossible | Required | SVG |
| Accessible diagrams | Poor | Excellent | SVG |
**Recommendation for Design Agent:** Use pure CSS for simple diagrams (process arrows, 2-circle Venn, basic tree). Generate inline SVG for anything more complex. Since the renderer produces static HTML for PDF export, there's no JS concern.
**Accessibility note:** SVG has built-in accessibility elements (`<title>`, `<desc>`, `aria-*`). For the Design Agent's output (primarily visual slides for PDF), this is less critical but good practice.
---
## 6. Korean Typography in CSS
### 6.1 Font Selection
**Primary recommendation: Pretendard**
- Modern system-ui replacement font designed for cross-platform use
- Built on Inter (Latin) + Source Han Sans (CJK) + M PLUS 1p
- 9 weights + variable font support
- Dynamic subset via CDN (Google Fonts-style loading for Korean)
- Most popular Korean web font according to HTTP Archive 2024
**CDN options:**
```css
/* Dynamic subset (recommended for web) */
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
/* Or from cdnjs */
@import url('https://cdnjs.cloudflare.com/ajax/libs/pretendard/1.3.9/variable/pretendardvariable-dynamic-subset.min.css');
```
**For PDF generation (self-hosted):**
Download WOFF2 files and serve locally to avoid CDN dependency during headless browser PDF generation.
**Fallback stack:**
```css
font-family: 'Pretendard Variable', 'Pretendard', -apple-system, 'Noto Sans KR',
'Malgun Gothic', sans-serif;
```
**Alternative: Noto Sans KR**
- Google Fonts native, widest browser support
- Available via Google Fonts CDN with automatic Korean subsetting
- Good for when Pretendard is not available
### 6.2 Line-Height and Letter-Spacing
**W3C KLREQ (Korean Layout Requirements) recommendations:**
| Property | Value | Rationale |
|---|---|---|
| `line-height` | 1.6-1.8 | CJK text needs ~1.7 (vs 1.2-1.5 for Latin) due to higher information density per character |
| `letter-spacing` | 0 to -0.02em | Korean text looks best with tight or default spacing. Avoid positive letter-spacing. |
| `word-spacing` | normal | Korean uses spaces between words (unlike Japanese/Chinese) |
**For slides specifically:**
```css
body {
line-height: 1.7;
letter-spacing: -0.01em;
}
h1, h2, h3 {
line-height: 1.3;
letter-spacing: -0.02em;
}
```
### 6.3 Word-Break Rules
```css
/* Korean text: keep syllable blocks together */
body {
word-break: keep-all;
overflow-wrap: break-word;
}
```
**`word-break: keep-all`** prevents line breaks within Korean syllable blocks (e.g., "한글" won't break as "한" / "글" mid-word). This is essential for readable Korean typography.
**`overflow-wrap: break-word`** is the safety net for extremely long strings (URLs, technical terms) that might overflow.
**Browser support:** All modern browsers support `keep-all` since 2016. There is ongoing W3C discussion about a potential `keep-all-hangul` value that would apply `keep-all` only to Hangul characters and `normal` to everything else.
### 6.4 Mixing Korean + English
**Key challenge:** At the same point size, Latin letters appear smaller than Korean characters.
**Solutions:**
1. Use a font designed for mixed text (Pretendard handles this well, being built on Inter + Source Han Sans)
2. Set the Latin font first in `font-family` stack, CJK font second (most CJK fonts include Latin glyphs, but their Latin is often inferior)
3. No need for `font-size-adjust` if using Pretendard (it's designed for optical balance between scripts)
**Punctuation:** Korean uses proportional-width punctuation (like Latin), unlike Japanese/Chinese which use full-width. Pretendard handles this correctly.
### 6.5 Language Attribute
Always set `lang="ko"` on the HTML element:
```html
<html lang="ko">
```
This enables the browser's Korean-specific line-breaking algorithms and font selection.
---
## 7. FastAPI Integration
### 7.1 Serving the Design Agent
The Design Agent should be a FastAPI application with these endpoints:
| Endpoint | Method | Purpose |
|---|---|---|
| `/api/analyze` | POST | Step 1: Send content to Opus for classification |
| `/api/generate` | POST | Steps 2-3: Sonnet selects + renderer produces HTML |
| `/api/generate/stream` | GET (SSE) | Steps 1-3 with real-time progress |
| `/api/preview/{job_id}` | GET | Return generated HTML for iframe preview |
| `/api/download/{job_id}/pdf` | GET | Generate and return PDF |
| `/api/download/{job_id}/html` | GET | Return HTML file |
| `/api/templates` | GET | List available block templates |
### 7.2 SSE Streaming
FastAPI has native SSE support (added to official docs). Two library options:
**Option A: sse-starlette (recommended)**
- Production-ready, W3C SSE spec compliant
- Already used in the HWPX project
- `pip install sse-starlette`
**Option B: fastapi-sse**
- Lighter weight, built specifically for FastAPI
- Supports sending Pydantic models as SSE events
- `pip install fastapi-sse`
**Implementation:**
```python
from sse_starlette.sse import EventSourceResponse
@router.get("/api/generate/stream")
async def stream_generation(content: str):
async def event_generator():
yield {"event": "step_start", "data": "analyzing"}
# ... Opus classification
yield {"event": "step_complete", "data": json.dumps(analysis)}
yield {"event": "step_start", "data": "selecting"}
# ... Sonnet selection
yield {"event": "step_complete", "data": json.dumps(slots)}
yield {"event": "step_start", "data": "rendering"}
# ... CSS Grid rendering
yield {"event": "complete", "data": job_id}
return EventSourceResponse(event_generator())
```
### 7.3 File Upload
```python
from fastapi import UploadFile, File
@router.post("/api/upload")
async def upload_content(file: UploadFile = File(...)):
# Read and extract text from uploaded file
text = await extract_text(file)
return {"text": text, "filename": file.filename}
```
### 7.4 Static File Serving for Preview
**For development:** FastAPI's `StaticFiles` mount for serving generated HTML:
```python
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="output"), name="static")
```
**For production:** Serve HTML via API response body (like the HWPX project does with `<iframe srcDoc>`). This is more secure -- no direct file path exposure.
**Font serving:** Self-hosted Pretendard WOFF2 files should be served as static files for reliable PDF generation.
---
## Summary: Technology Stack Recommendation
| Component | Technology | Version | Rationale |
|---|---|---|---|
| LLM (Analysis) | Claude Opus via Anthropic API | Structured Outputs beta | Content classification, layout direction |
| LLM (Selection) | Claude Sonnet via Anthropic API | Structured Outputs beta | Slot filling, content editing |
| Template Engine | Jinja2 | >=3.1 | Native Python, FastAPI integration, block inheritance |
| CSS Layout | CSS Grid + grid-template-areas | Native CSS | Named regions map to block composition |
| Design Tokens | CSS Custom Properties | Native CSS | Already defined in CLAUDE.md |
| Typography | Pretendard Variable | 1.3.9 | Best Korean web font, dynamic subset |
| Fallback Font | Noto Sans KR | Latest | Google Fonts CDN backup |
| PDF Generation | Playwright (Python) | >=1.40 | Native Python SDK, full CSS Grid support |
| Web Framework | FastAPI | >=0.115 | SSE support, file upload, same as HWPX project |
| SSE | sse-starlette | >=2.0 | Production-ready, W3C compliant |
| CSS Diagrams | Pure CSS + inline SVG fallback | N/A | Pseudo-elements for simple, SVG for complex |
| Slide Scaling | CSS transform: scale() | Native CSS | reveal.js-proven approach |
| Korean Line Breaking | word-break: keep-all | Native CSS | W3C KLREQ recommendation |
---
## Key References
### CSS Grid & Layout
- [CSS-Tricks Complete Guide to CSS Grid](https://css-tricks.com/complete-guide-css-grid-layout/)
- [Smashing Magazine: Understanding CSS Grid Template Areas](https://www.smashingmagazine.com/2020/02/understanding-css-grid-template-areas/)
- [MDN: aspect-ratio](https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio)
- [Grid by Example](https://gridbyexample.com/examples/)
- [reveal.js Presentation Size](https://revealjs.com/presentation-size/)
### Design Tokens
- [CSS-Tricks: What Are Design Tokens?](https://css-tricks.com/what-are-design-tokens/)
- [EightShapes: Naming Tokens in Design Systems](https://medium.com/eightshapes-llc/naming-tokens-in-design-systems-9e86c7444676)
- [FrontendTools: CSS Variables Guide](https://www.frontendtools.tech/blog/css-variables-guide-design-tokens-theming-2025)
- [Nord Design System: Naming](https://nordhealth.design/naming/)
### Content Classification & Presentation AI
- [PPTAgent (EMNLP 2025)](https://arxiv.org/abs/2501.03936)
- [SlideSpeak Layouts & Placeholders](https://docs.slidespeak.co/basics/custom-templates/layouts-and-placeholders)
- [SlideSpeak Template Preparation](https://docs.slidespeak.co/basics/custom-templates/preparing)
- [SlideModel: 12 Types of Slides](https://slidemodel.com/types-of-slides/)
- [SlideUpLift: Types of Slides](https://slideuplift.com/blog/types-of-slides/)
- [LLM-Powered Slide Decks Comparison](https://nbrosse.github.io/posts/llm-slides/llm-slides.html)
### Structured Output
- [Anthropic Structured Outputs Docs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)
- [PromptLayer: How JSON Schema Works for Structured Outputs](https://blog.promptlayer.com/how-json-schema-works-for-structured-outputs-and-tool-integration/)
### PDF Generation
- [Playwright Python page.pdf() API](https://playwright.dev/python/docs/api/class-page)
- [PDF Generation from HTML Comparison (2026)](https://medium.com/@coders.stop/pdf-generation-from-html-i-tested-puppeteer-playwright-and-wkhtmltopdf-so-you-dont-have-to-d14228d28c4c)
- [Checkly: Generating PDFs with Playwright](https://www.checklyhq.com/docs/learn/playwright/generating-pdfs/)
- [Print CSS Cheatsheet](https://www.customjs.space/blog/print-css-cheatsheet/)
### Pure CSS Diagrams
- [Adrian Roselli: A CSS Venn Diagram](https://adrianroselli.com/2018/12/a-css-venn-diagram.html)
- [CSS-Tricks: A CSS Venn Diagram](https://css-tricks.com/a-css-venn-diagram/)
- [FreeFrontend: 17 Pure CSS Flowcharts](https://freefrontend.com/css-flowcharts/)
- [Cory Rylan: Flow Charts with CSS Anchor Positioning](https://coryrylan.com/blog/flow-charts-with-css-anchor-positioning)
- [Treeflex: CSS Tree Library](https://dumptyd.github.io/treeflex/)
- [MDN: CSS Anchor Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Anchor_positioning)
### Korean Typography
- [W3C KLREQ: Requirements for Hangul Text Layout](https://www.w3.org/TR/klreq/)
- [Pretendard GitHub](https://github.com/orioncactus/pretendard)
- [Noto Sans KR on Google Fonts](https://fonts.google.com/noto/specimen/Noto+Sans+KR)
- [CJK Typesetting in 2025](https://asianabsolute.co.uk/blog/cjk-typesetting-challenges-workflows-and-best-practices/)
- [Typotheque: CJK Typesetting Principles](https://www.typotheque.com/articles/typesetting-cjk-text)
- [CSS WG: word-break for Korean (Issue #4285)](https://github.com/w3c/csswg-drafts/issues/4285)
### FastAPI & SSE
- [FastAPI SSE Tutorial](https://fastapi.tiangolo.com/tutorial/server-sent-events/)
- [sse-starlette on PyPI](https://pypi.org/project/sse-starlette/)
- [Real Python: FastAPI with Jinja2](https://realpython.com/fastapi-jinja2-template/)
### SVG vs CSS
- [Adobe Blog: CSS vs SVG](https://blog.adobe.com/en/publish/2015/09/16/css-vs-svg-the-final-roundup)
- [Sara Soueidan: Accessible Data Charts](https://www.sarasoueidan.com/blog/accessible-data-charts-for-khan-academy-2018-annual-report/)

View File

@@ -0,0 +1,466 @@
# CSS 보정 규칙
Figma → HTML 변환 시 Figma와 CSS 렌더링 차이를 수학적으로 보정하는 규칙 모음.
**모든 규칙은 수학적 근거가 있어야 한다. 감으로 보정하지 않는다.**
---
## R1. Descender 보정 (padding-bottom)
**문제:** CSS `line-height: 1`이면 글리프 하강부(g, y, p, 쉼표)가 잘림.
Figma는 line-height에 관계없이 글리프를 항상 표시하지만, CSS는 line box 밖을 자른다.
**원인:** 폰트의 content area > line box일 때 half-leading이 음수가 되어 잘림 발생.
**계산:**
```
content_area_ratio = (typoAscender + |typoDescender|) / UPM
half_leading = (line_height - content_area_ratio) / 2 ← 음수이면 잘림
clipped_px = |half_leading| × font_size
padding-bottom = ceil(clipped_px)
```
**폰트별 값:**
| 폰트 | UPM | Ascender | Descender | content_area_ratio |
|------|-----|----------|-----------|-------------------|
| Noto Sans KR | 1000 | 1160 | 288 | 1.448 |
| Pretendard | 1000 | 1100 | 300 | 1.400 |
**예시 (Noto Sans KR, font-size 27.1px, line-height 1):**
```
half_leading = (1 - 1.448) / 2 = -0.224
clipped = 0.224 × 27.1 = 6.07px
→ padding-bottom: 7px
```
**적용:** `line-height < content_area_ratio`인 모든 텍스트 요소에 padding-bottom 추가.
---
## R2. 회전 감지 (bbox 비율)
**문제:** Figma MCP는 `rotation`/`transform` 속성을 출력하지 않음.
**감지 방법:** 바운딩 박스의 가로세로 비율이 해당 글자의 정상 비율과 반대이면 회전.
```
단일 문자 "(" 정상: ~18×50 (세로가 김)
Figma bbox: 60×19 (가로가 김)
→ 가로:세로 = 3.2:1 → 90° 회전 확정
```
**규칙:**
- 단일 문자 텍스트에서 `width > height × 1.5` → 90° 회전
- 일반 텍스트에서 `width < fontSize × 0.8` → 세로 배치용 좁은 박스 (writing-mode 아님, <br> 줄바꿈)
**CSS 구현:**
```css
.rotated-bracket { transform: rotate(90deg); } /* 여는 괄호 */
.rotated-bracket-close { transform: rotate(-90deg); } /* 닫는 괄호 */
```
---
## R3. 세로 텍스트 (좁은 박스)
**문제:** Figma에서 좁은 박스(width < fontSize) 안에 텍스트를 넣으면 글자가 한 줄에 하나씩 배치됨.
**감지:** `bbox.width < fontSize × 0.8` + 2글자 이상
**CSS 구현:** `writing-mode` 사용하지 않음. HTML에서 `<br>`로 글자마다 줄바꿈.
```html
<span class="vlabel"><br></span>
```
이유: `writing-mode: vertical-rl`은 Figma 원본과 다른 간격/정렬을 만듦.
---
## R4. 그라데이션 텍스트
**Figma:** 텍스트 fills에 GRADIENT_LINEAR이 있으면 그라데이션 텍스트.
**CSS:**
```css
.gradient-text {
background: linear-gradient(...);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
```
**주의:** 흰 텍스트 스트로크(`-webkit-text-stroke: white`) 사용 금지.
HTML에서 보기 불편하므로 제거한다.
---
## R5. 다중 fills 처리
**Figma:** 하나의 노드에 여러 fill이 쌓일 수 있음 (리스트 순서 = 위에서 아래).
**규칙:** 첫 번째 fill이 불투명(opacity 1)이면 나머지는 가려짐 → 첫 번째만 사용.
---
## R6. 중복 노드
**감지:** 동일 좌표 + 동일 내용 + 동일 크기 → Figma 복사 흔적.
**처리:** 1개만 렌더링, 나머지 무시. flat 목록에 [중복] 표기.
---
## R7. 미리보기 배경
**슬라이드 배경:** 항상 `#ffffff` (흰색)
**블록 배경:** 항상 `#ffffff` (미리보기용). 원본 배경색은 주석으로 기록.
이유: 다크 배경에서 요소가 안 보이는 문제 방지. 위치/크기 확인이 우선.
---
## R8. 스케일 팩터
**계산:** `Scale = 1280 / 원본_width`
**적용 대상:**
- 위치 (x, y)
- 크기 (width, height)
- 폰트 크기 (fontSize)
- 스트로크 너비 (strokeWeight)
- 간격 (gap, padding)
- 그림자 (blur, offset)
**적용하지 않는 것:**
- 색상 (그대로 유지)
- 그라데이션 방향/퍼센트 (그대로 유지)
- 폰트 굵기 (그대로 유지)
- line-height 비율 (그대로 유지)
- border-radius 비율 (스케일 적용)
**구현 권장:** 매 값 수동 곱셈 대신 `transform: scale(S)` 한 번으로 균일 축소. MATH.md §1 참조.
---
## R9. 순수 CSS 우선, SVG는 곡선/필터에만
블록 라이브러리의 동적 재구성을 위해 가능한 한 **HTML div + CSS**로 구현한다.
| 요소 | 구현 |
|------|------|
| 원/사각형 + linear-gradient | `<div>` + `border-radius` + `background: linear-gradient(...)` |
| Stroke (경계선) | `border` + `box-sizing: border-box` |
| Drop shadow blur | `box-shadow: 0 0 {2×stdDev}px {color}` |
| **곡선 (아크, 비원형 path)** | **SVG `<path>` 또는 PNG** ← CSS 불가능 |
| **복잡한 SVG filter chain** | **SVG `<filter>`** ← CSS 근사 불가 시 |
| 텍스트 | HTML `<div>` 절대 배치 |
**이유:** SVG `<img src="...svg">`는 정적 파일. 색상/개수/위치 변경 시 매번 재export 필요. CSS는 변수/Jinja로 즉시 파라미터화 가능.
---
## R10. Blend mode 호환 (plus-darker → multiply)
**문제:** Figma의 `plus darker` blend mode는 Apple CoreGraphics 전용. CSS 스펙엔 `plus-darker`가 있지만 **Safari/WebKit만 지원**, Chrome/Firefox에서는 무시되어 효과 사라짐.
**규칙:**
1. SVG/CSS에 `mix-blend-mode: plus-darker` 발견 시 → **`multiply`로 교체**
2. SVG 파일 내부의 `style="mix-blend-mode:plus-darker"`도 함께 교체
3. 시각 차이 검증: 흰 배경 위 밝은 그라데이션은 거의 동일. 어두운 영역은 multiply가 더 강함
```
plus-darker(src, dst) = max(0, src + dst - 1) [Safari only]
multiply(src, dst) = src × dst [모든 브라우저]
```
자세한 비교: MATH.md §7
---
## R11. Stroke 정렬: viewBox padding 처리
SVG는 stroke가 fill의 안/밖으로 확장될 수 있어 viewBox에 padding이 들어감. CSS 변환 시 두 케이스로 나뉨:
### 케이스 A — Stroke가 fill **외부**
예: `r=140 fill` + `r=142.5 stroke-width=5` → stroke가 r=140~145 (외부)
```css
.ring {
width: 290px; height: 290px; /* fill 280 + 외부 stroke 5×2 */
border: 5px solid white;
box-sizing: border-box; /* border 안쪽 padding-box = 280 = fill */
background: linear-gradient(...); /* default origin: padding-box 280 */
border-radius: 50%;
}
/* 위치: Figma fill 위치에서 (-5, -5) 오프셋 */
```
### 케이스 B — Stroke가 fill **내부** (overlap)
예: `r=140 fill` + `r=137.5 stroke-width=5` → stroke가 r=135~140 (fill 외곽 overlap)
```css
.ring {
width: 280px; height: 280px; /* fill 280 그대로 */
border: 5px solid white;
box-sizing: border-box; /* padding-box 270 */
background: linear-gradient(...);
background-origin: border-box; /* gradient는 280 영역에 매핑 */
background-clip: border-box;
border-radius: 50%;
}
/* 위치: Figma fill 위치 그대로 */
```
판별: SVG 안의 stroke `r` 값이 fill `r`보다 **크면** 외부 (케이스 A), **작거나 같으면** 내부 (케이스 B).
---
## R12. viewBox padding gradient remap
viewBox padding이 있는 SVG의 그라데이션 좌표는 viewBox 공간 기준이므로, CSS 박스로 매핑할 때 **각 좌표에서 padding 만큼 빼야** 한다.
```python
# SVG viewBox 310, 실제 fill 280, padding 15
css_x1 = svg_x1 - 15
css_y1 = svg_y1 - 15
css_x2 = svg_x2 - 15
css_y2 = svg_y2 - 15
# 그 다음 svg_to_css(W=280, H=280, ...)
```
또는 `scripts/gradient_math.py``svg_to_css_remap()` 사용:
```python
svg_to_css_remap(css_W=280, css_H=280, viewbox_padding=15,
x1=..., y1=..., x2=..., y2=..., stops=[...])
```
---
## R14. 한글 줄바꿈은 word-break: keep-all (전역 default)
**문제:** Chrome 기본 동작은 한글을 글자 단위로 wrap (예: "수행공정의 쉬운이해로 관리 편의성 증" / "진"). Figma는 단어 단위 wrap이라 시각이 다름.
**규칙:** 모든 변환물의 base CSS에 `word-break: keep-all` 적용.
```css
body {
font-family: 'Noto Sans KR', sans-serif;
...
word-break: keep-all; /* 한글 단어 단위 wrap (Figma matching) */
}
```
또는 텍스트 컨테이너 단위로:
```css
.bullet-text, .left-text, .right-text, .body-text {
word-break: keep-all;
}
```
**언제 빼나:**
- `white-space: nowrap` 단일 라인 텍스트 (영향 없음, 안 빼도 무방)
- 코드/숫자 등 단어 경계가 없는 콘텐츠
**예외:** 영문/기호 혼합 텍스트는 `word-break: keep-all` 만으로는 부족할 수 있음. 그 경우 `overflow-wrap: anywhere` 또는 `<br>` 명시 split.
---
## R15. 박스 vertical center align (Figma flex justify-center 모방)
**문제:** Figma React 코드에서 자주 보이는 패턴:
```jsx
<div className="-translate-y-1/2 absolute flex flex-col h-[71px] justify-center top-[243.5px]">
```
이는 **컨테이너 박스의 vertical center에 텍스트를 정렬**한다는 의미. 단순히 `top` 값만 받아서 박는 건 잘못 — 텍스트가 박스 top에 붙어 다른 요소(예: cat pill의 vertical center)와 어긋남.
**올바른 변환:**
```css
.text-box {
position: absolute;
top: <visual_top>; /* Figma top - height/2 */
height: <figma_height>;
width: <figma_width>;
display: flex;
flex-direction: column;
justify-content: center; /* vertical center */
}
```
**또는 인접 박스(예: 옆에 있는 cat pill)와 동일한 top + height를 박고 flex justify-center 적용**하면 자동으로 가운데 align. 1:1 변환에서 가장 안전.
**검증:** 인접 박스 center y 와 텍스트 박스 center y 가 같은지 측정. 차이 > 5px이면 잘못된 것.
---
## R13. Custom-Marker Bullet List 패턴 (sub-pattern)
**감지 조건 (3가지 모두 충족):**
1. 여러 텍스트 항목이 세로로 나열됨
2. 각 항목 앞에 **장식 마커**가 있음 (체크박스 아이콘, 점, 화살표, 숫자, 원, PNG 등)
3. 마커는 인터랙티브하지 않고 순수 시각 요소 (실제 `<input type="checkbox">` 가 아님)
**Figma 원본에서는** 마커와 텍스트가 별도 요소로 평면 배치돼있을 수 있다. 그래도 **시맨틱적으로는 하나의 list item**으로 봐야 한다.
### 구조 (CSS Flex Pair Pattern)
```html
<div class="bullet-list" style="--icon-gap: ...;">
<div class="bullet-row">
<span class="bullet-icon"><img src="marker.png"></span>
<span class="bullet-text">텍스트 항목</span>
</div>
<div class="bullet-row compact">
<span class="bullet-icon"><img src="marker.png"></span>
<span class="bullet-text">긴 텍스트가<br>두 줄로</span>
</div>
</div>
```
### CSS
```css
.bullet-list {
display: flex;
flex-direction: column;
/* 동일 top/bottom 정렬을 위해 컨테이너에 fixed height + space-between */
justify-content: space-between;
}
.bullet-row {
display: flex;
align-items: flex-start;
--lh: 85px; /* 기본 라인 높이 */
}
.bullet-row.compact {
--lh: 50px; /* 2-line 항목용 타이트 lh */
}
.bullet-icon {
flex: none;
width: var(--icon-w);
height: var(--icon-h);
/* 핵심: 아이콘 vertical center를 첫 줄 vertical center에 align */
margin-top: calc(var(--lh) / 2 - var(--icon-h) / 2);
/* 컬럼별 figma gap (text_left icon_left icon_w) */
margin-right: var(--icon-gap);
}
.bullet-text {
flex: 1;
line-height: var(--lh);
white-space: normal;
word-break: keep-all; /* 한글: 단어 단위 줄바꿈 */
}
```
### 핵심 수학
```
icon margin-top = lh / 2 icon_h / 2 (첫 줄 vertical center)
icon margin-right = text_left icon_left icon_w (Figma 데이터)
```
### 절대 하지 말 것
- 마커와 텍스트를 별도 요소로 절대 배치 (`<div class="checkbox" style="left:..; top:..">` × N)
- row에 fixed `height` 설정 (wrap 시 overlap)
- `white-space: nowrap` (텍스트가 컨테이너 밖으로 overflow)
- 모든 row에 동일한 top/bottom margin 강제 (텍스트 길이가 결정해야 함)
### 정렬 원칙
3개 이상의 평행한 컬럼이 있을 때:
- **모든 컬럼은 동일한 top + 동일한 height** 로 시작
- 컬럼별 자연 콘텐츠 합 중 **가장 큰 값**을 height로 사용
- `justify-content: space-between` 으로 내부 균등 분포
- 결과: 컬럼별 spacing은 다르지만 vertical extent는 동일
### 적용 사례
| 프레임 | 사용 | 비고 |
|--------|------|------|
| 1171281191 (cards-3col-persona) | 3 컬럼 × 6~7 마커-text 페어 | 첫 적용 |
| (앞으로 비슷한 패턴 발견 시 추가) | | |
### 1:1 변환 단계의 임시 보정 (템플릿화 시 제거)
다음은 1:1 시각 fidelity를 위한 **임시 보정**이며, 템플릿화 시 모두 제거해야 한다 (자연 wrap이 처리):
- `letter-spacing: -1.5px` 등 — Chrome Noto Sans KR 너비가 Figma보다 약간 넓어 wrap이 일어나는 것을 방지하기 위한 보정
- `<br>` 명시적 줄바꿈 — Figma의 의도된 split 위치 보존용. 템플릿화 시 자연 wrap이 알아서 처리
- `class="compact"` 수동 지정 — 어떤 항목이 2-line인지 1:1 단계에선 수동, 템플릿화 시 텍스트 길이 자동 판정
이 보정들은 HTML 코멘트로 `<!-- TEMP: 1:1 fidelity, 템플릿화 시 제거 -->` 표시한다.
---
## R16. 이미지 프레임 배치 — overflow:hidden으로 부분 표시
**상황:** 하나의 원본 이미지에 양쪽 끝 모두 디자인 요소(곡선, 말림, 장식 등)가 있고, Figma에서 프레임(컨테이너)보다 이미지를 크게 배치하여 **한쪽만 보이게** 하는 경우.
**Figma가 하는 것:**
- 프레임: 457.96px (표시 영역)
- 이미지: 664px (원본, 프레임보다 큼)
- 이미지를 프레임 안에서 `left`, `width`로 위치/크기 지정
- 프레임에 `overflow: hidden` → 프레임 밖으로 나간 부분 안 보임
- 결과: 이미지의 **원하는 쪽만** 프레임 안에 보임
**Figma가 주는 값의 의미:**
```
left: -45.3%; width: 145.3%
→ 이미지를 좌측으로 45.3% 밀어서 배치
→ 좌측 끝이 프레임 밖으로 나감 → 좌측 디자인 요소 안 보임
→ 우측 디자인 요소만 프레임 안에 보임
left: 0; width: 151.25%
→ 이미지를 좌측 정렬, 우측이 프레임 밖으로 넘침
→ 우측 디자인 요소 안 보임
→ 좌측 디자인 요소만 프레임 안에 보임
```
**이것은 crop이 아니다.** 이미지를 자르는 것이 아니라, 프레임 안에서 이미지의 **위치**를 조절하는 것. 이미지 원본은 그대로 유지.
**CSS 구현:**
```css
.pill-frame {
position: relative; /* 또는 absolute */
width: 457.96px; /* 프레임 크기 */
height: 95.62px;
overflow: hidden; /* 핵심: 프레임 밖 숨김 */
}
.pill-frame img {
position: absolute;
top: 0;
left: -45.3%; /* Figma 값 그대로 */
width: 145.3%; /* Figma 값 그대로 */
height: 100%;
}
```
**절대 하지 말 것:**
- `width: 100%; object-fit: fill` — 이미지가 찌그러져 양쪽 디자인 요소가 다 보임
- `scaleX(-1)` 임의 추가 — Figma에 없는 변환
- `object-fit: cover/contain` — 이미지 비율/위치가 달라짐
- "crop"이라 부르기 — 이미지를 자르는 게 아니라 위치를 조절하는 것
**rotate(180deg) + 이미지 배치 주의:**
부모에 `rotate(180deg)`가 적용된 경우 (예: 하단 pill), 이미지가 상하좌우 모두 뒤집힘. 이때 **이미지 배치(left/width)를 상단과 반대로** 적용해야 최종 결과가 올바른 방향이 됨.
```
상단 left-pill: left: -45.3%; width: 145.3% → 우측 보임
하단 left-pill: rotate(180) + left: 0; width: 151.25% → 결과적으로 우측 보임 (뒤집혀서)
상단 right-pill: left: 0; width: 151.25% → 좌측 보임
하단 right-pill: rotate(180) + left: -45.3%; width: 145.3% → 결과적으로 좌측 보임 (뒤집혀서)
```
**검증 방법:** 각 pill을 개별 screenshot으로 뽑아서 Figma 원본 pill screenshot과 **곡선/직선 위치**를 1:1 대조. 양쪽 다 곡선이 보이면 이미지 배치가 잘못된 것.
**적용 사례:**
| 프레임 | 사용 | 비고 |
|--------|------|------|
| 1171281194 (issues-paired-rows) | 두루마리 pill 8개 | 첫 적용. 상/하 배치 반전 패턴 발견. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Some files were not shown because too many files have changed in this diff Show More