Compare commits

..

10 Commits

Author SHA1 Message Date
66c00924ed Add Type B slide pipeline and recipe rendering updates 2026-04-15 16:39:50 +09:00
51548fdc41 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>
2026-04-13 11:00:31 +09:00
360cd8e44c 블록 템플릿 업데이트: 수정 3개 + 신규 17개 + catalog.yaml 재정비
수정:
- compare-2col-badge, compare-detail-gradient, hero-icon-cards 개선

신규 블록:
- cards: category-strip-table, system-2col-center, hero-icon-cards_1
- emphasis: checklist-dark
- visuals: cycle-orbit
- new/: cards-3col-persona, compare-vs-rows, cycle-3way-intersect 등 8개
- redesign/: card_hero-icon-cards_1
- svg/: arc_left, arrow_conclusion, arrow_down, line_divider
- BEPs: process-product-2col SVG + 테스트

catalog.yaml 전면 재정비 (슬롯 스키마, height_cost, 카테고리 정리)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:58:09 +09:00
c42e01f060 문서 정리: Phase 히스토리 md를 docs/history/로 이동 + 오래된 테스트/에셋 정리
- 루트의 IMPROVEMENT-PHASE-*.md, PHASE-*.md 등 45개 → docs/history/로 이동
- docs/block-tests/ 오래된 블록 테스트 HTML 삭제 (figma_to_html_agent로 대체)
- docs/figma-analysis/, docs/figma-assets/, docs/figma-screenshots/ 정리
- docs/test-*.html 등 초기 테스트 파일 정리
- 참고 페이지/ 스크린샷 정리

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:56:23 +09:00
d57860578f PIPELINE.md 신규 작성 + ARCHITECTURE_OVERVIEW.md deprecated 처리
파이프라인 현황 문서 신규 작성:
- Stage 0~4 전체 흐름, Type A/B/B'/B'' 구분, MDX↔유형 매핑
- 단계별 파일/함수 맵, 데이터 흐름, 현재 구현 상태
- ARCHITECTURE_OVERVIEW.md는 3/27 스냅샷으로 archived 처리

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:51:01 +09:00
05703c8e72 WIP: hero-icon-cards_1 블록 + 오답노트 + figma 관련 파일
- hero-icon-cards_1.html: hero-icon-cards 변형 (icon → 소제목+불릿 계층)
- compare-detail-gradient.html: 하단 2열 비교 블록 (Figma Frame 4 기반)
- 오답노트.md: 절대 하지 말아야 하는 실수 목록
- figma_to_html.py: Figma→HTML 변환 스크립트
- static/figma-assets/: Figma export 이미지 (배지, 화살표)
- 주의: compare-detail-gradient CSS 폰트 크기가 임의 수정됨 — 원본 복원 필요

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:14:09 +09:00
076aeb0403 Type B'' 추가: 참고 이미지 스타일 (색상바+여백, border 없음)
- block_assembler_b2.py: B'' 전용 조립 함수 (별도 파일)
- 상단: 색상 바 제목 + 소제목(accent 색상) + 불릿(들여쓰기)
- 하단: 색상 바 제목 + 표(있으면) + 불릿
- border/gradient 박스 없음, 여백과 폰트로 구분
- 이제부터 스타일 세부 조정은 하나씩 반영 예정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:51:10 +09:00
3d1194a562 03번 들여쓰기 계층 적용: D1=bold 1단 들여쓰기, D2=불릿 2단 들여쓰기
- block_assembler B': D1(<strong>) → padding 0.5x, bold, 불릿 없음
- block_assembler B': D2(일반) → padding 1x, 불릿, 폰트 -2px
- 남은 문제: 상단/하단 콘텐츠 잘림 (공간 부족)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:26:11 +09:00
cba2ec2be7 03번 B' 개선: 카드 flex:1 균등 + 결론 텍스트 중복 제거
- block_assembler B': 카드 div에 flex:1 추가 → 가로 균등 높이/폭
- block_assembler: [핵심요약:] 이후 라인 break → 결론 텍스트가 하단에 섞이는 문제 해결
- 남은 문제: 하단 좌 콘텐츠 잘림, 상단 자연(여건) 불릿 잘림

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:15:26 +09:00
b13df8b176 03번 B' 정상 동작: 가로 3단 카드 + overflow 해소 + 하단 균형
- block_assembler B': 카드 3개 이상 + 이미지 없음 → 가로(row) 배치
- block_assembler B': section title이 카드 제목, D1은 카드 내 bold 불릿
- block_assembler: overflow:auto → overflow:hidden, [핵심요약:] 마커 필터
- block_assembler: \x01 바이트 수정
- pipeline: Selenium 실측 기반 zone 간 재배분 (allocated-scrollHeight로 slack 계산)
- pipeline: surplus 최대 50%만 이전 (하단 최소 공간 보장)
- pipeline: bottom_left/bottom_right → Selenium bottom zone 매핑
- kei_client: 상단은 팝업 대상 제외, 하단에서만 팝업 분리

결과: 02번/03번 모두 overflow 없이 정상 출력

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 10:32:14 +09:00
616 changed files with 26056 additions and 4955 deletions

65
.claude/settings.json Normal file
View File

@@ -0,0 +1,65 @@
{
"permissions": {
"allow": [
"Bash(cmd /c \"npx -y figma-developer-mcp --help\")",
"Bash(npx -y figma-developer-mcp --version)",
"mcp__Framelink_Figma_MCP__get_figma_data",
"mcp__Framelink_Figma_MCP__download_figma_images",
"Bash(start \"\" \"d:/ad-hoc/kei/design_agent/figma_to_html_agent/block-tests/prerequisites-3col.html\")",
"Bash(python -c \"from selenium import webdriver; print\\('selenium OK'\\)\")",
"Bash(python -c ':*)",
"Bash(python)",
"Bash(start \"\" \"d:/ad-hoc/kei/design_agent/figma_to_html_agent/block-tests/bim-goal-circles.html\")",
"Bash(start \"\" \"d:/ad-hoc/kei/design_agent/figma_to_html_agent/block-tests/bg-shapes-only.html\")",
"Bash(start \"\" \"d:/ad-hoc/kei/design_agent/figma_to_html_agent/block-tests/bim-figma-devmode.html\")",
"Bash(claude mcp:*)",
"Bash(curl -sS -o /dev/null -w \"mcp endpoint: HTTP %{http_code}\\\\n\" http://127.0.0.1:3845/mcp)",
"Bash(curl -sS -o /dev/null -w \"sse endpoint: HTTP %{http_code}\\\\n\" http://127.0.0.1:3845/sse)",
"Bash(curl -sS -o /dev/null -w \"root: HTTP %{http_code}\\\\n\" http://127.0.0.1:3845/)",
"Bash(curl -s -o NUL -w \"%{http_code}\" http://127.0.0.1:3845/mcp)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:3845/sse --max-time 3)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:3845/mcp --max-time 3)",
"Bash(curl -s -X POST http://127.0.0.1:3845/mcp -H \"Content-Type: application/json\" -H \"Accept: application/json, text/event-stream\" -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test\",\"version\":\"1.0\"}}}' --max-time 5)",
"Bash(curl -v http://127.0.0.1:3845/mcp)",
"Bash(curl -s -m 3 http://127.0.0.1:3845/sse)",
"Bash(curl -s -m 3 -o /dev/null -w \"%{http_code}\\\\n\" http://127.0.0.1:3845/sse)",
"mcp__figma-desktop__get_metadata",
"mcp__figma-desktop__get_screenshot",
"mcp__figma-desktop__get_design_context",
"Bash(curl -sSo bg_texture.png \"http://localhost:3845/assets/849658071be46a26936e8666e3722b2dd548aee7.png\")",
"Bash(curl -sSo arc_top.png \"http://localhost:3845/assets/f05ebf15a1125b6c5809f9ffa35b4e4e750687d3.png\")",
"Bash(curl -sSo arc_side.png \"http://localhost:3845/assets/2f0f17507c681b7bc2fb109f3d4fafb9ff2f7ce0.png\")",
"Bash(curl -sSo big_fill_productivity.svg \"http://localhost:3845/assets/21a89b8138bd47debcc6f12bb140ee63bbd9fdf2.svg\")",
"Bash(curl -sSo big_ring_productivity.svg \"http://localhost:3845/assets/fbe84134d2e14bbf84b2c42516e9b85ffe6f7c1e.svg\")",
"Bash(curl -sSo big_fill_safety.svg \"http://localhost:3845/assets/1f24875931dc3c36e2c841eaf5b94466fa035a48.svg\")",
"Bash(curl -sSo big_ring_safety.svg \"http://localhost:3845/assets/c5aeccdfc884051848fc60f04abf2a9d367dd731.svg\")",
"Bash(curl -sSo big_fill_trust.svg \"http://localhost:3845/assets/67ef527c29921d401d31032c02d6b3a0ae1d3050.svg\")",
"Bash(curl -sSo acc_outer_speed.svg \"http://localhost:3845/assets/1391787caa4cb8241a1adadbb2c70aed3625e1b8.svg\")",
"Bash(curl -sSo acc_inner_speed.svg \"http://localhost:3845/assets/eeb8e9bf8b1841215ae0253017512a7e4a6d5a95.svg\")",
"Bash(curl -sSo acc_inner_profit.svg \"http://localhost:3845/assets/4885055cba20f72e83401be371fe74b9b43ec869.svg\")",
"Bash(curl -sSo acc_outer_safety.svg \"http://localhost:3845/assets/688b5af1d813b16cd6410453e3d4d1f79c084222.svg\")",
"Bash(curl -sSo acc_inner_safety.svg \"http://localhost:3845/assets/2fab268821fc763dbdff12e1dd65820dfa9b628e.svg\")",
"Bash(ls block-tests/*.html block-tests/*.md)",
"Bash(python scripts/gradient_math.py --test)",
"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\")",
"Bash(python -c \"from scripts.gradient_math import svg_to_css; print\\(svg_to_css\\(W=350,H=350,x1=110.833,y1=18.2292,x2=219.479,y2=175,stops=[\\(0,'#FDC69E'\\),\\(1,'#E0782C'\\)]\\)\\)\")",
"Bash(python render.py cards-3col-persona example)",
"Bash(python render.py cards-3col-persona example-no-photos)",
"Bash(python render.py cycle-3way-intersect example)",
"WebFetch(domain:claude.com)",
"WebFetch(domain:help.figma.com)",
"Bash(curl -sSo \"527bd7809f4b2e5f3cd42f2e713ccbfb37537d82.png\" \"http://localhost:3845/assets/527bd7809f4b2e5f3cd42f2e713ccbfb37537d82.png\")",
"Bash(ls \"d:/ad-hoc/kei/design_agent/figma_to_html_agent/block-tests/_renders/pill_flex_\"*)",
"Bash(awk '/visual_diff:/{found=1} found && /^- id:/{print NR\": \"$0; found=0}' \"d:/ad-hoc/kei/design_agent/templates/catalog.yaml\")",
"Bash(curl -s -o \"bg_slide_texture.png\" \"http://localhost:3845/assets/16a1b2ea5b64663a3ee44bfad24671a612952c29.png\")",
"Bash(curl -s -o \"line_divider.svg\" \"http://localhost:3845/assets/01731a60f7d9d35816932c019149e301a3aae1a7.svg\")",
"Bash(head -20 /d/ad-hoc/kei/design_agent/samples/mdx/01*.mdx)",
"Bash(head -20 /d/ad-hoc/kei/design_agent/samples/mdx/02*.mdx)",
"Bash(head -20 /d/ad-hoc/kei/design_agent/samples/mdx/03*.mdx)",
"Bash(ls -la /d/ad-hoc/kei/design_agent/data/runs/20260413_*/)"
],
"additionalDirectories": [
"d:\\ad-hoc\\kei\\design_agent\\templates\\blocks\\new"
]
}
}

View File

@@ -1,15 +1,8 @@
{
"mcpServers": {
"Framelink Figma MCP": {
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"figma-developer-mcp",
"--figma-api-key=figd_s23TfSDL0hS97DIialy0R2P6QsoZQHfuGx1l_t-k",
"--stdio"
]
"figma-desktop": {
"type": "sse",
"url": "http://127.0.0.1:3845/sse"
}
}
}

View File

@@ -1,4 +1,12 @@
# Design Agent 전체 구조 파악 리포트
# ⚠ DEPRECATED — 이 문서는 2026-03-27 기준 스냅샷입니다
> **최신 파이프라인 문서는 [`PIPELINE.md`](PIPELINE.md)를 참조하세요.**
> 이 문서는 Type A/B 분기, B'/B'' 변형, Stage 1.7/1.8 등 현재 구조를 반영하지 않습니다.
> 히스토리 참고용으로만 유지합니다.
---
# Design Agent 전체 구조 파악 리포트 (archived)
**작성일:** 2026-03-27
**목표:** design_agent의 아키텍처, 파이프라인, 코드 구조를 체계적으로 이해

304
PIPELINE.md Normal file
View File

@@ -0,0 +1,304 @@
# Design Agent 파이프라인 현황
> **최종 갱신:** 2026-04-13
> **목적:** 새 세션의 AI가 이 문서만 읽으면 파이프라인 전체를 이해하고 작업할 수 있도록 한다.
---
## 1. 전체 흐름 요약
```
MDX 입력
[Stage 0] MDX 정규화 (코드)
[Stage 1A] Kei 실장 — 꼭지 추출 (AI: Opus)
→ layout_template: A 또는 B 선택
[Stage 1B] 컨셉 구체화 (AI: Opus)
→ relation_type, expression_hint, source_data
[Stage 1B-ST] 구조화 텍스트 생성 (AI: Opus)
→ structured_text per topic
[Stage 1.5a] 컨테이너 계산 (코드: 결정론적)
→ FontHierarchy, ContainerSpec, Preset
[Stage 1.7] 블록 레퍼런스 선택 (코드 + AI 1회)
→ relation_type → 카테고리 → 필터 → 블록 결정
[Stage 1.8] 적합성 검증 + 보강 (코드 + Selenium + AI)
→ overflow 감지 → Kei 에스컬레이션 → 재배분
[Stage 1.5b] 디자인 예산 계산 (코드)
[Stage 2] HTML 생성 (Type에 따라 다름)
→ Type B/B'/B'': block_assembler (코드)
→ Type A: Sonnet 재구성 (AI, 미완성)
[Stage 3] 렌더링 조립 (Type A만, Jinja2)
[Stage 4] 검증 (Selenium + Opus Vision)
→ overflow 측정 + 스크린샷 품질 평가
최종 HTML 출력 (data/runs/{id}/final.html)
```
---
## 2. 레이아웃 유형 (Type A / B / B' / B'')
### 2.1 Kei가 선택하는 유형: A와 B
Kei 프롬프트(`src/kei_client.py:34-46`)에서 A 또는 B를 선택한다.
| 유형 | 조건 | Zone 구조 |
|------|------|-----------|
| **Type A** | 참조자료(용어 정의, 부록 등)가 별도로 존재 | body(배경+본심) + sidebar(첨부) + footer(결론) |
| **Type B** | 본문 흐름만. 배경/첨부가 없거나 억지로 만들어야 하면 | top + bottom_left + bottom_right + footer |
### 2.2 Type B 변형: B'과 B''
B'과 B''은 **Kei가 선택하지 않는다.** 특정 MDX 테스트 과정에서 하드코딩한 변형이다.
| 변형 | 생성 경위 | 차이점 | 코드 위치 |
|------|----------|--------|----------|
| **B** | 범용 | 상단(전체폭 텍스트+이미지) + 하단 2분할 + 결론 | `block_assembler.py:461` `_assemble_slide_html_type_b()` |
| **B'** | 03번 MDX 테스트 중 생성 | 상단이 세로 카드 형태 + 하단에 표 렌더링 + 불릿 전용 | `block_assembler.py:885` `_assemble_slide_html_type_b_prime()` |
| **B''** | B'에서 스타일 변형 | border/gradient 없음. 색상바+여백으로 구분 | `block_assembler_b2.py:9` `_assemble_slide_html_type_b_double_prime()` |
**분기 코드** (`block_assembler.py:370-378`):
```python
if ctx.analysis.layout_template == "B":
return _assemble_slide_html_type_b(ctx, title_text)
if ctx.analysis.layout_template == "B'":
return _assemble_slide_html_type_b_prime(ctx, title_text)
if ctx.analysis.layout_template == "B''":
return _assemble_slide_html_type_b_double_prime(ctx, title_text)
```
### 2.3 향후 방향
B'/B''은 **범용화가 필요하다.** 현재는 03번 콘텐츠 구조(카드형+표)를 B 조립 함수가 커버하지 못해서 만든 땜질이다. 궁극적으로는 B 하나로 다양한 콘텐츠 구조를 커버하거나, AI가 서브타입을 판단하게 해야 한다.
---
## 3. MDX 샘플 ↔ 유형 매핑
| MDX | 파일 | 콘텐츠 성격 | 선택 유형 | 상태 |
|-----|------|-----------|----------|------|
| **01번** | `samples/mdx/01. 건설산업 DX의 올바른 이해(0127).mdx` | 용어 혼용 문제 + 용어 정의(참조) | **Type A** | ⚠ Stage 2 미완성 (Sonnet 의존) |
| **02번** | `samples/mdx/02. DX의 시행 목표 및 기대효과.mdx` | 본문 흐름 (3대 목표) | **Type B** | ✅ 동작 |
| **03번** | `samples/mdx/03. DX 시행을 위한 필수 요건 및 혁신 방안.mdx` | 카드형 구조 + 표 + 불릿 | **Type B'** | ✅ 동작 (하드코딩) |
---
## 4. 단계별 상세
### Stage 0: MDX 정규화
- **파일:** `src/mdx_normalizer.py``normalize_mdx_content()`
- **입력:** raw MDX 텍스트
- **출력:** `NormalizedContent` (sections, images, tables 분리)
### Stage 1A: Kei 실장 — 꼭지 추출
- **파일:** `src/kei_client.py``classify_content()`
- **AI:** Opus (Kei API)
- **입력:** 정규화된 텍스트
- **출력:** `Analysis` (title, core_message, layout_template, total_pages, page_structure, topics)
- **핵심 판단:**
- 꼭지 5개 이내 추출
- 각 꼭지에 purpose, layer, role, emphasis, direction 부여
- layout_template = "A" 또는 "B" 선택
- page_structure에 역할별 weight(비중) 배정
### Stage 1B: 컨셉 구체화
- **파일:** `src/kei_client.py``refine_concepts()`
- **AI:** Opus
- **출력:** topics에 relation_type, expression_hint, source_data 추가
### Stage 1B-ST: 구조화 텍스트 생성
- **파일:** `src/kei_client.py``generate_structured_text()`
- **AI:** Opus
- **출력:** topic별 structured_text (마크다운 형태)
### Stage 1.5a: 컨테이너 계산 (결정론적)
- **파일:** `src/space_allocator.py`
- **함수:**
- Type A → `calculate_container_specs()`
- Type B/B'/B'' → `build_containers_type_b()`
- 공통 → `calculate_font_hierarchy()`, `select_preset()`
- **입력:** page_structure의 weight, slide 크기(1280×720)
- **출력:** 역할별 `ContainerSpec` (width_px, height_px, zone)
- **로직:** weight × available_px = 각 zone px 확정
### Stage 1.7: 블록 레퍼런스 선택
- **파일:** `src/block_reference.py``select_and_generate_references()`
- **로직 (코드 결정론적 + AI 1회):**
1. relation_type → 블록 카테고리 매핑
2. expression_hint 키워드 매칭
3. 컨테이너 크기 적합성 필터
4. role/zone 제약 (sidebar → visuals/media 제외)
5. catalog.yaml 존재 검증 (유령 블록 차단)
6. 후보 2-3개 → Kei 1회 호출로 최종 선택
- **출력:** 역할별 `BlockReference` (block_id, design_reference_html)
### Stage 1.8: 적합성 검증 + 보강
- **파일:** `src/fit_verifier.py``calculate_fit()`
- **로직:**
1. 텍스트 분량 vs 할당 공간 계산
2. Selenium으로 실제 높이 측정 (3회 루프)
3. overflow 시 → Kei 에스컬레이션 (`call_kei_fit_escalation()`)
- 팝업 분리 판단, zone 간 재배분
4. 보강 제안: bold 키워드, 팝업 요약 등
### Stage 2: HTML 생성
**Type B/B'/B'' (코드 조립):**
- **파일:** `src/block_assembler.py``assemble_slide_html()`
- 역할별 `assemble_role_html()` 호출 → 블록 HTML 조립
- structured_text + design_reference_html 결합
- 이미지/팝업 embed
- **즉시 완성 HTML 반환**
**Type A (AI 재구성, 미완성):**
- **파일:** `src/content_verifier.py``generate_with_retry()`
- Sonnet에 phase_t_context 전달 → CSS + 레이아웃 생성
- **현재 검증 불완전**
### Stage 3: 렌더링 조립 (Type A만)
- **파일:** `src/renderer.py``render_slide_from_html()`
- Type B는 Stage 2에서 완전한 HTML이므로 스킵
### Stage 4: 검증
- **파일:** `src/slide_measurer.py`
- `measure_rendered_heights()` — Selenium 실측
- `capture_slide_screenshot()` — 스크린샷 캡처
- **파일:** `src/kei_client.py``vision_quality_gate()`
- Opus 멀티모달: 스크린샷 보고 시각 품질 평가
---
## 5. 핵심 파일 맵
```
src/
├── main.py ← FastAPI 서버, POST /api/generate
├── pipeline.py ← 파이프라인 오케스트레이터 (generate_slide)
├── pipeline_context.py ← PipelineContext 데이터 클래스
├── config.py ← 설정 (API key, 슬라이드 크기)
├── mdx_normalizer.py ← Stage 0: MDX → NormalizedContent
├── kei_client.py ← Stage 1A/1B/1B-ST: Kei API 호출 + 프롬프트
├── space_allocator.py ← Stage 1.5a: 컨테이너 px 계산
├── block_reference.py ← Stage 1.7: 블록 선택 (relation_type 기반)
├── fit_verifier.py ← Stage 1.8: 적합성 검증 + Selenium 루프
├── block_assembler.py ← Stage 2: Type B/B' HTML 조립
├── block_assembler_b2.py ← Stage 2: Type B'' HTML 조립
├── content_verifier.py ← Stage 2: Type A HTML (Sonnet, 미완성)
├── renderer.py ← Stage 3: Jinja2 렌더링 (Type A만)
├── slide_measurer.py ← Stage 4: Selenium 측정 + 스크린샷
├── validators.py ← Kei 응답 검증 (A/B별 구조 확인)
├── image_utils.py ← 이미지 크기 측정 + data URI 변환
├── svg_calculator.py ← SVG 다이어그램 좌표 계산
└── sse_utils.py ← SSE 스트리밍 유틸
templates/
├── slide-base.html ← 슬라이드 기본 구조 (Jinja2, CSS Grid)
├── catalog.yaml ← 블록 라이브러리 정의 (50+개)
└── blocks/ ← 블록 HTML 템플릿
├── headers/ (8개)
├── cards/ (17개)
├── emphasis/ (12개)
├── tables/ (8개)
├── visuals/ (6개)
├── media/ (4개)
└── BEPs/ (6개)
```
---
## 6. 데이터 흐름 (PipelineContext)
```
PipelineContext:
raw_content ← 원본 MDX
normalized ← NormalizedContent (sections, images, tables)
analysis ← Analysis (title, core_message, layout_template, page_structure, topics)
topics ← list[Topic] (relation_type, expression_hint, structured_text 포함)
page_structure ← PageStructure (roles → {topic_ids, weight, zone})
containers ← dict[role → ContainerSpec(width_px, height_px)]
font_hierarchy ← FontHierarchy (key_msg, core, bg, sidebar 폰트 크기)
references ← dict[role → list[BlockReference]]
sub_layouts ← dict[role → SubLayout]
fit_result ← 역할별 fit_status, 재배분값
enhancement_result ← bold_keywords, popup_summaries 등
generated_html ← Stage 2 출력
rendered_html ← Stage 3 출력 (완전 HTML)
measurement ← Selenium 측정값
quality_score ← 0-100
```
---
## 7. 현재 구현 상태 (Phase Y-11~13, 2026-04-15)
> Phase Y: slide-base 기반 파이프라인 재설계. 상세: `docs/history/PHASE-Y-PLAN.md`
### 파이프라인 흐름 (현재)
```
[Stage 0] MDX → normalized.sections (source of truth)
[Stage 1A] Kei 꼭지 추출 (영역/zone 판단 안 함)
[Phase Y] 코드: normalized → 대목차 추출 → group schema 분류 → 블록 매칭 → 영역 확정
[Stage 1.5a] space_allocator: weight → zone px (% 기반)
[Stage 1.7] block_reference: tag_match → schema_match → fallback 순서
[Stage 1.8] assembler(measure_mode) → Selenium 측정 → fit 루프
[Stage 2] assembler(slide-base + 블록) → final HTML
[Stage 4] Selenium overflow + 비전 (-1 미평가)
```
### MDX별 상태
| MDX | 상태 | 비고 |
|-----|------|------|
| **03** | ✅ 동작 | prerequisites-3col + pp2. 텍스트 누락 없음. 회귀 기준. |
| **02** | ⚠ schema 1차 | top: parallel_3_with_image. bottom: 분류 정교화 필요. |
| **01** | ⬜ 미착수 | Type A. 별도 작업. |
### 핵심 원칙 (확립됨)
- source of truth = normalized.sections (Stage 0)
- 영역 = 코드가 결정 (Kei 아님). sub_titles 기반 + group schema.
- 블록 CSS에 최종 고정값. slide_font_css는 공통 레이아웃 계약만.
- zone = % 기반, block = height:100%.
- 글씨 크기 고정. fit은 padding → 내용량 → font 1단계(responsive tier).
- 기존 경로 삭제 금지. 새 schema 점진적 추가. MDX 03 회귀 기준.
- 하드코딩 금지. 프로세스가 결과를 만드는 구조.
3. **블록 글씨 크기 하드코딩 (px 고정)**
- 블록 CSS에 font-size가 Figma 원본 px로 고정
- 컨테이너 크기에 따라 조정 불가 → overflow 원인
- CSS 변수(`var(--block-font-heading)`)로 전환 → assembler가 zone 크기에 따라 계산
### 미해결 프로세스
1. **overflow 시 font 조정 루프** — 재배분만으로 부족할 때 font/padding 줄이기 (Y-5)
2. **Sonnet redesign 경로** — tag 매칭 실패 시 블록 단위 redesign → 저장 (Y-6)
---
## 8. 검증 계획
업데이트된 템플릿이 파이프라인에서 제대로 동작하는지 확인한다.
| MDX | 유형 | 검증 포인트 |
|-----|------|-----------|
| **01번** | Type A | 업데이트된 블록 + slide-base.html로 조립 정상 동작 |
| **02번** | Type B | 업데이트된 블록 선택 + 조립 + overflow 없음 |
| **03번** | Type B' | 카드/표 구조가 업데이트된 템플릿으로 정상 렌더링 |
### 검증 방법
1. 각 MDX를 파이프라인에 투입
2. 중간 산출물(step1_analysis.json 등) 확인 — 블록 선택이 의도대로인지
3. 최종 HTML(final.html) 렌더링 — overflow, 시각 품질 확인
4. 업데이트 전/후 비교

431
README.md
View File

@@ -1,303 +1,248 @@
# Kei Design Agent
콘텐츠를 시각적으로 구조화된 슬라이드 HTML(1280×720px, 16:9)로 변환하는 AI 파이프라인.
MDX 콘텐츠를 분석해 1280x720 슬라이드 HTML로 변환하는 파이프라인입니다.
## 개요
이 문서는 "지금 실제 코드 기준으로 파이프라인이 어떻게 동작하는지"를 빠르게 파악하기 위한 개요 문서입니다. 과거 Phase 문서와 일부 legacy 경로는 남아 있지만, 아래 설명은 현재 메인 경로를 기준으로 정리했습니다.
텍스트/MDX 콘텐츠를 입력하면:
1. Kei 실장(Opus)이 정보 구조와 비중을 판단하고
2. 코드가 컨테이너 크기를 계산하고
3. 블록을 선택하고
4. 콘텐츠-컨테이너 적합성을 검증하고
5. AI(Sonnet)가 블록 디자인을 참고하여 HTML을 생성하고
6. 코드가 슬라이드 프레임에 조립하고
7. 측정+비전 모델로 검증합니다
## 한눈에 보기
---
파이프라인의 큰 흐름은 아래와 같습니다.
## 파이프라인 (10단계)
1. MDX를 코드가 정규화한다.
2. Kei가 문서의 의미와 topic을 읽는다.
3. 코드는 `normalized.sections`를 기준으로 실제 슬라이드 구조를 다시 만든다.
4. 코드는 schema, recipe, tag를 바탕으로 블록을 고른다.
5. Type B는 코드가 템플릿을 조립해 `final.html`을 만든다.
6. Type A는 아직 AI/renderer 비중이 더 크다.
7. Selenium과 vision gate로 측정/검증한 뒤 run 산출물을 저장한다.
```
MDX 원본
[Stage 0] MDX 정규화 (코드)
[Stage 1A] 꼭지 추출 + 영역 배정 (Kei API / Opus)
[Stage 1B] 컨셉 구체화 (Kei API / Opus)
[Stage 1.5a] 컨테이너 초기 계산 (코드)
[Stage 1.7] 블록 선택 (코드)
[Stage 1.8] 적합성 검증 + 재배분 + 보강 (코드 + Kei 에스컬레이션)
[Stage 1.5b] 디자인 예산 재계산 (코드)
[Stage 2] HTML 생성 (영역별 개별 호출) (Claude Sonnet)
[Stage 3] 렌더링 조립 + 후처리 (코드)
[Stage 4] 측정 + 품질 검증 (Selenium + Opus Vision)
검증 통과 시 → final.html 저장 + 팝업 분리 (파일 출력)
```
핵심 원칙은 다음과 같습니다.
※ Stage 4 이후의 파일 저장은 별도 Stage가 아닌 후처리입니다.
- source of truth는 `normalized.sections`
- block 선택은 문서명 하드코딩이 아니라 shape, schema, tag 기반
- 흐름의 우선순위는 `구조 -> payload -> layout -> fit`
- popup/detail은 overflow를 덮는 임시 장치가 아니라 `메인 요약 + 상세 보기`의 2단 표현 계약
---
## 타입 구조
## 단계별 상세
현재 메인 타입 선택은 사실상 `A``B`입니다.
### Stage 0: MDX 정규화
### Type A
| 항목 | 내용 |
|------|------|
| **목적** | 원본 MDX에서 JSX/frontmatter를 제거하고, 섹션/팝업/이미지/테이블로 분리 |
| **적용기술** | 코드 (`normalize_mdx_content()`) |
| **인풋** | 원본 MDX 문자열 |
| **아웃풋** | `normalized` — clean_text, title, sections[], popups[], images[], tables[] |
| **연계** | → Stage 1A가 clean_text를 Kei에게 전달 |
- 본문 외에 sidebar, reference, 부록성 영역이 함께 필요한 슬라이드
- 현재는 Type B보다 덜 닫혀 있고, AI 생성 + renderer 경로 비중이 큽니다
### Stage 1A: 꼭지 추출 + 영역 배정
### Type B
| 항목 | 내용 |
|------|------|
| **목적** | 콘텐츠에서 핵심 파트(꼭지)를 식별하고, 슬라이드의 어떤 영역(배경/본심/첨부/결론)에 배치할지 결정 |
| **적용기술** | Kei API (`classify_content()`) |
| **인풋** | normalized.clean_text |
| **아웃풋** | `topics[]` (id, title, purpose, layer, relation_type, expression_hint), `page_structure` (role별 topic_ids, weight) |
| **연계** | → Stage 1B가 각 꼭지를 구체화 |
- top, bottom 같은 본문 zone 조합으로 해결되는 슬라이드
- 현재 가장 안정적인 메인 경로입니다
- 최근 구조화 작업은 대부분 이 Type B 경로를 중심으로 진행되었습니다
### Stage 1B: 컨셉 구체화
### Type B' / B''
| 항목 | 내용 |
|------|------|
| **목적** | 각 꼭지에 실제 원본 텍스트(source_data)와 요약(summary)을 매핑 |
| **적용기술** | Kei API (`refine_concepts()`) |
| **인풋** | topics + clean_text |
| **아웃풋** | `topics` 업데이트 — source_data, summary 추가 |
| **연계** | → Stage 1.5a가 텍스트 양을 기반으로 컨테이너 비율 계산 |
- 역사적으로 실험/호환 경로에서 나온 변형입니다
- 문서와 일부 legacy 코드에 흔적이 남아 있습니다
- 현재 메인 개념의 1급 타입으로 보기보다, 과거 흐름과 호환 레이어로 이해하는 편이 맞습니다
### Stage 1.5a: 컨테이너 초기 계산
## 단계별 파이프라인
| 항목 | 내용 |
|------|------|
| **목적** | 폰트 위계 확정 + 슬라이드 내 영역별 컨테이너 크기(px) 계산 + 프리셋 선택 |
| **적용기술** | 코드 (`calculate_font_hierarchy()`, `calculate_dynamic_ratio()`, `calculate_container_specs()`) |
| **인풋** | topics, page_structure (weight), preset |
| **아웃풋** | `font_hierarchy` (key_msg/core/bg/sidebar px), `container_ratio` (71:29 등), `containers` (role별 width_px, height_px), `preset` |
| **연계** | → Stage 1.7이 컨테이너 크기를 보고 블록 선택 |
아래는 현재 기준의 실질적인 단계입니다.
### Stage 1.7: 블록 선택
| 단계 | 담당 | 주요 파일 | 하는 일 | 주요 산출물 |
|---|---|---|---|---|
| Stage 0 | 코드 | `src/mdx_normalizer.py` | MDX를 정규화하고 sections, tables, images, popups로 분리 | `NormalizedContent` |
| Stage 1A | AI (Kei/Opus) | `src/kei_client.py` | title, core message, topic, 초기 layout 힌트 추출 | `Analysis`, `Topic[]` |
| Phase Y | 코드 | `src/pipeline.py`, `src/section_parser.py` | `normalized.sections` 기반으로 group schema, recipe, zone/page_structure 결정 | `PageStructure`, `mdx_sections` |
| Stage 1B | AI (Kei/Opus) | `src/kei_client.py` | topic별 relation, source_data, 표현 힌트 보강 | 보강된 `Topic[]` |
| Stage 1B-ST | AI (Kei/Opus) | `src/kei_client.py` | structured_text 생성 | `Topic.structured_text` |
| Stage 1.5a | 코드 | `src/space_allocator.py` | zone/container 크기, preset, font hierarchy 계산 | `containers`, `font_hierarchy` |
| Stage 1.7 | 코드 | `src/block_reference.py`, `templates/catalog.yaml` | tag_match, schema_match, fallback 기준으로 block 선택 | `references` |
| Stage 1.8 | 코드 + Selenium + 일부 AI | `src/pipeline.py`, `src/slide_measurer.py` | fit 측정, overflow 확인, 재배분, 보정 | `fit_result`, `measurement` |
| Stage 2 | 코드 중심 | `src/block_assembler.py` | Type B 기준 slide-base + block template + payload 조립 | `generated_html` |
| Stage 3 | 코드 | `src/renderer.py` | Type A 쪽 Jinja/renderer 조립, Type B는 대체로 생략 | `rendered_html` |
| Stage 4 | 코드 + Vision AI | `src/slide_measurer.py`, `src/kei_client.py` | Selenium overflow 측정, screenshot, vision quality 평가 | `measurement`, `quality_score` |
| Stage 5 | 코드 | `src/pipeline.py` | `final.html`, `final_context.json`, popup/detail html 저장 | run 산출물 |
| 항목 | 내용 |
|------|------|
| **목적** | 각 꼭지의 relation_type + expression_hint + 컨테이너 크기로 적합한 블록 결정. 같은 영역 꼭지들의 layer가 다르면 주종관계 판단 (블록 1개로 합침) |
| **적용기술** | 코드 (`select_and_generate_references()`) — catalog.yaml 기반 결정론적 매칭 |
| **인풋** | topics, containers, page_structure |
| **아웃풋** | `references` — role별 block_id, variant, design_reference_html, topic_id, is_hierarchical, supporting_topic_ids |
| **연계** | → Stage 1.8이 선택된 블록+콘텐츠가 컨테이너에 맞는지 검증 |
## 현재 메인 실행 경로
### Stage 1.8: 적합성 검증 + 재배분 + 보강 + 서브 컨테이너
### Type B 메인 경로
| 항목 | 내용 |
|------|------|
| **목적** | 콘텐츠가 컨테이너에 들어가는지 검증 → 안 맞으면 재배분 → 여전히 안 되면 Kei 에스컬레이션 → 여유 공간에 보충 콘텐츠 → 서브 컨테이너 배치 계산 |
| **적용기술** | 코드 (`calculate_fit()`, `redistribute()`, `analyze_enhancements()`, `apply_enhancements()`, `calculate_sub_layout()`) + Kei API (에스컬레이션 시 `call_kei_fit_escalation()`) |
| **인풋** | topics, containers, references, font_hierarchy, normalized, core_message |
| **아웃풋** | `containers` (재배분된 height_px), `fit_result` (role별 fit_status, redistribution), `enhancement_result` (V-7 subordinate_treatments, V-8 supplement_blocks, V-9 emphasis_blocks, V-10 bold_keywords, V-4 kei_decisions), `sub_layouts` (role별 서브 컨테이너 name/width/height, table_rows) |
| **내부 흐름** | Step 1: 필요 높이 계산 → Step 2: 재배분 → Step 3: Kei 에스컬레이션 → Step 4-5: 보강 분석+적용 → Step 6: fit 재검증 → Step 7: 서브 컨테이너 배치 → Step 8: 확정 |
| **연계** | → Stage 1.5b가 재배분된 크기로 디자인 예산 재계산, → Stage 2가 sub_layouts + enhancements를 프롬프트에 반영 |
지금 실전에서 가장 중요한 경로는 아래입니다.
### Stage 1.5b: 디자인 예산 재계산
1. Stage 0에서 MDX를 정규화
2. Stage 1A/1B에서 Kei가 의미와 topic 추출
3. Phase Y에서 코드가 `normalized.sections`를 읽고 page_structure를 다시 생성
4. Stage 1.7에서 block 선택
5. Stage 1.8에서 fit/overflow 검증
6. Stage 2에서 `assemble_slide_html_final()`로 최종 HTML 조립
7. Stage 4/5에서 측정과 산출물 저장
| 항목 | 내용 |
|------|------|
| **목적** | 재배분된 컨테이너 크기 + 선택된 블록 schema 기준으로 영역별 가용 공간 계산 |
| **적용기술** | 코드 (`calculate_design_budget()`) |
| **인풋** | containers (재배분 후), references (블록 schema) |
| **아웃풋** | `containers` 업데이트 — design_budget (available_height_px, available_width_px, fits) |
| **연계** | → Stage 2가 design_budgets를 프롬프트에 포함 |
Type B의 핵심 파일은 아래입니다.
### Stage 2: HTML 생성 (영역별 개별 호출)
- `src/pipeline.py`
- `src/section_parser.py`
- `src/block_reference.py`
- `src/block_assembler.py`
- `src/space_allocator.py`
- `templates/catalog.yaml`
| 항목 | 내용 |
|------|------|
| **목적** | page_structure에 존재하는 각 역할(배경/본심/첨부/결론)의 HTML을 **영역별 개별 Sonnet 호출**로 생성. 블록 디자인을 참고하되 콘텐츠가 구조를 결정 (Phase R' 방식) |
| **적용기술** | Claude Sonnet API — 영역당 1회 호출 (`build_area_prompt()``_call_claude()`) |
| **인풋** | raw_content, topics, containers, font_hierarchy, references (design_reference_html), sub_layouts (서브 컨테이너 치수), enhancements (V-4~V-10 지시), design_budgets |
| **호출 흐름** | Sonnet(배경) → bg_html, Sonnet(본심) → core_html, Sonnet(첨부) → sidebar_html, Sonnet(결론) → footer_html. 해당 역할에 꼭지가 없으면 스킵. body_html = bg_html + spacer + core_html |
| **아웃풋** | `generated_html` — body_html, sidebar_html, footer_html |
| **프롬프트에 포함되는 것** | 서브 컨테이너 레이아웃 제약, 디자인 레퍼런스 HTML (블록 CSS 참고), Kei 에스컬레이션 결정, 종속 꼭지 처리 지시, 보충 블록 지시, 강조 문장, bold 키워드, 폰트/컨테이너 크기 제약 |
| **연계** | → Stage 3이 영역별 HTML을 슬라이드 프레임에 배치 |
### Type A 경로
### Stage 3: 렌더링 조립 + 후처리
Type A는 현재도 살아 있지만, Type B만큼 단단하게 닫힌 상태는 아닙니다.
| 항목 | 내용 |
|------|------|
| **목적** | 생성된 HTML 조각을 CSS Grid 슬라이드 프레임에 삽입 + 후처리 (폰트 캡핑, overflow 제거, sidebar width 조정, bold 변환) |
| **적용기술** | 코드 (`render_slide_from_html()`) |
| **인풋** | generated_html, preset (grid_areas, grid_columns), font_hierarchy, container_ratio |
| **아웃풋** | `rendered_html``final.html` 파일 저장 |
| **연계** | → Stage 4가 렌더링 결과를 측정+검증 |
- AI 생성 비중이 더 큼
- `src/renderer.py` 의존도가 더 큼
- sidebar/reference 구조를 포함하는 쪽에서 의미가 큼
### Stage 4: 품질 검증
## schema -> recipe -> block
| 항목 | 내용 |
|------|------|
| **목적** | Selenium으로 실제 브라우저 렌더링 후 overflow 측정 + Opus Vision으로 시각적 품질 평가 |
| **적용기술** | Selenium (`measure_rendered_heights()`) + Claude Opus Vision (`vision_quality_gate()`) |
| **인풋** | rendered_html |
| **아웃풋** | `measurement` (zone별 clientHeight, scrollHeight, overflow, excess_px), `quality_score` |
| **연계** | 파이프라인 완료. overflow 시 경고 포함하여 진행 |
최근 구조화에서 가장 중요한 변화 중 하나는 `schema -> recipe -> block` 레이어입니다.
---
### schema
## 중간 산출물
콘텐츠의 의미 구조입니다.
파이프라인 실행마다 `data/runs/{timestamp}/`에 단계별 결과가 저장된다.
예:
### JSON Context (Stage별 누적 상태)
| 파일 | Stage | 내용 |
|------|-------|------|
| `stage_0_context.json` | 0 | normalized (섹션, 팝업, 이미지) |
| `stage_1a_context.json` | 1A | topics, page_structure |
| `stage_1b_context.json` | 1B | topics (source_data 추가) |
| `stage_1_5a_context.json` | 1.5a | font_hierarchy, containers, ratio |
| `stage_1_7_context.json` | 1.7 | references (블록 선택 결과) |
| `stage_1_8_context.json` | 1.8 | fit_result, enhancements, sub_layouts |
| `stage_1_5b_context.json` | 1.5b | containers (design_budget 추가) |
| `stage_2_context.json` | 2 | generated_html |
| `stage_3_context.json` | 3 | (rendered_html은 final.html로 별도 저장) |
| `stage_4_context.json` | 4 | measurement, quality_score |
| `final_context.json` | 최종 | 전체 context |
- `parallel_cluster`
- `parallel_cluster_plus_visual`
- `compare_asymmetric_paired`
- `sequence_plus_visual`
- `single_block`
### HTML 시각화 (`steps/` 폴더)
| 파일 | Stage | 내용 |
|------|-------|------|
| `stage_0.html` | 0 | 섹션/팝업/이미지 목록 |
| `stage_1a.html` | 1A | 꼭지 테이블 (purpose, layer, 영역) |
| `stage_1b.html` | 1B | 꼭지 + source_data + summary |
| `stage_1_5a.html` | 1.5a | 빈 컨테이너 (1280×720) |
| `stage_1_5a_content.html` | 1.5a | 컨테이너에 콘텐츠 배치 |
| `stage_1_5b.html` | 1.5b | 디자인 예산 (available height/width) |
| `stage_1_7.html` | 1.7 | 블록 선택 표시 |
| `stage_1_8_fit_before.html` | 1.8 | 적합성 (재배분 전) |
| `stage_1_8_fit_after.html` | 1.8 | 재배분 후 + 보강 |
| `stage_1_8_blocks.html` | 1.8 | SLOT 구조 + 블록 디자인 + 주종관계 (1280×720) |
| `stage_2.html` | 2 | 영역별 Sonnet 출력을 실제 렌더링 (역할별 개별 확인) |
| `stage_3.html` | 3 | 영역을 합쳐 슬라이드 프레임에 배치한 결과 (1280×720 실제 렌더링) |
| `stage_4.html` | 4 | 측정 결과 + 품질 점수 |
### recipe
---
block 이름이 아니라 표현 규칙입니다.
## 핵심 원칙
예:
1. **콘텐츠가 구조를 결정** — 블록 CSS는 참고만. AI가 콘텐츠 전달 의도를 보고 HTML 구조 결정 (Phase R')
2. **하드코딩 금지** — font-size 외 모든 수치는 동적 계산. 어떤 MDX가 들어와도 동일하게 동작
3. **스크롤 절대 금지** — overflow:auto/scroll 어떤 영역에서도 불허
4. **Kei API 필수** — fallback 없음. 성공할 때까지 무한 재시도
5. **AI가 옵션 생성, Kei가 결정** — 공간 부족 시 하드코딩 대응이 아니라 Kei 판단 요청
6. **계산 먼저, AI 판단 나중에, 렌더링은 검증만**
7. **overflow 상태에서 출력 금지** — Vision 모델 품질 게이트 통과 필수
- `single_block`
- `two_col_text_visual`
- `stacked_summary_detail`
---
recipe는 이런 계약을 가질 수 있습니다.
## 블록 라이브러리 (38개)
- left/right kind
- top/bottom kind
- ratio
- vertical align
6개 카테고리, 38개 블록. 각 블록은 `catalog.yaml`에 용도(when), 금지(not_for), purpose_fit, schema(슬롯 정의)가 있음.
### block
| 카테고리 | 개수 | 용도 |
|---------|------|------|
| **headers** | 5 | 타이틀, 꼭지 헤더 |
| **cards** | 9 | 항목 나열, 카드 그리드 |
| **tables** | 3 | 비교표, 데이터 테이블 |
| **visuals** | 6 | SVG 다이어그램, 관계도 |
| **emphasis** | 10 | 강조, 인용, 결론, 불릿 |
| **media** | 5 | 이미지/사진 |
실제 구현 템플릿 후보입니다.
---
예:
## 기술 스택
- `prerequisites-3col`
- `process-product-2col`
- `compare-detail-gradient`
- `card-icon-desc`
| 역할 | 도구 |
|------|------|
| 서버 | FastAPI + uvicorn (포트 8001) |
| AI (Kei 실장/편집자) | Kei API → Opus (localhost:8000) |
| AI (HTML 생성) | Anthropic API → Claude Sonnet |
| AI (품질 검증) | Anthropic API → Claude Opus Vision |
| 블록 검색 | FAISS + bge-m3 |
| 템플릿 | Jinja2 (블록 디자인 레퍼런스용) |
| 렌더링 | CSS Grid + 디자인 토큰 (1280×720) |
| 렌더링 측정 | Selenium headless Chrome |
| SVG 시각화 | svg_calculator.py (N개 동적 배치) |
| 이미지 | Pillow (크기 측정) + base64 인라인 |
| 폰트 | Pretendard Variable |
| 공간 계산 | space_allocator.py + fit_verifier.py (결정론적) |
즉, "무슨 문서냐"가 아니라 "무슨 구조냐"를 먼저 읽고, 그 구조에 맞는 표현 규칙을 정한 뒤, 마지막에 구현 블록을 고르는 방향으로 가고 있습니다.
---
## popup / detail 계약
## 설치 및 실행
popup은 지금 다음 철학으로 정리되는 중입니다.
```bash
# 설치
cd design_agent
pip install -e .
- 메인 슬라이드에는 존 크기에 맞는 요약만 남긴다
- 큰 표, 시각 컴포넌트, 과다한 bullet은 상세 popup으로 분리한다
- 메인에서는 `자세히보기` 링크를 제공한다
# FAISS 인덱스 빌드 (블록 추가/수정 시)
python scripts/build_block_index.py
현재 popup 관련 핵심은 아래입니다.
# .env 설정
ANTHROPIC_API_KEY=sk-ant-...
KEI_API_URL=http://localhost:8000
LOG_LEVEL=DEBUG
```
- `PopupItem` 모델이 도입되어 popup 데이터를 명시적으로 다룸
- `popup_id`, `popup_file` 생애주기를 분리해 관리 중
- 최종 목표는 popup 판단을 휴리스틱이 아니라 명시적 contract로 만드는 것
```bash
# 터미널 1: Kei API (필수)
cd D:\ad-hoc\kei\persona_agent
python -m uvicorn backend.main:app --host 127.0.0.1 --port 8000
다만 아직 일부 구간엔 추측 로직과 이중 관리가 남아 있어, 이 부분은 계속 정리 중입니다.
# 터미널 2: Design Agent
cd D:\ad-hoc\kei\design_agent
python -m uvicorn src.main:app --host 127.0.0.1 --port 8001 --reload
```
## run 산출물 구조
접속: http://localhost:8001
각 실행은 `data/runs/{run_id}/` 아래에 저장됩니다.
---
주요 파일은 다음과 같습니다.
## 개선 이력
- `final.html`
- `final_context.json`
- `steps/*.html`
- popup/detail html
| Phase | 내용 | 상태 |
|-------|------|------|
| A~D | 슬라이드 품질 핵심 | 완료 |
| G~N | Kei API, 스토리라인, 정합성, 블록 선택, 비중, 측정 | 완료 |
| O | 컨테이너 기반 레이아웃 | 완료 |
| P | 다후보 렌더링 비교 | 완료 (20/100점 → 방향 전환) |
| Q | 제약 기반 블록 선택 | 완료 |
| R | 하이브리드 블록 (실패 — P=Q=R 동일 구조) | 실패 |
| R' | 블록 CSS 참고 + AI 구조 결정 | 설계 확정 |
| S | 검증 합격 프롬프트 + Claude HTML 생성 | 설계 확정 |
| T | 11-Stage 파이프라인 + 디자인 레퍼런스 | 완료 (31/31 통과) |
| V | 적합성 검증 + Kei 에스컬레이션 + 서브 컨테이너 | 완료 |
| W | Stage 2 출력 품질 수정 (6건) | 진행 중 |
### `final.html`
---
- 최종 렌더 결과
- 실제 눈으로 보는 산출물
## Kei Persona와의 관계
### `final_context.json`
```
Kei Persona Agent (localhost:8000)
├── Opus + RAG + 세션 컨텍스트
├── 도메인 지식 (건설/DX/BIM)
└── 대화/생성/피드백/실행 모드
- 각 단계 결과를 최종 context 형태로 저장
- block 선택, page_structure, measurement, quality_score 등을 확인할 때 가장 중요
### `steps/*.html`
- 단계별 디버그/설명용 보드
- 현재는 검토용으로 유용하지만, 일부 인코딩/설명 품질은 더 다듬을 필요가 있습니다
## 자주 봐야 하는 파일
### 파이프라인 핵심
- [src/pipeline.py](src/pipeline.py)
- [src/pipeline_context.py](src/pipeline_context.py)
- [src/section_parser.py](src/section_parser.py)
- [src/block_reference.py](src/block_reference.py)
- [src/block_assembler.py](src/block_assembler.py)
- [src/space_allocator.py](src/space_allocator.py)
### 템플릿 / 카탈로그
- [templates/catalog.yaml](templates/catalog.yaml)
- [templates/blocks/new/prerequisites-3col.html](templates/blocks/new/prerequisites-3col.html)
- [templates/blocks/redesign/process-product-2col.html](templates/blocks/redesign/process-product-2col.html)
- [templates/blocks/cards/compare-detail-gradient.html](templates/blocks/cards/compare-detail-gradient.html)
- [templates/blocks/slide-base.html](templates/blocks/slide-base.html)
주의: 현재 삭제/legacy 정리 여부를 별도로 확인해야 하는 경로입니다.
### 검증 / 측정
- [src/slide_measurer.py](src/slide_measurer.py)
- [src/validators.py](src/validators.py)
### 역사 / 계획 문서
- [PIPELINE.md](PIPELINE.md)
- [docs/history/PHASE-Y-PLAN.md](docs/history/PHASE-Y-PLAN.md)
- [ARCHITECTURE_OVERVIEW.md](ARCHITECTURE_OVERVIEW.md)
## 현재 상태 요약
### 잘 닫혀가는 것
- Type B 메인 경로
- `normalized.sections` 기반 구조 해석
- schema / recipe 기반 block selection
- `prerequisites-3col`, `process-product-2col` 같은 redesign 블록 자산화
- popup/detail 2단 표현 계약의 초안 연결
### 아직 정리 중인 것
- Type A 전체 안정화
- popup을 완전한 source of truth로 정리
- `tag_match``schema_match`의 완전한 동등 점수 비교
- step 보드 인코딩/설명 품질
- legacy 경로와 문서의 정리
## 읽는 방법 추천
프로세스를 빠르게 파악하려면 아래 순서가 좋습니다.
1.`README.md`
2. [src/pipeline.py](src/pipeline.py)
3. [src/section_parser.py](src/section_parser.py)
4. [src/block_assembler.py](src/block_assembler.py)
5. 최근 run의 `final_context.json`
히스토리까지 보려면 아래 문서를 이어서 보면 좋습니다.
- [PIPELINE.md](PIPELINE.md)
- [docs/history/PHASE-Y-PLAN.md](docs/history/PHASE-Y-PLAN.md)
Design Agent (localhost:8001, 이 프로젝트)
├── 슬라이드 생성 전용
├── Kei API로 꼭지 추출(1A) + 컨셉 구체화(1B) + 에스컬레이션(1.8) 호출
├── Sonnet으로 HTML 생성(Stage 2)
├── Opus Vision으로 품질 검증(Stage 4)
└── 두 프로젝트는 독립. 코드 공유 없음. API 연동만.
```

View File

@@ -1,105 +0,0 @@
# 45개 블록 BLOCK_SLOTS — design_director.py에 반영 필요
# 다른 쪽 작업 완료 후 교체
BLOCK_SLOTS = {
# headers/
"section-title-with-bg": {"required": ["title_ko"], "optional": ["title_en", "breadcrumb", "bg_image"]},
"section-header-bar": {"required": ["title"], "optional": ["subtitle"]},
"topic-left-right": {"required": ["title", "description"], "optional": []},
"topic-center": {"required": ["title"], "optional": ["subtitle", "description"]},
"topic-numbered": {"required": ["number", "title"], "optional": ["description", "color"]},
# cards/
"card-image-3col": {"required": ["cards"], "optional": []},
"card-text-grid": {"required": ["cards"], "optional": []},
"card-dark-overlay": {"required": ["cards"], "optional": []},
"card-tag-image": {"required": ["cards"], "optional": []},
"card-icon-desc": {"required": ["cards"], "optional": []},
"card-compare-3col": {"required": ["cards"], "optional": []},
"card-step-vertical": {"required": ["steps"], "optional": []},
"card-image-round": {"required": ["cards"], "optional": []},
"card-stat-number": {"required": ["stats"], "optional": []},
"card-numbered": {"required": ["items"], "optional": []},
# tables/
"compare-3col-badge": {"required": ["headers", "rows"], "optional": []},
"compare-2col-split": {"required": ["left_title", "right_title", "rows"], "optional": []},
"table-simple-striped": {"required": ["headers", "rows"], "optional": []},
# visuals/
"venn-diagram": {"required": ["center_label", "items"], "optional": ["center_sub", "description"]},
"circle-gradient": {"required": ["label"], "optional": ["sub_label"]},
"compare-pill-pair": {"required": ["left_label", "right_label"], "optional": ["left_sub", "right_sub"]},
"process-horizontal": {"required": ["steps"], "optional": []},
"flow-arrow-horizontal": {"required": ["steps"], "optional": []},
"keyword-circle-row": {"required": ["keywords"], "optional": []},
"layer-diagram": {"required": ["layers"], "optional": ["title"]},
"timeline-vertical": {"required": ["events"], "optional": []},
"timeline-horizontal": {"required": ["events"], "optional": []},
"pyramid-hierarchy": {"required": ["levels"], "optional": []},
# emphasis/
"quote-left-border": {"required": ["quote_text"], "optional": ["source"]},
"quote-big-mark": {"required": ["quote_text"], "optional": ["source"]},
"quote-question": {"required": ["question"], "optional": ["description"]},
"conclusion-accent-bar": {"required": ["conclusion_text"], "optional": ["label"]},
"comparison-2col": {"required": ["left_title", "left_content", "right_title", "right_content"], "optional": ["left_subtitle", "right_subtitle"]},
"banner-gradient": {"required": ["text"], "optional": ["sub_text"]},
"dark-bullet-list": {"required": ["bullets"], "optional": ["title"]},
"highlight-strip": {"required": ["segments"], "optional": []},
"callout-solution": {"required": ["title", "description"], "optional": ["icon", "source"]},
"callout-warning": {"required": ["title", "description"], "optional": ["icon"]},
"tab-label-row": {"required": ["tabs"], "optional": []},
"divider-text": {"required": ["text"], "optional": []},
# media/
"image-row-2col": {"required": ["images"], "optional": []},
"image-grid-2x2": {"required": ["images"], "optional": []},
"image-side-text": {"required": ["image_src"], "optional": ["image_alt", "title", "description", "bullets"]},
"image-full-caption": {"required": ["src"], "optional": ["alt", "caption"]},
"image-before-after": {"required": ["before_src", "after_src"], "optional": ["before_label", "after_label", "caption"]},
}
# _apply_defaults 용
BLOCK_DEFAULTS = {
"section-title-with-bg": {"title_ko": "(제목)"},
"section-header-bar": {"title": "(섹션)"},
"topic-left-right": {"title": "(소제목)", "description": ""},
"topic-center": {"title": "(제목)"},
"topic-numbered": {"number": "1", "title": "(단계)"},
"card-image-3col": {"cards": []},
"card-text-grid": {"cards": []},
"card-dark-overlay": {"cards": []},
"card-tag-image": {"cards": []},
"card-icon-desc": {"cards": []},
"card-compare-3col": {"cards": []},
"card-step-vertical": {"steps": []},
"card-image-round": {"cards": []},
"card-stat-number": {"stats": []},
"card-numbered": {"items": []},
"compare-3col-badge": {"headers": [], "rows": []},
"compare-2col-split": {"left_title": "A", "right_title": "B", "rows": []},
"table-simple-striped": {"headers": [], "rows": []},
"venn-diagram": {"center_label": "관계도", "items": [], "center_sub": "", "description": ""},
"circle-gradient": {"label": "(라벨)"},
"compare-pill-pair": {"left_label": "A", "right_label": "B"},
"process-horizontal": {"steps": []},
"flow-arrow-horizontal": {"steps": []},
"keyword-circle-row": {"keywords": []},
"layer-diagram": {"layers": []},
"timeline-vertical": {"events": []},
"timeline-horizontal": {"events": []},
"pyramid-hierarchy": {"levels": []},
"quote-left-border": {"quote_text": "(인용)"},
"quote-big-mark": {"quote_text": "(인용)"},
"quote-question": {"question": "(질문)"},
"conclusion-accent-bar": {"conclusion_text": "(결론)"},
"comparison-2col": {"left_title": "A", "left_content": "-", "right_title": "B", "right_content": "-"},
"banner-gradient": {"text": "(배너)"},
"dark-bullet-list": {"bullets": []},
"highlight-strip": {"segments": []},
"callout-solution": {"title": "(솔루션)", "description": ""},
"callout-warning": {"title": "(경고)", "description": ""},
"tab-label-row": {"tabs": []},
"divider-text": {"text": "구분"},
"image-row-2col": {"images": []},
"image-grid-2x2": {"images": []},
"image-side-text": {"image_src": ""},
"image-full-caption": {"src": ""},
"image-before-after": {"before_src": "", "after_src": ""},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 936 KiB

View File

@@ -0,0 +1,611 @@
# Phase Y: slide-base 기반 블록 조립 파이프라인 재설계
> **작성일:** 2026-04-14
> **목적:** assembler를 slide-base.html 기반으로 재작성. 블록 선택 → 배치 → 측정 → 조정 루프 완성.
> **근거:** Phase X-BX까지 assembler가 slide-base를 무시하고 HTML을 처음부터 생성. 블록 선택(1.7)이 조립(Stage 2)에 반영 안 됨. Stage 4 품질 점수 거짓말. 전체 파이프라인 연결 끊김.
---
## 핵심 전환
```
[이전] assembler가 HTML 전체를 하드코딩 생성. 블록 무시. slide-base 미사용.
[이후] slide-base.html 위에 tag 매칭된 블록을 배치. 사전계산 + 실측 조정 루프.
```
---
## 전체 흐름 (2026-04-15 재설계)
```
[1] slide-base.html 로드
├── title → .slide-title
├── 핵심요약(:::note) → .slide-footer (footer_text)
└── .slide-body (590px 가용) → 영역(zone)들이 여기에 배치됨
[2] Stage 1A: Kei 꼭지 추출 (영역 없이, 꼭지만)
├── Kei에게 zone/영역 판단을 시키지 않음
├── 꼭지별 title, purpose, layer, relation_type만 추출
└── 핵심요약은 conclusion_text로 분리
[3] 코드: MDX ## 파싱 → 꼭지-대목차 매핑
├── MDX에서 ## 대목차 목록 추출 (## 없는 도입부도 포함)
├── 각 꼭지의 source_data/title이 어느 ## 아래에 속하는지 매핑
└── 대목차별 꼭지 묶음 생성
[4] 코드: 대목차별 묶음으로 블록 tag 매칭 시도 (영역 확정 단계)
├── 묶음별 item_count + 꼭지 title → catalog tag 검색
├── 매칭됨 → 이 묶음 = 하나의 영역 (코드가 확정, 블록도 확정)
└── 매칭 안 됨 → Kei에게 이 꼭지들의 영역 판단 요청
├── "이 꼭지들을 어떻게 묶을지?"
├── "sidebar로 뺄 것이 있는지?" → Type A 결정
└── Kei는 주어진 꼭지 목록 안에서만 판단 (새 영역 이름 만들지 않음)
[5] 영역 확정 + 콘텐츠 소스 결정
├── 영역 제목 = MDX 원본 ## 제목 그대로
├── 영역 콘텐츠 = normalized.sections에서 MDX 원본 텍스트
├── Kei structured_text가 아님 (Kei는 구조 판단만)
└── sidebar 지정된 영역 → Type A / 나머지 → Type B
[6] 사전 계산: 영역별 비중 → 대략적 px 배정
└── 블록 후보 필터링 (너무 큰 블록 제외)
[7] .slide-body에 블록 배치 + 실측 조정 루프
├── 블록 템플릿 Jinja2 렌더링 (슬롯에 MDX 원본 텍스트 삽입)
├── slide-base + 블록 HTML 조합
├── Selenium 측정 (measure_mode: overflow:auto) → overflow 확인
├── overflow 시:
│ ├── ① font/padding 조정 (CSS 변수, 코드)
│ ├── ② 간격 축소
│ ├── ③ 텍스트 압축 (최후 수단, AI 1회)
│ └── 재측정 (최대 3회 루프)
└── overflow 없음 → 확정
[8] 최종 HTML 출력
└── slide-base + 확정된 블록들 = final.html (overflow:hidden)
[9] 품질 검증
├── Selenium overflow 측정
├── 비전 모델 평가 (가능할 때만)
└── 미평가 시 -1 (거짓말 안 함)
```
### 핵심 변경 (이전 대비)
| 항목 | 이전 (문제) | 이후 (재설계) |
|------|-----------|-------------|
| 영역/zone 결정 | Kei가 zone 구조를 만듦 → 매번 다름, 오인 | 코드가 ##파싱 + 블록매칭으로 먼저 확정. 안 되는 것만 Kei |
| 콘텐츠 소스 | Kei structured_text (재구성) | MDX 원본 (normalized.sections) |
| 영역 제목 | Kei가 축약 ("필수요건") | MDX 원본 ## 제목 그대로 |
| Kei 역할 | 꼭지+영역+zone+텍스트 전부 | 꼭지 추출 + 성격 판단(sidebar/팝업)만 |
| 블록 매칭 시점 | 영역 확정 후 | 영역 확정 전 (블록이 영역을 결정) |
---
## 태스크 목록
### Y-1: Kei 프롬프트 — zone = 대목차 (완료)
- [x] zone = `##` 대목차 단위, `###` 소목차 = zone 안 블록
- [x] 2단계 판단: 먼저 zone 잡고 → 용어정의 있으면 Type A
- [x] 결론/핵심요약 = slide-base footer (별도 zone 아님)
- [ ] Kei 프롬프트에서 결론 zone 제거 (page_structure에서 제외)
### Y-2: space_allocator — zone=bottom 지원
- [ ] `bottom` zone 전체폭 처리 추가
- [ ] Type A/B에 따라 .slide-body 내 레이아웃 분기
- [ ] 결론 zone 미생성 (slide-base footer로 처리)
### Y-3: block_reference — tag 매칭 강화
- [x] tag 기반 0순위 매칭 추가
- [x] item_count 범위("2-3") 처리
- [ ] content_pattern 가중치 추가 (item_count만으로 매칭 방지)
- [ ] 결론 role 매칭 제외 (slide-base가 처리)
### Y-4: assembler 재작성 — slide-base 기반
- [ ] slide-base.html 로드 → Jinja2 렌더링
- [ ] title, footer_text(핵심요약) 삽입
- [ ] .slide-body에 zone별 블록 HTML 삽입
- [ ] render_block_for_role()로 블록 렌더링
- [ ] 블록 CSS를 slide-base <style>에 합침
- [ ] 기존 하드코딩 assembler 제거 (fallback 아님)
### Y-5: 사이즈 조정 루프
- [ ] 사전 계산: zone 비중 → px 배정 → 블록 필터링
- [ ] 실측 조정: Selenium 측정 → overflow → font/padding 조정
- [ ] 루프 최대 3회
- [ ] overflow 해소 안 되면 텍스트 압축 (AI 1회)
### Y-6: Sonnet redesign 경로
- [ ] tag 매칭 실패 시 유사 블록 선택
- [ ] Sonnet에 블록 HTML + 콘텐츠 구조 전달 → 블록 단위 redesign
- [ ] redesign 결과를 templates/blocks/redesign/에 저장
- [ ] catalog.yaml, INDEX.md 자동 업데이트
### Y-7: step_visualizer 정합성
- [ ] 각 stage step HTML이 실제 파이프라인 데이터와 일치
- [ ] stage_1_8_blocks → 실제 블록 렌더링 (샘플 아님)
- [ ] stage_2 → slide-base + 블록 조합 결과 표시
### Y-8: Stage 4 품질 검증
- [x] 비전 실패 시 -1 (거짓말 방지)
- [x] 비전 미평가 시 차단 안 함, 경고만
- [ ] Selenium overflow 검사 정확도 검증
### Y-9: 검증 — MDX 03 end-to-end
- [ ] MDX 03 파이프라인 실행
- [ ] 각 stage 전후 데이터 연결 확인
- [ ] 최종 결과물이 85점 수준인지 시각 확인
---
## 의존 관계
```
Y-1 (Kei 프롬프트) ──→ Y-2 (space_allocator) ──→ Y-3 (block_reference)
Y-4 (assembler 재작성)
Y-5 (사이즈 루프)
Y-6 (Sonnet redesign)
Y-7 (step_visualizer)
Y-8 + Y-9 (검증)
```
## 블록 tag 업데이트 (별도 작업)
- 사용자가 다른 클로드와 진행
- catalog.yaml에 tags 필드 추가
- content_pattern, item_count, content_example 등
- Phase Y와 병렬 진행 가능
---
## 2026-04-14 진행 결과
### 완료된 것
| 태스크 | 상태 | 비고 |
|--------|------|------|
| Y-1 Kei 프롬프트 | ✅ | zone=대목차(##) 단위. 결론=conclusion_text 필드. page_structure에서 제거. |
| Y-2 space_allocator | ✅ | zone=bottom 전체폭 지원. 결론 zone 미생성. slide-body 590px 기준. |
| Y-3 block_reference | ✅ | tag 매칭 0순위. item_count+content_example AND 조건. 결론 매칭 제외. |
| Y-4 assembler | ✅ | slide-base.html 로드 → zone별 블록 렌더링 → .slide-body에 배치. measure_mode 분리. |
| Y-7 step_visualizer | ✅ | 1_5a~1_8 모두 slide-base 기반 _wrap(). _hdr/_box/_calc_coords 제거. |
| Y-8 Stage 4 | ✅ | 비전 실패 시 -1. 거짓말 방지. |
| validator | ✅ | 결론 zone 필수 검증 제거. |
| slide_measurer | ✅ | zone- 클래스 감지 추가 (area-만 보던 것 수정). |
### 검증된 것 (MDX 03 파이프라인 실행)
```
Kei → zone 2개 (필수요건[1,2,3] + 혁신과변화[4,5]) + conclusion_text ✅
space_allocator → top 295px + bottom 287px (합 582px, gap 8px = 590px) ✅
block_reference → prerequisites-3col (tag_match) + compare-detail-gradient (tag_match) ✅
assembler → slide-base + 블록 렌더링 → .slide-body에 배치 ✅
Selenium → zone별 overflow 감지 (bottom +190px) ✅
Stage 4 → -1 미평가 ✅
```
### 근본 설계 오류 (2026-04-14 발견)
**근본 오류: 콘텐츠 소스가 잘못됨**
- 현재: Kei가 structured_text를 재구성 → assembler가 이걸 블록에 넣음
- 올바른 방향: **assembler는 normalized.sections(MDX 원본 텍스트)에서 직접 가져옴**
- Kei 역할: 구조 판단만 (zone 분류, 팝업 분리, 블록 선택). **텍스트 재구성 안 함.**
- zone 제목도 Kei가 줄인 role_name이 아니라, MDX 원본 `##` 제목을 그대로 사용
- 데이터 흐름 변경:
```
[현재] MDX → Kei structured_text(재구성) → assembler
[올바름] MDX → normalized.sections(원본) → assembler
Kei → 구조 판단(zone/블록/팝업) → assembler에 지시만
```
- 수정 대상:
1. assembler: `topic.structured_text` 대신 `normalized.sections`에서 원본 텍스트 가져오기
2. zone 제목: `role_name` 대신 `normalized.sections`의 `##` 제목 사용
3. Kei의 topic_ids로 어떤 section이 어떤 zone에 가는지 매핑
### 미해결 오류 (수정 중)
**오류 1: 블록 색상이 Figma 원본과 다름** → nth-child로 수정 완료 (인라인 style 제거)
**오류 2: ### 마크다운 헤더 그대로 노출** → _parse_topic_to_items에서 ### 스킵 수정 완료
**오류 3: 글씨 크기 px 고정** → CSS 변수 전환 수정 완료
**오류 4: slide-base HTML 주석이 출력에 노출** → re.sub로 주석 제거 수정 완료
**오류 5: zone-bottom 중복 (body가 2번 삽입)** → 주석 안 {% block body %} 제거로 수정
**오류 6: zone 제목이 Kei 축약본** → 근본 오류. normalized.sections에서 원본 제목 가져와야 함
### 미해결 오류 (원래 3건, 추가 발견 포함)
**오류 1: 블록 색상이 Figma 원본과 다름** (수정 완료)
- prerequisites-3col 블록: 3열이 각각 다른 색(파랑/금/초록)이어야 하는데 전부 같은 색
- 원인: 블록 템플릿 CSS의 `|default()` 값이 단일 색상(파랑)만 있음
- assembler에서 색상을 전달하는 방식은 잘못됨
- 해결: **블록 템플릿 CSS 자체에 열별 색상을 가지고 있어야 함** (`:nth-child(1)`, `:nth-child(2)`, `:nth-child(3)`로 각각 다른 색)
- 또는 catalog.yaml에 블록 디자인 속성(색상 팔레트)을 정의하고 assembler가 읽어서 전달
- assembler는 콘텐츠만 전달. 디자인은 블록이 가지고 있거나 catalog에서 오는 것.
**오류 2: 하단 zone에 `### 과정의 혁신`, `### 결과의 변화` 마크다운 헤더가 그대로 노출**
- `_parse_topic_to_items()`에서 `### ` 접두사를 heading으로 포함시킴
- topic.title이 이미 "과정(Process)의 혁신"인데, structured_text 첫 줄에 또 `### 과정(Process)의 혁신`이 있음
- 해결: `_parse_topic_to_items()`에서 `### `으로 시작하는 줄은 무시 (topic.title과 중복)
- 또는 structured_text 파싱 시 `### ` 접두사 제거
**오류 3: 블록 글씨 크기가 하드코딩 (px 고정)**
- 블록 템플릿 CSS에 `font-size: 27px`, `font-size: 21px` 등 Figma 원본 크기가 고정
- 컨테이너 크기에 따라 font가 조정되어야 하는데 고정이라 overflow 발생
- 해결 방향:
1. 블록 CSS에서 font-size를 CSS 변수(`var(--block-font-heading)`)로 변환
2. assembler가 zone 크기에 따라 CSS 변수 값을 계산하여 전달
3. overflow 루프에서 CSS 변수를 줄여가며 재측정
- 이것이 Y-5(사이즈 조정 루프)의 핵심
### 미해결 프로세스
**overflow 재배분 루프가 실질적으로 안 동작**
- Selenium이 overflow +190px를 감지했지만, 재배분할 surplus zone이 없음 (top도 꽉 참)
- 재배분만으로는 해결 안 됨 → font/padding 조정이 필요
- 현재 파이프라인에 font 조정 로직 없음
- Y-5에서 구현 필요:
1. overflow 감지 → font-size 1~2px 줄이기 → 재렌더링 → 재측정
2. 최대 3회 루프
3. 그래도 안 되면 텍스트 압축 (AI 1회)
**Sonnet redesign 경로 (Y-6) 미구현**
- tag 매칭 실패 시 유사 블록 기반 Sonnet redesign → templates/blocks/redesign/에 저장
- 현재는 tag 매칭 실패 시 기존 relation_type 방식으로 fallback
**콘텐츠 소스 전환 (Y-10)** → ✅ 완료 (2026-04-15)
- normalized.sections를 단일 소스로 확정
- section_parser: raw MDX 파싱 → normalized.sections 기반으로 변경
- assembler: _find_section_content() → normalized.sections에서 가져옴
- D1:/D2: 포맷 파싱 지원 추가
- Kei structured_text 의존 제거
---
## 2026-04-15 진행 결과
### 구조 안정화 1차 완료
| 항목 | 상태 | 비고 |
|------|------|------|
| process wiring | ✅ 복구됨 | normalized.sections → section shape → block assignment → slide-base → final html |
| overflow control | ✅ 통과 | Selenium zone별 측정 동작, overflow 0 (최신 run) |
| single source of truth | ✅ 확정 | normalized.sections (Stage 0). raw MDX 직접 사용 안 함. |
| layout_template 결정 | ✅ 코드 | sidebar 유무로 A/B 자동 결정 (Kei 의존 아님) |
| 영역 확정 | ✅ 코드 | sub_titles 기반 블록 매칭 → 영역 확정 (Kei zone 판단 제거) |
| validator 정리 | ✅ | page_structure 검증을 section_parser 후로 이동. Type B purpose 모순 = 경고만. |
| **semantic block matching** | ❌ 미해결 | prerequisites-3col 대신 category-strip-table 선택됨 |
| **slot filling** | ❌ 미해결 | 블록 slot에 텍스트가 비어있음 (껍데기만) |
| quality gate | ⚠ 부분 | vision 404. Selenium만 동작. |
### 핵심 미해결: 블록 매칭 정확도 + slot 채움
**1. 블록 매칭이 엉뚱한 블록을 선택**
- top: prerequisites-3col(원하는 것) → category-strip-table(선택됨)
- bottom: compare-detail-gradient(원하는 것) → dark-bullet-list(선택됨)
- 원인: tag 매칭 점수에서 category-strip-table이 더 높은 점수를 받음
- 해결: tag 매칭 점수 로직 보정 필요
- prerequisites-3col의 content_example에 "기술/사람/자연"이 정확히 매칭되면 최우선
- compare-detail-gradient의 content_example에 "과정/결과"가 매칭되면 최우선
**2. slot 채움이 비어있음**
- category-strip-table 블록의 slot에 기술/사람/자연 데이터가 안 들어감
- 원인: _build_slot_data()가 sub_title별로 normalized.sections에서 content를 찾는데,
sub_title "기술(디지털)"의 content가 대목차 "DX 시행을 위한 필수 요건"의 합친 content에 있어서
개별 sub_title별로 분리 안 됨
- 해결: normalized.sections에서 sub_title별 개별 content를 직접 가져와야 함
(major_sections의 합친 content가 아니라, 개별 level=2 section)
**3. section shape와 topic 매핑 불일치**
- bottom의 topic_ids=[4]만 있음 (5가 빠짐)
- 원인: Kei가 꼭지를 매번 다르게 만들어서 매핑이 흔들림
- 해결: topic 매핑도 sub_titles 기반으로 안정화 필요
### 추가 발견 (2026-04-15 후반)
**블록 매칭은 해결됨 (sub_titles 기반 + min_height 감점 제거):**
- top: prerequisites-3col ✅ (sub_titles 3개 매칭)
- bottom: compare-detail-gradient ✅ (sub_titles 2개 매칭)
**하지만 결과물 품질이 안 맞음:**
1. 결론 텍스트 3번 중복 (블록 slot + footer)
2. bottom 좌측 텍스트 안 보임 (D1 only → desc 빈 배열)
3. 텍스트 중복 렌더링
4. zone height px 고정 (참고는 % 기반)
5. font_scale 방식으로 글자 과도 축소 (참고는 글자 크기 고정)
6. 블록이 zone height:100%로 안 채워짐
7. weight가 content 글자 수 기준 (중요도가 아님)
8. bottom 블록으로 cdg가 선택됐지만, 참고 결과물은 pp2 사용
**근본 원인:**
- cdg로도 가능하지만, pp2가 이 콘텐츠 구조(비대칭: 표+불릿 vs 불릿)에 더 적합
- pp2는 catalog 미등록 → tag 매칭 불가
- 블록 글씨 크기가 Figma 원본(18px) → 슬라이드 적용(12px) 변환 필요
- 블록 payload schema가 확정 안 됨 → slot 데이터가 제대로 안 채워짐
- fit 루프가 구조/payload 이전에 실행돼서 의미 없음
---
## Phase Y-11: 블록 자산 → payload → layout → fit → 검증 (2026-04-15 확정)
> **원칙:** 구조 → payload → layout → fit 순서. 하드코딩 금지. 전체 프로세스 구조 속에서 정리.
> **핵심:** "프로세스가 결과를 만드는" 구조. 결과를 보고 프로세스를 땜질하지 않음.
### 전체 파이프라인 (Y-11 반영)
```
[1] slide-base.html 로드 (title + footer)
[2] Kei: 꼭지 추출만 (영역/zone 안 함)
[3] 코드: normalized.sections → 대목차 추출 → 꼭지 매핑
[4] 코드: shape 기반 block selection
sub_titles 수 + 구조 타입 → catalog tag 매칭 → 블록 확정
[5] payload 조립
블록별 payload schema에 맞게 normalized data 변환
[6] payload contract 검증 ← 새로 추가
필수 slot 비어있지 않은지, 결론이 body에 안 섞였는지
[7] layout 조립
zone % 기반 + block height:100% + 고정 글씨 크기
[8] fit 루프
overflow → padding/spacing 먼저 → 팝업 분리 → font 축소(최후)
[9] 최종 검증
block 선택 + 글자 누락 + 결론 위치 + overflow + 밀도
```
### Y-11 태스크 목록
**[Y-11a] pp2 블록 자산 정리**
- BEPs/process-product-2col.html → blocks/redesign/ 이동
- slot 구조: left_title, right_title, left_compare(asis/tobe), left_sections[], right_sections[]
- 블록 목적, 사용 조건, 미사용 조건 문서화
- HTML/CSS의 글씨 크기를 슬라이드 적용값(header:13px, mid:12px, body:11px)으로 확정
**[Y-11b] pp2 catalog.yaml 등록 + tag**
- content_pattern: "2-section-asymmetric-compare-table-and-bullets"
- item_count: 2
- content_example에 구조 설명 (문서명 하드코딩 금지)
- slide_font 필드에 슬라이드 적용 글씨 크기 기록
**[Y-11c] block selection 규칙 shape 기반 재정의**
- sub_titles 3개 + 병렬 → prerequisites-3col
- sub_titles 2개 + 비대칭(표+불릿) → pp2
- sub_titles 2개 + 대칭 비교 → cdg
- sidebar/reference → 해당 전용 블록
- pipeline Phase Y + Stage 1.7 block_reference 일관 적용
**[Y-11d] pp2 payload schema + 파이프라인 연결**
- payload 구조 확정 (left_title, right_title, left_compare, left/right_sections)
- _build_slot_data()에서 블록별 payload 생성
- D1/D2 → payload schema 변환 규칙 (D1 only 표 복원 포함)
**[Y-11e] 본문 데이터 정리 규칙**
- 결론/핵심요약 → footer 전용, 블록 payload에 절대 안 섞기
- 표 → left_compare.left_items/right_items
- 불릿 → sections[].bullets
- D1 only 평탄화 → 표 구조 복원
**[Y-11f] zone wrapper 정리**
- zone height: % 기반 (px 아님)
- block wrapper: height:100%
- zone 제목: 13px, margin-bottom:8px
- zone 간 여백: margin-bottom:1%
- 글씨 크기: catalog slide_font 값 (font_scale 아님)
**[Y-11g] 블록 내부 typography 규칙**
- prerequisites-3col: heading 12px, desc 11px, vlabel 14px
- pp2: header 13px, mid_title 12px, body 11px
- bullet 기호, padding-left, text-indent, line-height
- catalog.yaml slide_font에 기록, assembler가 읽어서 적용
**[Y-11h] payload contract 검증 (fit 전 게이트)** ✅ 구현
- 필수 slot 비어있지 않은지
- conclusion이 body payload에 안 들어갔는지
- 같은 데이터가 compare와 section에 중복 주입 안 되는지 ← 추가 필요
---
### Y-11 1차 검증 결과 (2026-04-15, run 20260415_091309)
**block selection: ✅ 통과**
- top: prerequisites-3col (tag_match)
- bottom: process-product-2col (tag_match)
- overflow: top 0, bottom 0
- font_scale: 1.0 (축소 안 함)
**payload/contract/layout: ❌ 미완**
아래 항목들이 남아있음 → Y-12로 정리:
### Phase Y-12: payload 정제 → contract → layout → asset → validation
> block selection은 통과. 이제 "선택된 블록에 정확한 데이터를 정확한 형태로 넣는" 단계.
**[Y-12a] payload normalization (정제)**
- `**` `****` 마크다운 잔여 토큰 제거 (final 출력 전 cleanup gate)
- top desc: `/`로 이어진 plain text → `<div class="bul">• ...</div>` 불릿 구조
- 표 잔여 토큰 (D1: As-is, D1: 구분 등) 정리
- `[핵심요약: ...]` stray note 제거
**[Y-12b] block contract assembly (중복 제거)**
- left_compare에 들어간 데이터가 left_sections에 다시 들어가지 않게
(현재: Analogue 기반 업무의 Digital화가 compare 제목 + section mid-title에 중복)
- compare → left_sections 분리 규칙: 표 항목은 compare에만, 나머지는 sections에만
- pp2 좌우 section packing 규칙 고정
**[Y-12c] layout/style contract** — 공통 레이아웃 계약 + 블록별 내부 contract
공통 들여쓰기 계층 (모든 슬라이드에 적용):
```
대제목 (slide-base) ← left: 52px (고정)
중제목 (zone 제목) ← 대제목과 같은 시작선 (padding-left: 12px)
블록 wrapper ← 중제목보다 안쪽 (padding: 0 12px 0 24px)
소제목 ← 블록 내부 기준선
불릿 ← 소제목보다 안쪽
두번째줄 ← 첫줄 문장 시작선 정렬 (hanging indent: padding-left:14px; text-indent:-14px)
```
블록별 내부 contract:
- p3c: bar 56px, vlabel-area 56px, section left:60px
- pp2: display:flex 좌/우 병렬, 소제목 행 정렬, body padding 6px 16px
- 불릿: `.bul`, `.pp2-body-text`, `.cdg-bullet` 공통 hanging indent
**[Y-12d] asset packaging**
- 배경 텍스처 (svg/bg_slide_texture.png) → base64 내장 또는 data URI
- 화살표 이미지 (arrow) → base64
- final.html이 단독으로 열어도 asset 깨지지 않게
**[Y-12e] final validation**
- 글자 누락 없음
- markdown residue (`**`, `****`) 없음
- 본문 중복 없음 (compare/section 중복)
- 결론이 footer에만 있음
- 좌우 정렬 이상 없음
- asset 깨짐 없음
- overflow 없음
**[Y-12f] 파이프라인 실행 + 참고 비교**
- MDX 03 실행 → ✅ 완료 (run 20260415_105516, 텍스트 누락 없음)
- mdx03_final/final.html과 비교
- 하드코딩 없이 프로세스로 도달한 결과인지 확인
---
## Phase Y-13: Group Schema 계층 (fit 블록 없을 때의 프로세스)
> **근거:** MDX 02를 돌렸을 때, fit한 블록이 없어서 venn-diagram 같은 엉뚱한 fallback으로 빠짐.
> fit 블록이 있으면 Y-11~12 프로세스로 충분. 없을 때 "바로 fallback 아무거나"가 아니라
> **중목차 → 소목차 관계 판단 → group schema → 블록 선택/조합** 경로가 필요.
### 핵심 프로세스
```
[1] section group 추출
## 중목차 기준으로 하나의 section group으로 묶기
(이미 extract_major_sections()가 sub_titles를 제공)
[2] group relation classifier
같은 중목차 안의 소목차들의 관계 판단:
- 병렬 목표 (안전/생산성/소통) → parallel_3
- 비교 (과정혁신/결과변화) → compare_2
- 순서/프로세스 → process_list
- 독립 카드 → independent_cards
- 보조 설명 → summary_with_visual
Kei가 판단 지원 (코드만으로 어려움)
[3] group schema enum
relation → schema 변환:
- parallel_3 → 3열 카드/표/요약
- compare_2 → 2열 비교
- process_list → 단계/변화 목록
- summary_with_visual → 텍스트+이미지
[4] schema → block matcher
schema에 맞는 블록을 catalog에서 찾기
├── 정확히 맞는 블록 있음 → 사용
├── 유사 블록 → Sonnet redesign → blocks/redesign/ 저장
└── 없음 → composition (메인 블록 + 보조 블록 조합)
fallback 아무거나 금지
[5] payload 조립 (group schema 기준)
group schema → block payload 변환
이미지 포함 여부, bullet 정리, 표 축약, popup 분기
[6] layout / fit
zone 비율, padding, indent, overflow, popup fallback
```
### Y-13 진행 결과 (2026-04-15)
**Y-13a~d: 구현 완료**
구현된 것:
- `classify_group_relations()`: D1: 개수로 병렬 항목 감지 + 키워드 기반 schema 분류
- schema 세분화: `compare_asymmetric_2col` (표+비대칭), `process_plus_visual` (불릿+시각) 추가
- 기존 `process_list` 유지 (삭제 안 함, 점진적 추가)
- `GROUP_SCHEMA_BLOCK_MAP`: 새 schema → 블록 후보 매핑 추가
- pipeline + block_reference에서 tag 실패 시 schema 후보로 선택
**MDX 03 회귀 검증 ✅:**
```
top: parallel_3 → prerequisites-3col ✅ (이전과 동일)
bottom: compare_asymmetric_2col → process-product-2col ✅ (pp2 유지)
```
**MDX 02 현재 상태:**
```
top: parallel_3_with_image → prerequisites-3col (이미지 배치 미해결)
bottom: compare_2 → compare-detail-gradient (<DxEffect> 감지 안 됨 → process_plus_visual 미적용)
```
### 미해결 + 방향
**1. tag_match와 schema_match 동등 비교 (향후)**
- 현재: tag 실패 시에만 schema fallback
- 향후: 둘 다 점수화해서 동등 비교
- schema_match를 block selection의 1급 기준으로 승격 예정
**2. GROUP_SCHEMA_BLOCK_MAP → 선언형 이동 (향후)**
- 현재: section_parser.py에 dict 하드코딩
- 향후: catalog.yaml 또는 별도 schema 파일로 선언형 관리
**3. Kei는 보조 힌트**
- 구조적 근거(D1: 수, sub_titles 수, 키워드) = 1순위
- Kei = 보조 의미 힌트 (하드 의존 안 함)
**4. composition 경로 (향후)**
- 단일 블록으로 안 되는 경우에만
- 지금은 먼저 단일 블록 경로를 정교화
- composition은 검증 결과를 보고 필요한 경우에만 추가
**5. MDX 02 bottom 분류 정교화** → ✅ 해결
- `<DxEffect />`가 normalized에서 제거됨 → sub_titles 키워드("기대효과")로 분기
- content + sub_text 합쳐서 키워드 검색하도록 수정
- MDX 02 bottom: `process_plus_visual` → checklist-dark 후보
**6. section_parser 책임 분리 (향후)**
- 현재: group 추출 + relation 분류 + schema enum + block 후보 다 있음
- 향후: group_schema.py 별도 모듈로 분리 예정
**7. 메인/popup 2단 표현 계약 (파이프라인 공통)**
- 콘텐츠가 zone에 다 안 들어갈 때의 공통 규칙
- MDX의 `<DxEffect />` 같은 시각 컴포넌트, 큰 표, 과다 불릿 대응
- 구조:
```
메인 HTML (zone 안):
- 존 크기에 맞는 요약형 (2~3행 요약, 핵심 포인트)
- "자세히보기" 링크/버튼
popup HTML (별도 파일):
- 전체 표, 전체 bullet, 컴포넌트 원형 구조
- 예: detail_dx_effect.html (run 폴더 안)
```
- popup 분기 조건:
- 표가 크다 (행 5개 이상)
- 시각 컴포넌트가 있다 (`<DxEffect />` 등)
- 존 높이에 안 맞는다 (overflow)
- 본문에 넣으면 가독성이 깨진다
- 검증 규칙:
- popup으로 보낸 경우 본문엔 최소 요약이 남아 있어야 함 (빈칸 금지)
- 링크 대상 파일이 실제 생성돼 있어야 함
- Astro 컴포넌트 연결:
- `<DxEffect />` → `samples/src/components/dx.astro`에 연결
- 파이프라인은 Astro를 실행하지 않음
- dx.astro를 읽어서 HTML로 변환 → popup 파일로 생성
- 메인에는 요약형 카드 + 자세히보기 링크
- fit 루프와의 관계:
- overflow 발생 → font 축소(최후) 전에 popup 분리를 먼저 시도
- 순서: padding 조정 → popup 분리 → font 1단계 축소
**8. tag_match / schema_match 동등 비교 (향후)**
- 현재: tag 실패 시에만 schema fallback
- 향후: 둘 다 점수화해서 동등 비교
- schema_match를 block selection의 1급 기준으로 승격
### 의존 관계
```
Y-12 (payload/layout) → Y-13 (group schema) → Y-14 (popup 2단 표현)
MDX 03: Y-12로 충분 ✅ (fit 블록 있음, 회귀 검증 통과)
MDX 02: Y-13 분류 완료 ✅, 블록 선택 후 popup 경로 필요 (Y-14)
MDX 01: Type A — 별도 작업
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,806 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>건설정보모델링(BIM)</title>
<style>
/* Design Agent — 디자인 토큰 */
/* CLAUDE.md에 정의된 디자인 원칙을 CSS 변수로 구현 */
:root {
/* 색상 */
--color-primary: #1e293b;
--color-accent: #2563eb;
--color-neutral: #64748b;
--color-bg: #ffffff;
--color-bg-subtle: #f8fafc;
--color-border: #e2e8f0;
--color-danger: #dc2626;
--color-success: #16a34a;
--color-text: #1e293b;
--color-text-secondary: #64748b;
--color-text-light: #94a3b8;
/* 폰트 크기 */
--font-title: 2rem;
--font-subtitle: 1.25rem;
--font-body: 0.95rem;
--font-caption: 0.8rem;
--font-small: 0.7rem;
/* 폰트 두께 */
--weight-normal: 400;
--weight-medium: 500;
--weight-bold: 700;
--weight-black: 900;
/* 여백 */
--spacing-page: 40px;
--spacing-block: 20px;
--spacing-inner: 16px;
--spacing-small: 8px;
/* 기타 */
--radius: 6px;
--border-width: 1px;
--accent-border: 3px;
--line-height-ko: 1.7;
}
/* Design Agent — 기본 슬라이드 스타일 */
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 슬라이드 컨테이너: 16:9 고정 비율 */
.slide {
width: 1280px;
height: 720px;
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--color-bg);
font-family: 'Pretendard Variable', 'Pretendard', 'Noto Sans KR', sans-serif;
color: var(--color-text);
font-size: var(--font-body);
line-height: var(--line-height-ko);
word-break: keep-all;
padding: var(--spacing-page);
display: grid;
gap: var(--spacing-block);
}
/* 슬라이드 제목 */
.slide-title {
font-size: var(--font-title);
font-weight: var(--weight-black);
color: var(--color-primary);
border-bottom: var(--accent-border) solid var(--color-accent);
padding-bottom: var(--spacing-small);
}
/* 섹션 제목 */
.section-title {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
color: var(--color-primary);
margin-bottom: var(--spacing-small);
}
/* 본문 */
.body-text {
font-size: var(--font-body);
color: var(--color-text);
line-height: var(--line-height-ko);
}
/* 캡션/출처 */
.caption {
font-size: var(--font-caption);
color: var(--color-text-light);
font-style: italic;
}
/* 강조 텍스트 */
.highlight {
color: var(--color-accent);
font-weight: var(--weight-bold);
}
/* 경고/문제 강조 */
.danger {
color: var(--color-danger);
font-weight: var(--weight-bold);
}
</style>
<style>
.slide-1 {
grid-template-areas: 'header' 'topic1' 'topic2' 'images' 'mountain' 'compare' 'table' 'circle' 'cards';
grid-template-columns: 1fr;
grid-template-rows: 500px auto auto 354px auto auto auto auto auto;
}
.slide-1 .area-header {
grid-area: header;
}
.slide-1 .area-topic1 {
grid-area: topic1;
}
.slide-1 .area-topic2 {
grid-area: topic2;
}
.slide-1 .area-images {
grid-area: images;
}
.slide-1 .area-compare {
grid-area: compare;
}
.slide-1 .area-table {
grid-area: table;
}
.slide-1 .area-circle {
grid-area: circle;
}
.slide-1 .area-cards {
grid-area: cards;
}
/* 다중 페이지: 페이지 간 간격 */
.slide + .slide {
margin-top: 40px;
}
/* 인쇄 시 페이지 분리 */
@media print {
.slide {
page-break-after: always;
}
.slide + .slide {
margin-top: 0;
}
}
</style>
</head>
<body>
<div class="slide slide-1">
<div class="slide-title" style="grid-area: header;">건설정보모델링(BIM)</div>
<div class="area-header">
<!-- 섹션 타이틀: 배경 헤더 위 영문+한글 타이틀 오버레이 -->
<!--
📋 section-title
─────────────────
용도: 자세히보기 페이지 상단, 배경 이미지 위에 타이틀 표시
슬롯: title_ko (필수), title_en (선택), breadcrumb (선택), bg_image (선택)
Figma 원본: 공통 > section_title + bg 컴포넌트
-->
<div class="block-section-title">
<img class="st-bg" src="figma-assets/bg_header.png" alt="">
<div class="st-breadcrumb">건설산업에서의 디지털전환 BIM</div>
<div class="st-text">
<div class="st-en">Building Information Modeling</div>
<div class="st-ko">건설정보모델링(BIM)</div>
</div>
</div>
<style>
.block-section-title {
position: relative;
width: 100%;
height: 500px;
overflow: hidden;
}
.st-bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.st-bg-default {
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 50%, #4dc4ff 100%);
}
.st-breadcrumb {
position: absolute;
top: 18px;
left: 89px;
z-index: 5;
font-size: 13px;
color: rgba(255,255,255,0.7);
}
.st-text {
position: absolute;
bottom: 40px;
left: 89px;
z-index: 5;
}
.st-en {
font-size: 15px;
font-weight: var(--weight-normal, 400);
color: #ffffff;
opacity: 0.85;
margin-bottom: 4px;
}
.st-ko {
font-size: 35px;
font-weight: var(--weight-bold, 700);
color: #ffffff;
line-height: 1.3;
}
</style>
</div>
<div class="area-topic1">
<!-- 꼭지 제목+설명: 좌측 질문/소제목 + 우측 설명 -->
<!--
📋 topic-header
─────────────────
용도: 각 꼭지의 시작부, 좌측에 파란 굵은 제목 + 우측에 본문 설명
슬롯: title (필수), description (필수)
비율: 좌 240px : 우 나머지
Figma 원본: sub_제목,내용 (742x68~78)
-->
<div class="block-topic-header">
<div class="th-title">단순 BIM의 적용이 D/X가 아닙니다</div>
<div class="th-desc">BIM은 건설산업의 디지털전환을 수행하는 과정에서 가장 기초가 되는 일부분임을 인지하는 것이 매우 중요합니다.</div>
</div>
<style>
.block-topic-header {
display: flex;
gap: 20px;
padding: 12px 0;
}
.th-title {
width: 240px;
flex-shrink: 0;
font-size: 24px;
font-weight: var(--weight-bold, 700);
color: var(--color-accent-deep, #004cbe);
line-height: 1.4;
word-break: keep-all;
}
.th-desc {
flex: 1;
font-size: 16px;
font-weight: var(--weight-normal, 400);
color: var(--color-text, #000000);
line-height: 1.7;
word-break: keep-all;
}
</style>
</div>
<div class="area-topic2">
<!-- 꼭지 제목+설명: 좌측 질문/소제목 + 우측 설명 -->
<!--
📋 topic-header
─────────────────
용도: 각 꼭지의 시작부, 좌측에 파란 굵은 제목 + 우측에 본문 설명
슬롯: title (필수), description (필수)
비율: 좌 240px : 우 나머지
Figma 원본: sub_제목,내용 (742x68~78)
-->
<div class="block-topic-header">
<div class="th-title">건설산업에서의 BIM</div>
<div class="th-desc">BIM은 설계, 시공, 유지관리 단계에서의 정보를 통합하고 협업을 촉진하여 인프라 건설 전 생애주기 과정의 효율성을 향상시킵니다.</div>
</div>
<style>
.block-topic-header {
display: flex;
gap: 20px;
padding: 12px 0;
}
.th-title {
width: 240px;
flex-shrink: 0;
font-size: 24px;
font-weight: var(--weight-bold, 700);
color: var(--color-accent-deep, #004cbe);
line-height: 1.4;
word-break: keep-all;
}
.th-desc {
flex: 1;
font-size: 16px;
font-weight: var(--weight-normal, 400);
color: var(--color-text, #000000);
line-height: 1.7;
word-break: keep-all;
}
</style>
</div>
<div class="area-images">
<!-- 이미지 행: 2~4장 이미지 나란히 -->
<!--
📋 image-row
─────────────────
용도: 시공 사진, 근거 자료, 현장 이미지 나란히 배치
슬롯: images[] 배열 (각 이미지에 src, alt, caption)
Figma 원본: 2-1_02 > image grid (460x354 x 2)
-->
<div class="block-image-row" style="--ir-count: 2">
<div class="ir-item">
<img src="figma-assets/image_grid_left.png" alt="현장1">
</div>
<div class="ir-item">
<img src="figma-assets/image_grid_right.png" alt="현장2">
</div>
</div>
<style>
.block-image-row {
display: grid;
grid-template-columns: repeat(var(--ir-count, 2), 1fr);
gap: 0;
}
.ir-item {
overflow: hidden;
}
.ir-item img {
width: 100%;
height: 354px;
object-fit: cover;
display: block;
}
.ir-caption {
font-size: 11px;
color: var(--color-text-light, #94a3b8);
text-align: center;
padding: 4px;
}
</style>
</div>
<div class="area-compare">
<!-- 비교 박스: 이미지 배경 + 텍스트 오버레이 + VS 라벨 -->
<!--
📋 compare-box
─────────────────
용도: 2개 개념을 시각적으로 대비 (배경 이미지 + 텍스트 오버레이)
슬롯: left_label, left_sub, right_label, right_sub, left_bg (선택), right_bg (선택)
Figma 원본: 2-1_02 > Group 1171281597/1598
-->
<div class="block-compare-box">
<div class="cb-item">
<div class="cb-bg cb-bg-default cb-bg-left"></div>
<div class="cb-overlay">
<div class="cb-label">D/X</div>
<div class="cb-sub">디지털 기술을 활용한
협업 프로세스</div>
</div>
</div>
<div class="cb-vs">VS</div>
<div class="cb-item">
<div class="cb-bg cb-bg-default cb-bg-right"></div>
<div class="cb-overlay">
<div class="cb-label">BIM</div>
<div class="cb-sub">시설물의 전 생애주기 동안
정보의 생성 및 관리</div>
</div>
</div>
</div>
<style>
.block-compare-box {
display: flex;
gap: 12px;
align-items: center;
justify-content: center;
padding: 10px 0;
}
.cb-item {
position: relative;
width: 327px;
height: 116px;
border-radius: 10px;
overflow: hidden;
}
.cb-bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
}
.cb-bg-default {
background: linear-gradient(135deg, #006aff 0%, #004cbe 100%);
}
.cb-overlay {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #ffffff;
text-align: center;
}
.cb-label {
font-size: 20px;
font-weight: var(--weight-bold, 700);
}
.cb-sub {
font-size: 12px;
opacity: 0.85;
margin-top: 4px;
line-height: 1.5;
}
.cb-vs {
background: #ffffff;
border-radius: 8px;
padding: 8px 18px;
font-size: 20px;
font-weight: var(--weight-bold, 700);
color: var(--color-accent-bright, #006eff);
flex-shrink: 0;
}
</style>
</div>
<div class="area-table">
<!-- 비교 테이블 블록: 다항목 비교 -->
<div class="block-table">
<table class="comparison-table">
<thead>
<tr>
<th class="table-row-header">BIM</th>
<th>VS</th>
<th>D/X</th>
</tr>
</thead>
<tbody>
<tr>
<td class="table-row-header">Only 3D</td>
<td>BIM·D/X</td>
<td>BIM ≪ D/X (ENG.+Management 포함)</td>
</tr>
<tr>
<td class="table-row-header">모델 제작용 상용 S/W</td>
<td>S/W</td>
<td>제작 및 운영(상용+전용 40~80개)</td>
</tr>
<tr>
<td class="table-row-header">기존 2D 설계방식 유지</td>
<td>프로세스</td>
<td>근본적 문제의식을 통한 개선</td>
</tr>
<tr>
<td class="table-row-header">3D 모델 중심, 기존 성과품 유지</td>
<td>성과물</td>
<td>공학 정보 및 콘텐츠 연계에 집중</td>
</tr>
<tr>
<td class="table-row-header">3D 모델에 의한 일반적 이해 향상</td>
<td>활용</td>
<td>설계/시공의 혁신(개념의 재정립)</td>
</tr>
<tr>
<td class="table-row-header">(설계/시공/운영) 분야별 단절</td>
<td>확장성</td>
<td>전 생애주기 활용 시스템</td>
</tr>
</tbody>
</table>
</div>
<style>
.block-table {
overflow: auto;
height: 100%;
display: flex;
align-items: flex-start;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
font-size: var(--font-caption);
line-height: var(--line-height-ko);
}
.comparison-table th {
background: var(--color-primary);
color: white;
font-weight: var(--weight-bold);
padding: var(--spacing-small) var(--spacing-inner);
text-align: left;
font-size: var(--font-caption);
}
.comparison-table td {
padding: var(--spacing-small) var(--spacing-inner);
border-bottom: var(--border-width) solid var(--color-border);
font-size: var(--font-caption);
vertical-align: top;
}
.comparison-table tbody tr:nth-child(even) {
background: var(--color-bg-subtle);
}
.table-row-header {
font-weight: var(--weight-bold);
color: var(--color-primary);
white-space: nowrap;
}
</style>
</div>
<div class="area-circle">
<!-- 원형 라벨: CSS 그라데이션 원 + 중앙 텍스트 -->
<!--
📋 circle-label
─────────────────
용도: 섹션 전환점, 핵심 키워드 강조, 시각적 구분자
슬롯: label (필수), sub_label (선택)
Figma 원본: 2-1_02 > Group 1171281590 (190x190)
-->
<div class="block-circle-label">
<div class="cl-outer">
<div class="cl-inner">
<div class="cl-text">단계별
BIM의 활용</div>
</div>
</div>
</div>
<style>
.block-circle-label {
display: flex;
justify-content: center;
padding: 20px 0;
}
.cl-outer {
width: 190px;
height: 190px;
border-radius: 50%;
background: linear-gradient(180deg, #3db8ff 0%, #006aff 100%);
box-shadow: 0 0 30px rgba(0, 106, 255, 0.25), 0 0 60px rgba(0, 106, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
}
.cl-inner {
width: 170px;
height: 170px;
border-radius: 50%;
background: linear-gradient(180deg, #4dc4ff 0%, #0080ff 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #ffffff;
text-align: center;
}
.cl-text {
font-size: 20px;
font-weight: var(--weight-bold, 700);
line-height: 1.4;
}
.cl-sub {
font-size: 12px;
opacity: 0.8;
margin-top: 4px;
}
</style>
</div>
<div class="area-cards">
<!-- 이미지 카드: 상단 이미지 + 하단 텍스트 (2~4열) -->
<!--
📋 card-image
─────────────────
용도: 단계별 설명, 카테고리별 설명 (이미지가 핵심인 카드)
슬롯: cards[] 배열 (각 카드에 image, title, title_en, items[])
Figma 원본: 2-1_02 > Group 1171281594 (카드 3열)
-->
<div class="block-card-image" style="--ci-count: 3">
<div class="ci-card">
<img class="ci-img" src="figma-assets/card_img_design.png" alt="설계단계">
<div class="ci-body">
<div class="ci-title" style="color: #00aaff">설계단계</div>
<div class="ci-title-en">Design Stage</div>
<div class="ci-divider"></div>
<ul class="ci-list">
<li>고도화된 BIM 구현</li>
<li>최첨단 디지털트윈</li>
<li>시뮬레이션 분석 & 성능평가</li>
<li>지속가능한 인프라개발</li>
</ul>
</div>
</div>
<div class="ci-card">
<img class="ci-img" src="figma-assets/card_img_construction.png" alt="시공 단계">
<div class="ci-body">
<div class="ci-title" style="color: #006aff">시공 단계</div>
<div class="ci-title-en">Construction Stage</div>
<div class="ci-divider"></div>
<ul class="ci-list">
<li>향상된 건설 계획과 공정 순서 관리</li>
<li>Big Room 등 환경을 통한 협업 및 조정</li>
<li>정확한 수량산출서와 비용 산정</li>
</ul>
</div>
</div>
<div class="ci-card">
<img class="ci-img" src="figma-assets/card_img_maintenance.png" alt="유지관리 단계">
<div class="ci-body">
<div class="ci-title" style="color: #004cbe">유지관리 단계</div>
<div class="ci-title-en">Maintenance Stage</div>
<div class="ci-divider"></div>
<ul class="ci-list">
<li>자산 정보 및 데이터 관리</li>
<li>예측 기반 유지보수 및 생애주기 분석</li>
<li>효율적인 시설 운영 및 지속가능한 관리</li>
</ul>
</div>
</div>
</div>
<style>
.block-card-image {
display: grid;
grid-template-columns: repeat(var(--ci-count, 3), 1fr);
gap: 16px;
}
.ci-card {
background: var(--color-bg, #ffffff);
border-radius: var(--radius, 8px);
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
overflow: hidden;
display: flex;
flex-direction: column;
}
.ci-img {
width: 100%;
height: 160px;
object-fit: contain;
background: #f8f9fb;
padding: 10px;
}
.ci-body {
padding: 16px;
flex: 1;
display: flex;
flex-direction: column;
}
.ci-title {
font-size: 14px;
font-weight: var(--weight-bold, 700);
text-decoration: underline;
text-underline-offset: 3px;
margin-bottom: 2px;
}
.ci-title-en {
font-size: 12px;
font-weight: var(--weight-normal, 400);
color: var(--color-text-secondary, #666);
margin-bottom: 10px;
}
.ci-divider {
width: 100%;
height: 1px;
background: #000;
margin-bottom: 10px;
}
.ci-list {
list-style: disc;
padding-left: 18px;
font-size: 13px;
line-height: 1.7;
color: var(--color-text, #000);
flex: 1;
}
.ci-list li {
margin-bottom: 3px;
}
.ci-source {
font-size: 11px;
color: var(--color-text-light, #94a3b8);
font-style: italic;
margin-top: 8px;
border-top: 1px solid var(--color-border, #e2e8f0);
padding-top: 6px;
}
</style>
</div>
</div>
</body>
</html>

View File

@@ -1,401 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>2-1_02 건설정보모델링(BIM) — 레이어 분리 v2</title>
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Pretendard Variable', 'Noto Sans KR', sans-serif;
background: #e8ecf0;
display: flex;
justify-content: center;
padding: 20px;
color: #000;
}
.frame { width: 920px; background: #fff; }
/* ═══ 1. 배경 헤더 + 타이틀 오버레이 ═══ */
.header {
position: relative;
width: 100%;
height: 500px;
overflow: hidden;
}
.header img { width: 100%; height: 100%; object-fit: cover; }
.header-text {
position: absolute;
bottom: 40px;
left: 89px;
z-index: 5;
}
.header-text .en { font-size: 15px; font-weight: 400; color: #fff; opacity: 0.85; }
.header-text .ko { font-size: 35px; font-weight: 700; color: #fff; margin-top: 4px; }
.breadcrumb {
position: absolute;
top: 18px; left: 89px;
font-size: 13px; color: rgba(255,255,255,0.7);
z-index: 5;
}
.breadcrumb span { margin: 0 6px; color: rgba(255,255,255,0.5); }
.close-x {
position: absolute;
top: 18px; right: 20px;
font-size: 22px; color: #fff;
cursor: pointer; z-index: 5;
}
/* ═══ 2. 꼭지 제목+설명 ═══ */
.topic { padding: 28px 89px 12px; }
.topic-row { display: flex; gap: 20px; margin-bottom: 18px; }
.topic-title {
width: 240px; flex-shrink: 0;
font-size: 24px; font-weight: 700; color: #004cbe;
line-height: 1.4; word-break: keep-all;
}
.topic-desc {
flex: 1;
font-size: 16px; color: #000; line-height: 1.7; word-break: keep-all;
}
/* ═══ 3. 이미지 그리드 2열 ═══ */
.img-grid { display: flex; gap: 0; padding: 0; }
.img-grid img { flex: 1; height: 354px; object-fit: cover; }
/* ═══ 4. 산맥 시각화 ═══ */
.mountain img { width: 100%; }
/* ═══ 5. 비교 박스 (이미지 배경 + HTML 텍스트) ═══ */
.compare-boxes {
padding: 25px 89px;
display: flex;
gap: 12px;
align-items: center;
justify-content: center;
}
.cbox {
position: relative;
width: 327px; height: 116px;
border-radius: 10px;
overflow: hidden;
}
.cbox img { width: 100%; height: 100%; object-fit: cover; }
.cbox-text {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
color: #fff; text-align: center;
z-index: 2;
}
.cbox-text .label { font-size: 20px; font-weight: 700; }
.cbox-text .sub { font-size: 12px; opacity: 0.85; margin-top: 4px; }
.vs-badge {
background: #fff;
border-radius: 8px;
padding: 8px 18px;
font-size: 20px; font-weight: 700; color: #006eff;
flex-shrink: 0;
}
/* ═══ 6. 비교 테이블 (HTML) ═══ */
.compare-table { padding: 10px 89px 20px; }
.compare-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
line-height: 1.6;
}
.compare-table thead th {
padding: 10px 12px;
font-weight: 700;
font-size: 15px;
}
.compare-table thead th:first-child { color: #6bcdff; }
.compare-table thead th:nth-child(2) {
background: linear-gradient(135deg, #006eff, #00aaff);
color: #fff; border-radius: 20px;
text-align: center; width: 100px;
}
.compare-table thead th:last-child { color: #006eff; text-align: right; }
.compare-table tbody td {
padding: 8px 12px;
border-bottom: 1px solid #f0f0f0;
vertical-align: top;
}
.compare-table tbody td:first-child { color: #333; }
.compare-table tbody td:nth-child(2) {
text-align: center;
font-weight: 700;
color: #333;
background: #f8f9fb;
}
.compare-table tbody td:last-child { text-align: right; color: #333; }
.compare-table tbody tr:nth-child(even) td { background: #fafbfc; }
.compare-table tbody tr:nth-child(even) td:nth-child(2) { background: #f0f2f5; }
/* ═══ 7. 원형 라벨 (CSS + HTML) ═══ */
.circle-label-section {
display: flex; justify-content: center;
padding: 25px 0 15px;
}
.circle-outer {
width: 190px; height: 190px;
border-radius: 50%;
background: linear-gradient(180deg, #3db8ff 0%, #006aff 100%);
box-shadow: 0 0 30px rgba(0, 106, 255, 0.25), 0 0 60px rgba(0, 106, 255, 0.1);
display: flex;
align-items: center; justify-content: center;
position: relative;
}
.circle-inner {
width: 170px; height: 170px;
border-radius: 50%;
background: linear-gradient(180deg, #4dc4ff 0%, #0080ff 100%);
display: flex;
flex-direction: column;
align-items: center; justify-content: center;
color: #fff;
text-align: center;
}
.circle-inner .main { font-size: 20px; font-weight: 700; line-height: 1.4; }
/* ═══ 8. 카드 3열 (HTML + 개별 이미지) ═══ */
.cards-section { padding: 10px 89px 40px; }
.cards-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
overflow: hidden;
display: flex;
flex-direction: column;
}
.card-img {
width: 100%;
height: 160px;
object-fit: contain;
background: #f8f9fb;
padding: 10px;
}
.card-body { padding: 16px; }
.card-stage {
font-size: 14px; font-weight: 700;
text-decoration: underline;
text-underline-offset: 3px;
margin-bottom: 2px;
}
.card-stage-en {
font-size: 12px; font-weight: 400; color: #666;
margin-bottom: 10px;
}
.card-divider {
width: 100%; height: 1px;
background: #000; margin-bottom: 10px;
}
.card-body ul {
list-style: disc;
padding-left: 18px;
font-size: 13px;
line-height: 1.7;
color: #000;
}
.card-body li { margin-bottom: 3px; }
.card-design .card-stage { color: #00aaff; }
.card-construction .card-stage { color: #006aff; }
.card-maintenance .card-stage { color: #004cbe; }
</style>
</head>
<body>
<div class="frame">
<!-- 1. 배경 헤더 + 타이틀 -->
<div class="header">
<img src="figma-assets/bg_header.png" alt="">
<div class="breadcrumb">건설산업에서의 디지털전환 <span></span> Building Information Modeling</div>
<div class="close-x"></div>
<div class="header-text">
<div class="en">Building Information Modeling</div>
<div class="ko">건설정보모델링(BIM)</div>
</div>
</div>
<!-- 2. 꼭지 -->
<div class="topic">
<div class="topic-row">
<div class="topic-title">단순 BIM의 적용이<br>D/X가 아닙니다</div>
<div class="topic-desc">BIM은 건설산업의 디지털전환을 수행하는 과정에서 가장 기초가 되는 일부분임을 인지하는 것이 매우 중요합니다.</div>
</div>
<div class="topic-row">
<div class="topic-title">건설산업에서의 BIM</div>
<div class="topic-desc">BIM은 설계, 시공, 유지관리 단계에서의 정보를 통합하고 협업을 촉진하여 인프라 건설 전 생애주기 과정의 효율성을 향상시킵니다.</div>
</div>
</div>
<!-- 3. 이미지 그리드 -->
<div class="img-grid">
<img src="figma-assets/image_grid_left.png" alt="">
<img src="figma-assets/image_grid_right.png" alt="">
</div>
<!-- 4. 산맥 -->
<div class="mountain">
<img src="figma-assets/mountain_viz.png" alt="">
</div>
<!-- 5. 비교 박스 -->
<div class="compare-boxes">
<div class="cbox">
<img src="figma-assets/compare_box_left.png" alt="">
<div class="cbox-text">
<div class="label">D/X</div>
<div class="sub">디지털 기술을 활용한<br>협업 프로세스</div>
</div>
</div>
<div class="vs-badge">VS</div>
<div class="cbox">
<img src="figma-assets/compare_box_right.png" alt="">
<div class="cbox-text">
<div class="label">BIM</div>
<div class="sub">시설물의 전 생애주기 동안<br>정보의 생성 및 관리</div>
</div>
</div>
</div>
<!-- 6. 비교 테이블 (HTML) -->
<div class="compare-table">
<table>
<thead>
<tr>
<th>BIM</th>
<th>VS</th>
<th>D/X</th>
</tr>
</thead>
<tbody>
<tr>
<td>• Only 3D</td>
<td>BIM · D/X</td>
<td>• BIM ≪ D/X (ENG. + Management 포함)</td>
</tr>
<tr>
<td>• 모델 제작용 상용 S/W<br>(Civil 3D, Revit, Navisworks, Autocad)</td>
<td>S/W</td>
<td>• 제작 및 운영(상용 + 전용 40~80개)<br>[Rhino, Sketchup, Blender...] + [EG-BIM 등]</td>
</tr>
<tr>
<td>• 기존 2D 설계방식 유지</td>
<td>프로세스</td>
<td>• 근본적 문제의식을 통한 개선</td>
</tr>
<tr>
<td>• 3D 모델 중심<br>• 기존 성과품 유지</td>
<td>성과물</td>
<td>• 공학 정보 및 콘텐츠 연계에 집중<br>• 도면, 수량, 시공계획 등 일식</td>
</tr>
<tr>
<td>• 3D 모델에 의한 일반적 이해 향상</td>
<td>활용</td>
<td>• 설계/시공의 혁신(개념의 재정립)</td>
</tr>
<tr>
<td>• (설계/시공/운영) 분야별 단절</td>
<td>확장성</td>
<td>• 전 생애주기 활용 시스템</td>
</tr>
<tr>
<td>• 단순화(오류) - 수동적/집단적 동질화</td>
<td>수행개념</td>
<td>• 구체화(복잡) - 적극/구체적 실현 방안</td>
</tr>
<tr>
<td>• 소극적, 상용 기술에 의존</td>
<td>CIVIL + IT</td>
<td>• 적극적, 주체적인 기술 접목/융합</td>
</tr>
<tr>
<td>• S/W 제작사 판매 정책에 의존</td>
<td>주체</td>
<td>• 자체 수행능력 - 지속가능성 확보</td>
</tr>
<tr>
<td>• 평준화, 국내 중심</td>
<td>발주처</td>
<td>• 차별화 및 경쟁력 확보, 해외 진출</td>
</tr>
<tr>
<td>• 소규모 BIM팀 운영 + 단순교육에 집중</td>
<td>설계사</td>
<td>• IT + CIVIL ENG 220명 운영 + 기술 개발</td>
</tr>
<tr>
<td>• 국내 토목 소극적/ 해외 토목증가</td>
<td>시공사</td>
<td>• 분야 확장 모델 및 시스템</td>
</tr>
</tbody>
</table>
</div>
<!-- 7. 원형 라벨 (CSS) -->
<div class="circle-label-section">
<div class="circle-outer">
<div class="circle-inner">
<div class="main">단계별<br>BIM의 활용</div>
</div>
</div>
</div>
<!-- 8. 카드 3열 (HTML + 개별 이미지) -->
<div class="cards-section">
<div class="cards-row">
<div class="card card-design">
<img class="card-img" src="figma-assets/card_img_design.png" alt="설계">
<div class="card-body">
<div class="card-stage">설계단계</div>
<div class="card-stage-en">Design Stage</div>
<div class="card-divider"></div>
<ul>
<li>고도화된 BIM 구현</li>
<li>최첨단 디지털트윈</li>
<li>시뮬레이션 분석 & 성능평가</li>
<li>지속가능한 인프라개발</li>
</ul>
</div>
</div>
<div class="card card-construction">
<img class="card-img" src="figma-assets/card_img_construction.png" alt="시공">
<div class="card-body">
<div class="card-stage">시공 단계</div>
<div class="card-stage-en">Construction Stage</div>
<div class="card-divider"></div>
<ul>
<li>향상된 건설 계획과 공정 순서 관리</li>
<li>Big Room 등 환경을 통한 협업 및 조정</li>
<li>정확한 수량산출서와 비용 산정</li>
</ul>
</div>
</div>
<div class="card card-maintenance">
<img class="card-img" src="figma-assets/card_img_maintenance.png" alt="유지관리">
<div class="card-body">
<div class="card-stage">유지관리 단계</div>
<div class="card-stage-en">Maintenance Stage</div>
<div class="card-divider"></div>
<ul>
<li>자산 정보 및 데이터 관리</li>
<li>예측 기반 유지보수 및 생애주기 분석</li>
<li>효율적인 시설 운영 및 지속가능한 관리</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,328 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>2-1_02 건설정보모델링(BIM) — 레이어 방식 재현</title>
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Pretendard Variable', 'Noto Sans KR', sans-serif;
background: #e8ecf0;
display: flex;
justify-content: center;
padding: 20px;
}
/* ═══ 프레임 컨테이너 (920x2179 원본 비율) ═══ */
.frame {
width: 920px;
background: #ffffff;
position: relative;
overflow: hidden;
}
/* ═══ 섹션 1: 배경 헤더 + 타이틀 ═══ */
.header-section {
position: relative;
width: 100%;
height: 515px;
overflow: hidden;
}
.header-bg {
position: absolute;
inset: 0;
z-index: 1;
}
.header-bg img {
width: 100%;
height: 100%;
object-fit: cover;
}
.header-overlay {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
flex-direction: column;
padding: 40px 89px;
}
.close-btn {
position: absolute;
top: 20px;
right: 20px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
color: white;
font-size: 24px;
}
.section-title-block {
margin-top: auto;
margin-bottom: 30px;
}
.section-title-en {
font-size: 15px;
font-weight: 400;
color: #ffffff;
opacity: 0.9;
margin-bottom: 4px;
}
.section-title-ko {
font-size: 35px;
font-weight: 700;
color: #ffffff;
line-height: 1.3;
}
/* ═══ 섹션 2-3: 꼭지 제목+설명 ═══ */
.topic-section {
padding: 25px 89px 15px;
}
.topic-row {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.topic-title {
width: 240px;
flex-shrink: 0;
font-size: 24px;
font-weight: 700;
color: #004cbe;
line-height: 1.4;
word-break: keep-all;
}
.topic-desc {
flex: 1;
font-size: 16px;
font-weight: 400;
color: #000000;
line-height: 1.7;
word-break: keep-all;
}
/* ═══ 섹션 4: 이미지 그리드 2열 ═══ */
.image-grid-section {
padding: 0 89px 20px;
display: flex;
gap: 0;
}
.image-grid-item {
flex: 1;
overflow: hidden;
}
.image-grid-item img {
width: 100%;
height: 354px;
object-fit: cover;
}
/* ═══ 섹션 5: 산맥 시각화 ═══ */
.mountain-section {
width: 100%;
overflow: hidden;
}
.mountain-section img {
width: 100%;
height: auto;
}
/* ═══ 섹션 6: 이미지 (EbYopf) ═══ */
.middle-image-section {
width: 100%;
background: #f6f7f9;
padding: 0;
display: flex;
justify-content: center;
}
/* ═══ 섹션 7: 비교 박스 2열 ═══ */
.compare-boxes-section {
padding: 25px 89px;
display: flex;
gap: 15px;
justify-content: center;
align-items: center;
}
.compare-box {
width: 327px;
height: 116px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.compare-box img {
width: 100%;
height: 100%;
object-fit: cover;
}
.vs-label {
background: #ffffff;
border-radius: 8px;
padding: 8px 20px;
font-size: 20px;
font-weight: 700;
color: #006eff;
}
.dx-label, .bim-label {
position: absolute;
font-size: 20px;
font-weight: 700;
color: #ffffff;
z-index: 2;
}
/* ═══ 섹션 8: 비교 테이블 ═══ */
.table-section {
padding: 10px 89px 20px;
}
.table-section img {
width: 100%;
height: auto;
border-radius: 4px;
}
/* ═══ 섹션 9: 원형 라벨 ═══ */
.circle-section {
display: flex;
justify-content: center;
padding: 20px 0;
}
.circle-section img {
width: 190px;
height: 190px;
}
/* ═══ 섹션 10: 카드 3열 ═══ */
.card-section {
padding: 10px 89px 40px;
}
.card-section img {
width: 100%;
height: auto;
border-radius: 4px;
}
/* ═══ 네비게이션 화살표 ═══ */
.nav-arrows {
position: absolute;
bottom: 200px;
width: 100%;
display: flex;
justify-content: space-between;
padding: 0 15px;
z-index: 10;
pointer-events: none;
}
.nav-arrow {
width: 29px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: #006aff;
font-size: 24px;
pointer-events: auto;
cursor: pointer;
opacity: 0.5;
}
/* ═══ 브레드크럼 ═══ */
.breadcrumb {
position: absolute;
top: 20px;
left: 89px;
z-index: 5;
font-size: 13px;
color: rgba(255,255,255,0.7);
}
.breadcrumb span {
margin: 0 6px;
color: rgba(255,255,255,0.5);
}
</style>
</head>
<body>
<div class="frame">
<!-- ═══ 섹션 1: 배경 헤더 + 타이틀 ═══ -->
<div class="header-section">
<div class="header-bg">
<img src="figma-assets/bg_header.png" alt="">
</div>
<div class="header-overlay">
<div class="breadcrumb">
건설산업에서의 디지털전환 <span></span> Building Information Modeling
</div>
<div class="close-btn"></div>
<div class="section-title-block">
<div class="section-title-en">Building Information Modeling</div>
<div class="section-title-ko">건설정보모델링(BIM)</div>
</div>
</div>
</div>
<!-- ═══ 섹션 2: 꼭지1 ═══ -->
<div class="topic-section">
<div class="topic-row">
<div class="topic-title">단순 BIM의 적용이<br>D/X가 아닙니다</div>
<div class="topic-desc">BIM은 건설산업의 디지털전환을 수행하는 과정에서 가장 기초가 되는 일부분임을 인지하는 것이 매우 중요합니다.</div>
</div>
<div class="topic-row">
<div class="topic-title">건설산업에서의 BIM</div>
<div class="topic-desc">BIM은 설계, 시공, 유지관리 단계에서의 정보를 통합하고 협업을 촉진하여 인프라 건설 전 생애주기 과정의 효율성을 향상시킵니다.</div>
</div>
</div>
<!-- ═══ 섹션 3: 이미지 그리드 2열 ═══ -->
<div class="image-grid-section">
<div class="image-grid-item">
<img src="figma-assets/image_grid_left.png" alt="시공 현장 1">
</div>
<div class="image-grid-item">
<img src="figma-assets/image_grid_right.png" alt="시공 현장 2">
</div>
</div>
<!-- ═══ 섹션 4: 산맥 시각화 ═══ -->
<div class="mountain-section">
<img src="figma-assets/mountain_viz.png" alt="산맥 시각화">
</div>
<!-- ═══ 섹션 5: 비교 박스 ═══ -->
<div class="compare-boxes-section">
<div class="compare-box">
<img src="figma-assets/compare_box_left.png" alt="DX">
</div>
<div class="vs-label">VS</div>
<div class="compare-box">
<img src="figma-assets/compare_box_right.png" alt="BIM">
</div>
</div>
<!-- ═══ 섹션 6: DX vs BIM 비교 테이블 ═══ -->
<div class="table-section">
<img src="figma-assets/dx_bim_table.png" alt="DX vs BIM 비교">
</div>
<!-- ═══ 섹션 7: 원형 라벨 ═══ -->
<div class="circle-section">
<img src="figma-assets/circle_label.png" alt="단계별 BIM의 활용">
</div>
<!-- ═══ 섹션 8: 카드 3열 ═══ -->
<div class="card-section">
<img src="figma-assets/card_3col.png" alt="설계/시공/유지관리">
</div>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,206 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>DX와 BIM의 개념적 구분과 재정립</title>
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Pretendard Variable', sans-serif; }
.slide {
width: 1280px;
height: 720px;
position: relative;
overflow: hidden;
}
/* Layer 1: 배경 이미지 */
.bg-layer {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
z-index: 1;
}
.bg-layer img {
width: 100%; height: 100%;
object-fit: cover;
}
/* Layer 2: 슬라이드 제목 */
.title-layer {
position: absolute;
top: 15px; left: 30px;
z-index: 10;
font-size: 1.6rem;
font-weight: 900;
color: #1e293b;
border-bottom: 3px solid #2563eb;
padding-bottom: 6px;
}
/* Layer 3: 상단 5개 원 위 아이콘 + 텍스트 */
.circle-labels {
position: absolute;
z-index: 10;
text-align: center;
}
.circle-labels .icon {
font-size: 1.8rem;
margin-bottom: 2px;
}
.circle-labels .label {
font-size: 0.95rem;
font-weight: 800;
color: #1e293b;
line-height: 1.3;
}
.circle-labels .desc {
font-size: 0.7rem;
color: #64748b;
line-height: 1.4;
margin-top: 2px;
}
.circle-labels .highlight {
color: #2563eb;
font-weight: 700;
}
/* 각 원 위치 (배경 이미지의 원 위치에 맞춤) */
.circle-1 { top: 175px; left: 52px; width: 120px; }
.circle-2 { top: 95px; left: 245px; width: 120px; }
.circle-3 { top: 50px; left: 460px; width: 120px; }
.circle-4 { top: 95px; left: 680px; width: 120px; }
.circle-5 { top: 175px; left: 870px; width: 140px; }
/* Layer 4: 중앙 큰 원 텍스트 */
.center-label {
position: absolute;
top: 410px; left: 440px;
width: 200px;
z-index: 10;
text-align: center;
}
.center-label .main {
font-size: 1.3rem;
font-weight: 900;
color: #1e293b;
line-height: 1.4;
}
.center-label .sub {
font-size: 0.75rem;
color: #64748b;
margin-top: 4px;
}
/* Layer 5: 좌우 하단 박스 텍스트 */
.bottom-box {
position: absolute;
bottom: 35px;
z-index: 10;
padding: 8px 20px;
text-align: center;
}
.bottom-left {
left: 30px;
width: 260px;
}
.bottom-right {
right: 30px;
width: 260px;
}
.bottom-box .box-title {
font-size: 0.7rem;
color: #64748b;
margin-bottom: 2px;
}
.bottom-box .box-content {
font-size: 0.85rem;
font-weight: 700;
color: #1e293b;
line-height: 1.4;
}
/* Layer 6: 하단 결론 바 */
.conclusion-bar {
position: absolute;
bottom: 0; left: 0; right: 0;
z-index: 10;
background: #1e293b;
color: white;
text-align: center;
padding: 10px 40px;
font-size: 1rem;
font-weight: 700;
letter-spacing: -0.3px;
}
</style>
</head>
<body>
<div class="slide">
<!-- Layer 1: AI 생성 배경 이미지 -->
<div class="bg-layer">
<img src="test-bg-layer.png" alt="background">
</div>
<!-- Layer 2: 슬라이드 제목 -->
<div class="title-layer">DX와 BIM의 개념적 구분과 재정립</div>
<!-- Layer 3: 상단 5개 원 위 텍스트 -->
<div class="circle-labels circle-1">
<div class="icon">📋</div>
<div class="label">용어 혼용</div>
<div class="desc">DX와 BIM 개념이<br>명확히 <span class="highlight">정립되지 않은 채</span><br>혼용되어 사용</div>
</div>
<div class="circle-labels circle-2">
<div class="icon">🏛️</div>
<div class="label">정책 사례</div>
<div class="desc">건설기술진흥 기본계획<br><span class="highlight">BIM 도입 = 디지털화</span><br>로 표현</div>
</div>
<div class="circle-labels circle-3">
<div class="icon">📐</div>
<div class="label">BIM</div>
<div class="desc">3D 모델 기반<br><span class="highlight">정보 통합·관리</span> 도구<br>핵심 인프라 기술</div>
</div>
<div class="circle-labels circle-4">
<div class="icon">🔄</div>
<div class="label">DX</div>
<div class="desc">디지털 기술 기반<br><span class="highlight">산업 패러다임 전환</span><br>업무방식·가치 구조 변혁</div>
</div>
<div class="circle-labels circle-5">
<div class="icon">🔗</div>
<div class="label">기술 융합</div>
<div class="desc"><span class="highlight">GIS + BIM + DT</span><br>기술 융합으로만<br>DX 실현 가능</div>
</div>
<!-- Layer 4: 중앙 큰 원 텍스트 -->
<div class="center-label">
<div class="main">DX와 BIM의<br>관계</div>
<div class="sub">개념적 구분과 재정립</div>
</div>
<!-- Layer 5: 좌우 하단 박스 -->
<div class="bottom-box bottom-left">
<div class="box-title">상위 개념</div>
<div class="box-content">산업 패러다임 전환<br>프로세스 혁신</div>
</div>
<div class="bottom-box bottom-right">
<div class="box-title">핵심 기초 기술</div>
<div class="box-content">건설정보 통합 관리<br>디지털 협업 인프라</div>
</div>
<!-- Layer 6: 결론 바 -->
<div class="conclusion-bar">
BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
</div>
</body>
</html>

View File

@@ -1,222 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>DX와 BIM의 개념적 구분과 재정립</title>
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Pretendard Variable', sans-serif; background: #f0f4f8; display: flex; justify-content: center; padding: 20px; }
.slide {
width: 1280px;
height: 720px;
position: relative;
overflow: hidden;
}
/* ═══ Layer 1: 배경 텍스처 ═══ */
.bg { position: absolute; inset: 0; z-index: 1; }
.bg img { width: 100%; height: 100%; object-fit: cover; }
/* ═══ Layer 2: 연결선 (SVG) ═══ */
.lines-layer {
position: absolute; inset: 0; z-index: 2;
}
/* ═══ Layer 3: 원 (각각 개별) ═══ */
.node {
position: absolute; z-index: 5;
display: flex; flex-direction: column; align-items: center;
}
.node-circle {
width: 90px; height: 90px;
border-radius: 50%;
background: white;
border: 3px solid #d0dce8;
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
display: flex; align-items: center; justify-content: center;
font-size: 2rem;
}
.node-label {
margin-top: 8px; text-align: center;
}
.node-label .title {
font-size: 1rem; font-weight: 800; color: #1e293b;
}
.node-label .desc {
font-size: 0.72rem; color: #64748b; line-height: 1.5; margin-top: 3px;
}
.node-label .highlight {
color: #2563eb; font-weight: 700;
}
/* 중앙 큰 원 */
.center-node {
position: absolute; z-index: 5;
left: 530px; top: 390px;
display: flex; flex-direction: column; align-items: center;
}
.center-circle {
width: 160px; height: 160px;
border-radius: 50%;
background: linear-gradient(180deg, #ffffff 0%, #e8f4fd 100%);
border: 4px solid #2563eb;
box-shadow: 0 0 40px rgba(37, 99, 235, 0.3), 0 0 80px rgba(37, 99, 235, 0.1);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
.center-circle .main-text {
font-size: 1.2rem; font-weight: 900; color: #1e293b; text-align: center; line-height: 1.4;
}
.center-circle .sub-text {
font-size: 0.7rem; color: #64748b; margin-top: 4px;
}
/* ═══ Layer 4: 제목 ═══ */
.slide-title {
position: absolute; top: 18px; left: 30px; z-index: 10;
font-size: 1.5rem; font-weight: 900; color: #1e293b;
border-bottom: 3px solid #2563eb; padding-bottom: 6px;
}
/* ═══ Layer 5: 하단 좌우 박스 ═══ */
.bottom-box {
position: absolute; z-index: 10; bottom: 45px;
background: rgba(255,255,255,0.85);
border: 1px solid #d0dce8;
border-radius: 25px;
padding: 10px 24px;
text-align: center;
}
.bottom-left { left: 40px; }
.bottom-right { right: 40px; }
.bottom-box .box-label {
font-size: 0.65rem; color: #94a3b8; font-weight: 500;
}
.bottom-box .box-text {
font-size: 0.9rem; font-weight: 700; color: #1e293b; line-height: 1.5;
}
.bottom-box .arrow {
font-size: 1.2rem; color: #2563eb; font-weight: 900;
}
.bottom-left-wrap, .bottom-right-wrap {
display: flex; align-items: center; gap: 12px;
}
/* ═══ Layer 6: 결론 바 ═══ */
.conclusion {
position: absolute; bottom: 0; left: 0; right: 0; z-index: 10;
background: #1e293b;
color: white; text-align: center;
padding: 10px 40px;
font-size: 0.95rem; font-weight: 700;
}
/* ═══ 5개 노드 위치 ═══ */
.node-1 { left: 50px; top: 170px; width: 130px; }
.node-2 { left: 250px; top: 80px; width: 140px; }
.node-3 { left: 510px; top: 40px; width: 130px; }
.node-4 { left: 770px; top: 80px; width: 150px; }
.node-5 { left: 1000px; top: 170px; width: 140px; }
</style>
</head>
<body>
<div class="slide">
<!-- Layer 1: 배경 텍스처만 -->
<div class="bg">
<img src="bg-texture-only.png" alt="">
</div>
<!-- Layer 2: 연결선 SVG -->
<svg class="lines-layer" viewBox="0 0 1280 720" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 5개 원에서 중앙 원으로 곡선 -->
<path d="M 115,260 Q 300,400 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M 320,170 Q 420,350 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M 575,130 Q 590,300 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M 845,170 Q 750,350 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
<path d="M 1070,260 Q 850,400 610,470" stroke="#b0c4de" stroke-width="1.5" fill="none" opacity="0.5"/>
</svg>
<!-- Layer 3: 슬라이드 제목 -->
<div class="slide-title">DX와 BIM의 개념적 구분과 재정립</div>
<!-- Layer 4: 상단 5개 노드 (각각 원 + 아이콘 + 텍스트) -->
<div class="node node-1">
<div class="node-circle">📋</div>
<div class="node-label">
<div class="title">용어 혼용</div>
<div class="desc">DX와 BIM 개념이<br><span class="highlight">정립되지 않은 채</span><br>혼용되어 사용</div>
</div>
</div>
<div class="node node-2">
<div class="node-circle">🏛️</div>
<div class="node-label">
<div class="title">정책 사례</div>
<div class="desc">건설기술진흥 기본계획<br><span class="highlight">BIM 도입 = 디지털화</span></div>
</div>
</div>
<div class="node node-3">
<div class="node-circle">📐</div>
<div class="node-label">
<div class="title">BIM</div>
<div class="desc">3D 모델 기반<br><span class="highlight">정보 통합·관리</span> 도구</div>
</div>
</div>
<div class="node node-4">
<div class="node-circle">🔄</div>
<div class="node-label">
<div class="title">DX</div>
<div class="desc">디지털 기술 기반<br><span class="highlight">산업 패러다임 전환</span><br>업무방식·가치 구조 변혁</div>
</div>
</div>
<div class="node node-5">
<div class="node-circle">🔗</div>
<div class="node-label">
<div class="title">기술 융합</div>
<div class="desc"><span class="highlight">GIS + BIM + DT</span><br>기술 융합으로만<br>DX 실현 가능</div>
</div>
</div>
<!-- Layer 5: 중앙 큰 원 -->
<div class="center-node">
<div class="center-circle">
<div class="main-text">DX와 BIM의<br>관계</div>
<div class="sub-text">개념적 구분과 재정립</div>
</div>
</div>
<!-- Layer 6: 하단 좌우 박스 -->
<div class="bottom-box bottom-left">
<div class="bottom-left-wrap">
<span class="arrow"></span>
<div>
<div class="box-label">상위 개념</div>
<div class="box-text">산업 패러다임 전환<br>프로세스 혁신</div>
</div>
</div>
</div>
<div class="bottom-box bottom-right">
<div class="bottom-right-wrap">
<div>
<div class="box-label">핵심 기초 기술</div>
<div class="box-text">건설정보 통합 관리<br>디지털 협업 인프라</div>
</div>
<span class="arrow"></span>
</div>
</div>
<!-- Layer 7: 결론 바 -->
<div class="conclusion">
BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
</div>
</body>
</html>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Regular/result.css" type="text/css"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Bold/result.css" type="text/css"?>
<svg height="445" width="434.5" style="" font-family="Alibaba PuHuiTi" xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 434.5 445"><defs /><defs><radialGradient id="#1783ff-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#b0d5ff" /><stop offset="100%" stop-color="#1783ff" /></radialGradient><linearGradient id="#ff6b6b-badge" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff6b6b" /><stop offset="100%" stop-color="#ee5a52" /></linearGradient><radialGradient id="#00c9c9-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#63ffff" /><stop offset="100%" stop-color="#00c9c9" /></radialGradient><radialGradient id="#f0884d-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#fce6da" /><stop offset="100%" stop-color="#f0884d" /></radialGradient><radialGradient id="#d580ff-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#ffffff" /><stop offset="100%" stop-color="#d580ff" /></radialGradient></defs><g id="infographic-container"><g><g><g transform="translate(310.5, 150)"><ellipse x="0" y="0" width="80" height="80" fill="url(##1783ff-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">건설산업 DX</span></foreignObject></g></g><g transform="translate(160.5, 300)"><ellipse x="0" y="0" width="80" height="80" fill="url(##00c9c9-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">GIS</span></foreignObject></g></g><g transform="translate(10.5, 150.00000000000003)"><ellipse x="0" y="0" width="80" height="80" fill="url(##f0884d-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">BIM</span></foreignObject></g></g><g transform="translate(160.49999999999997, 0)"><ellipse x="0" y="0" width="80" height="80" fill="url(##d580ff-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Digital Twin</span></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Regular/result.css" type="text/css"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Bold/result.css" type="text/css"?>
<svg height="445" width="434.5" style="" font-family="Alibaba PuHuiTi" xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 434.5 445"><defs /><defs><radialGradient id="#1783ff-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#b0d5ff" /><stop offset="100%" stop-color="#1783ff" /></radialGradient><linearGradient id="#ff6b6b-badge" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff6b6b" /><stop offset="100%" stop-color="#ee5a52" /></linearGradient><radialGradient id="#00c9c9-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#63ffff" /><stop offset="100%" stop-color="#00c9c9" /></radialGradient><radialGradient id="#f0884d-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#fce6da" /><stop offset="100%" stop-color="#f0884d" /></radialGradient><radialGradient id="#d580ff-icon" cx="50%" cy="30%" r="70%"><stop offset="0%" stop-color="#ffffff" /><stop offset="100%" stop-color="#d580ff" /></radialGradient></defs><g id="infographic-container"><g><g><g transform="translate(310.5, 150)"><ellipse x="0" y="0" width="80" height="80" fill="url(##1783ff-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">건설산업 DX</span></foreignObject></g></g><g transform="translate(160.5, 300)"><ellipse x="0" y="0" width="80" height="80" fill="url(##00c9c9-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">GIS</span></foreignObject></g></g><g transform="translate(10.5, 150.00000000000003)"><ellipse x="0" y="0" width="80" height="80" fill="url(##f0884d-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">BIM</span></foreignObject></g></g><g transform="translate(160.49999999999997, 0)"><ellipse x="0" y="0" width="80" height="80" fill="url(##d580ff-icon)" cx="40" cy="40" rx="40" ry="40" /><ellipse x="60" width="24" height="24" fill="url(##ff6b6b-badge)" cx="72" cy="12" rx="12" ry="12" /><g transform="translate(60, 0)"><text width="24" height="24" x="12" y="12" fill="#ffffff" font-size="10" text-anchor="middle" dominant-baseline="central" font-weight="bold" /></g><g transform="translate(0, 88)"><foreignObject height="17" width="80" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:12px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Digital Twin</span></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Regular/result.css" type="text/css"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Bold/result.css" type="text/css"?>
<svg height="210" width="460" style="" font-family="Alibaba PuHuiTi" xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 460 210"><defs /><g id="infographic-container"><g><g><g><g><g><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Start</span></foreignObject></g><g transform="translate(0, 40)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Step 1</span></foreignObject></g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#1783ff" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">01</text></g></g></g></g><g transform="translate(140, 0)"><g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#00c9c9" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">02</text></g></g><g transform="translate(0, 110)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Step 2</span></foreignObject></g><g transform="translate(0, 130)"><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Progress</span></foreignObject></g></g></g><g transform="translate(280, 0)"><g><g><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Complete</span></foreignObject></g><g transform="translate(0, 40)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Step 3</span></foreignObject></g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#f0884d" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">03</text></g></g></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Regular/result.css" type="text/css"?>
<?xml-stylesheet href="https://assets.antv.antgroup.com/AlibabaPuHuiTi-Bold/result.css" type="text/css"?>
<svg height="210" width="460" style="" font-family="Alibaba PuHuiTi" xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 460 210"><defs /><g id="infographic-container"><g><g><g><g><g><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Start</span></foreignObject></g><g transform="translate(0, 40)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Step 1</span></foreignObject></g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#1783ff" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">01</text></g></g></g></g><g transform="translate(140, 0)"><g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#00c9c9" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">02</text></g></g><g transform="translate(0, 110)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Step 2</span></foreignObject></g><g transform="translate(0, 130)"><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-start;align-items:flex-start" xmlns="http://www.w3.org/1999/xhtml">Progress</span></foreignObject></g></g></g><g transform="translate(280, 0)"><g><g><foreignObject height="40" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#5a5a5a;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Complete</span></foreignObject></g><g transform="translate(0, 40)"><foreignObject height="20" width="140" y="0" x="0" overflow="visible"><span style="overflow:visible;color:#262626;font-weight:bold;font-size:14px;line-height:1.4;width:100%;height:100%;display:flex;flex-wrap:wrap;word-break:break-word;white-space:pre-wrap;text-align:center;justify-content:center;align-content:flex-end;align-items:flex-end" xmlns="http://www.w3.org/1999/xhtml">Step 3</span></foreignObject></g><g transform="translate(0, 70)"><polygon width="140" height="30" fill="#f0884d" points="0,0 130,0 140,15 130,30 0,30 10,15" /><g><text width="140" height="30" x="70" y="15" fill="#ffffff" font-size="16" text-anchor="middle" dominant-baseline="central" font-weight="bold">03</text></g></g></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,402 +0,0 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>DX와 BIM의 개념적 구분과 재정립</title>
<style>
/* Design Agent — 디자인 토큰 */
/* CLAUDE.md에 정의된 디자인 원칙을 CSS 변수로 구현 */
:root {
/* 색상 */
--color-primary: #1e293b;
--color-accent: #2563eb;
--color-neutral: #64748b;
--color-bg: #ffffff;
--color-bg-subtle: #f8fafc;
--color-border: #e2e8f0;
--color-danger: #dc2626;
--color-success: #16a34a;
--color-text: #1e293b;
--color-text-secondary: #64748b;
--color-text-light: #94a3b8;
/* 폰트 크기 */
--font-title: 2rem;
--font-subtitle: 1.25rem;
--font-body: 0.95rem;
--font-caption: 0.8rem;
--font-small: 0.7rem;
/* 폰트 두께 */
--weight-normal: 400;
--weight-medium: 500;
--weight-bold: 700;
--weight-black: 900;
/* 여백 */
--spacing-page: 40px;
--spacing-block: 20px;
--spacing-inner: 16px;
--spacing-small: 8px;
/* 기타 */
--radius: 6px;
--border-width: 1px;
--accent-border: 3px;
--line-height-ko: 1.7;
}
/* Design Agent — 기본 슬라이드 스타일 */
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 슬라이드 컨테이너: 16:9 고정 비율 */
.slide {
width: 1280px;
height: 720px;
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--color-bg);
font-family: 'Pretendard Variable', 'Pretendard', 'Noto Sans KR', sans-serif;
color: var(--color-text);
font-size: var(--font-body);
line-height: var(--line-height-ko);
word-break: keep-all;
padding: var(--spacing-page);
display: grid;
gap: var(--spacing-block);
}
/* 슬라이드 제목 */
.slide-title {
font-size: var(--font-title);
font-weight: var(--weight-black);
color: var(--color-primary);
border-bottom: var(--accent-border) solid var(--color-accent);
padding-bottom: var(--spacing-small);
}
/* 섹션 제목 */
.section-title {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
color: var(--color-primary);
margin-bottom: var(--spacing-small);
}
/* 본문 */
.body-text {
font-size: var(--font-body);
color: var(--color-text);
line-height: var(--line-height-ko);
}
/* 캡션/출처 */
.caption {
font-size: var(--font-caption);
color: var(--color-text-light);
font-style: italic;
}
/* 강조 텍스트 */
.highlight {
color: var(--color-accent);
font-weight: var(--weight-bold);
}
/* 경고/문제 강조 */
.danger {
color: var(--color-danger);
font-weight: var(--weight-bold);
}
</style>
<style>
.slide-1 {
grid-template-areas: 'header header' 'left right' 'footer footer';
grid-template-columns: 6.5fr 3.5fr;
grid-template-rows: auto 1fr auto;
}
.slide-1 .area-header {
grid-area: header;
}
.slide-1 .area-left {
grid-area: left;
}
.slide-1 .area-right {
grid-area: right;
}
.slide-1 .area-footer {
grid-area: footer;
}
/* 다중 페이지: 페이지 간 간격 */
.slide + .slide {
margin-top: 40px;
}
/* 인쇄 시 페이지 분리 */
@media print {
.slide {
page-break-after: always;
}
.slide + .slide {
margin-top: 0;
}
}
</style>
</head>
<body>
<div class="slide slide-1">
<div class="slide-title" style="grid-area: header;">DX와 BIM의 개념적 구분과 재정립</div>
<div class="area-header">
<!-- 강조 인용 블록: 문제 제기, 핵심 메시지 -->
<div class="block-quote">
<div class="quote-text">건설산업의 디지털 전환 논의에서 DX와 BIM이 개념적으로 명확히 정립되지 않은 채 혼용되어 사용되고 있으며, BIM 기술의 도입을 DX의 완성으로 오인하거나 DX를 BIM 기술 도입 수준으로 한정하는 인식이 확산되고 있다.</div>
</div>
<style>
.block-quote {
background: var(--color-bg-subtle);
border-left: var(--accent-border) solid var(--color-danger);
padding: var(--spacing-inner) var(--spacing-block);
border-radius: 0 var(--radius) var(--radius) 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.quote-text {
font-size: var(--font-body);
color: var(--color-text);
line-height: var(--line-height-ko);
font-weight: var(--weight-medium);
}
.quote-source {
font-size: var(--font-caption);
color: var(--color-text-light);
font-style: italic;
margin-top: var(--spacing-small);
}
</style>
</div>
<div class="area-left">
<!-- 카드 그리드 블록: 2~4열 카드 배열 -->
<div class="block-card-grid" style="--card-count: 2">
<div class="card" style="border-top-color: None">
<div class="card-title">제7차 건설기술진흥 기본계획</div>
<span class="card-category">국토교통부, 2023</span>
<div class="card-description">추진방향: 디지털 전환을 통한 스마트 건설 확산
추진과제: BIM 도입으로 건설산업 디지털화</div>
</div>
<div class="card" style="border-top-color: None">
<div class="card-title">스마트 건설 활성화 방안</div>
<span class="card-category">국토교통부, 2022</span>
<div class="card-description">추진과제: 건설산업 디지털화
세부내용: BIM 전면 도입 및 제도 정비, BIM 전문인력 양성</div>
</div>
</div>
<style>
.block-card-grid {
display: grid;
grid-template-columns: repeat(var(--card-count, 3), 1fr);
gap: var(--spacing-inner);
height: 100%;
}
.card {
background: var(--color-bg);
border: var(--border-width) solid var(--color-border);
border-top: var(--accent-border) solid var(--color-accent);
border-radius: var(--radius);
padding: var(--spacing-inner);
display: flex;
flex-direction: column;
}
.card-icon {
font-size: 1.5rem;
margin-bottom: var(--spacing-small);
}
.card-title {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
color: var(--color-primary);
margin-bottom: 4px;
}
.card-category {
font-size: var(--font-small);
font-weight: var(--weight-medium);
color: var(--color-accent);
background: #dbeafe;
padding: 2px 8px;
border-radius: 12px;
display: inline-block;
margin-bottom: var(--spacing-small);
width: fit-content;
}
.card-description {
font-size: var(--font-body);
color: var(--color-text);
line-height: var(--line-height-ko);
flex: 1;
}
.card-source {
font-size: var(--font-small);
color: var(--color-text-light);
font-style: italic;
margin-top: var(--spacing-small);
border-top: var(--border-width) solid var(--color-border);
padding-top: var(--spacing-small);
}
</style>
</div>
<div class="area-right">
<!-- 카드 그리드 블록: 2~4열 카드 배열 -->
<div class="block-card-grid" style="--card-count: 3">
<div class="card" style="border-top-color: None">
<div class="card-title">BIM</div>
<span class="card-category">디지털 전환 핵심 기술</span>
<div class="card-description">시설물 생애주기 정보를 3D 모델 기반으로 통합·관리하는 인프라 기술</div>
<div class="card-source">건설산업 BIM 기본지침, 국토교통부, 2020</div>
</div>
<div class="card" style="border-top-color: None">
<div class="card-title">건설산업</div>
<span class="card-category">종합산업</span>
<div class="card-description">다양한 시설물을 광범위한 기술을 통합·융합하여 만들어내는 종합산업</div>
</div>
<div class="card" style="border-top-color: None">
<div class="card-title">DX</div>
<span class="card-category">산업 패러다임 변화</span>
<div class="card-description">디지털 기술 기반으로 업무방식과 가치 창출 구조를 전환하는 과정 및 결과</div>
<div class="card-source">IBM, 2011 / Agile Elephant, 2015</div>
</div>
</div>
<style>
.block-card-grid {
display: grid;
grid-template-columns: repeat(var(--card-count, 3), 1fr);
gap: var(--spacing-inner);
height: 100%;
}
.card {
background: var(--color-bg);
border: var(--border-width) solid var(--color-border);
border-top: var(--accent-border) solid var(--color-accent);
border-radius: var(--radius);
padding: var(--spacing-inner);
display: flex;
flex-direction: column;
}
.card-icon {
font-size: 1.5rem;
margin-bottom: var(--spacing-small);
}
.card-title {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
color: var(--color-primary);
margin-bottom: 4px;
}
.card-category {
font-size: var(--font-small);
font-weight: var(--weight-medium);
color: var(--color-accent);
background: #dbeafe;
padding: 2px 8px;
border-radius: 12px;
display: inline-block;
margin-bottom: var(--spacing-small);
width: fit-content;
}
.card-description {
font-size: var(--font-body);
color: var(--color-text);
line-height: var(--line-height-ko);
flex: 1;
}
.card-source {
font-size: var(--font-small);
color: var(--color-text-light);
font-style: italic;
margin-top: var(--spacing-small);
border-top: var(--border-width) solid var(--color-border);
padding-top: var(--spacing-small);
}
</style>
</div>
<div class="area-footer">
<!-- 결론 바 블록: 하단 핵심 한 줄 -->
<div class="block-conclusion">
<div class="conclusion-label">핵심 요약</div>
<div class="conclusion-text">BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다</div>
</div>
<style>
.block-conclusion {
background: var(--color-primary);
color: white;
padding: var(--spacing-inner) var(--spacing-block);
border-radius: var(--radius);
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
}
.conclusion-label {
font-size: var(--font-caption);
color: var(--color-text-light);
font-weight: var(--weight-medium);
text-transform: uppercase;
letter-spacing: 1px;
}
.conclusion-text {
font-size: var(--font-subtitle);
font-weight: var(--weight-bold);
line-height: var(--line-height-ko);
}
</style>
</div>
</div>
</body>
</html>

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,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,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

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