cleanup: legacy templates/blocks + figma_to_html_agent block-tests / 옛 docs 정리

전체 561 files, 32464 deletions. Phase Z runtime 의존성 0 — 모두 옛 작업 흔적.

대상:
- templates/blocks/ (104) — 옛 design_agent block library (BEPs, cards, structures,
  emphasis, headers, media, svg, visuals, tables 등). Phase Z 가 templates/phase_z2/
  로 이전 후 outdated.
- templates/catalog.yaml — 위 block library 의 catalog (의존성과 같이 폐기).
- figma_to_html_agent/block-tests/ (301) — figma 옛 변환 시도들 (renders, assets,
  html, png, json, txt, md). 새 figma_to_html_agent/blocks/ 가 대체.
- figma_to_html_agent/templates_staging/ (49) — 옛 staging 시도.
- figma_to_html_agent/figma-{assets,screenshots,analysis,_ref}/ — 옛 figma 자료.
- figma_to_html_agent/previews/ (3) — 옛 preview.
- figma_to_html_agent/{FIGMA-*.md, PLAN.md, RESEARCH.md, PHASE-FIGMA-BLOCKS.md} (7)
  — outdated docs (figma 통합 axis 가 끝난 후 polluting).
- figma_to_html_agent/{block_index.faiss, block_metadata.json, figma_*.json} (6)
  — 옛 FAISS index + metadata.

Phase Z runtime / V4 catalog 영향 0 (의존성 grep 으로 사전 검증).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 09:39:36 +09:00
parent 7762f6766a
commit cc2f434000
561 changed files with 0 additions and 32464 deletions

View File

@@ -1,582 +0,0 @@
# 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

@@ -1,150 +0,0 @@
# 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

@@ -1,164 +0,0 @@
# 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

@@ -1,674 +0,0 @@
# 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

@@ -1,478 +0,0 @@
# 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

@@ -1,98 +0,0 @@
# 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

@@ -1,686 +0,0 @@
# 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/)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

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