Compare commits

..

2 Commits

Author SHA1 Message Date
46e53beeaf README 재정리: AS-IS / TO-BE 프로세스 구조 명시
- 현재 프로세스(AS-IS) 7단계 정리
- 개선 프로세스(TO-BE) 5단계 정리
- 핵심 차이: 꼭지 추출 시점, redesign 반복 조정, 빈 공간 재분배, 시각 검증
- 프로젝트 구조/산출물/향후 방향 포함

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:53:51 +09:00
1ff6c6cbb2 Add remaining samples, tooling, and local project assets 2026-04-15 18:02:17 +09:00
863 changed files with 19088 additions and 301 deletions

View File

@@ -56,7 +56,76 @@
"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_*/)"
"Bash(ls -la /d/ad-hoc/kei/design_agent/data/runs/20260413_*/)",
"Bash(python -c \"from src.config import settings; print\\(f'API configured: {bool\\(settings.anthropic_api_key\\)}'\\)\")",
"Bash(python run_test.py)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8000/health)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8000/)",
"Read(//d/ad-hoc/kei/**)",
"Bash(curl -s http://localhost:8000/docs)",
"Bash(taskkill //F //IM python.exe)",
"Bash(python assemble_mdx02_test.py)",
"Bash(wc -c data/runs/20260407_*/final.html)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8000/docs)",
"Bash(curl -s http://localhost:8080/docs -o /dev/null -w \"%{http_code}\")",
"Bash(curl -s http://localhost:8001/docs -o /dev/null -w \"%{http_code}\")",
"Bash(curl -s http://localhost:3000/ -o /dev/null -w \"%{http_code}\")",
"Bash(python assemble_mdx02_v3.py)",
"Bash(python assemble_mdx02_v4.py)",
"Bash(python assemble_mdx02_v5.py)",
"Bash(python assemble_mdx02_v6.py)",
"Bash(python assemble_mdx02_v7.py)",
"Bash(python assemble_mdx02_v8_3plans.py)",
"Bash(python assemble_mdx02_v9.py)",
"Bash(python build_plan1.py)",
"Bash(python build_plan2.py)",
"Bash(python build_plan3.py)",
"Bash(python build_plan1_v2.py)",
"Bash(python build_4plans_final.py)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:8000/docs)",
"Bash(uvicorn backend.main:app --port 8000)",
"Bash(python final_plan1.py)",
"Bash(python final_plan2.py)",
"Bash(python build_all_4plans.py)",
"Bash(python build_plan3_kei.py)",
"Bash(python build_plan4_kei.py)",
"Bash(python make_4plans.py)",
"Bash(ls -la \"d:/ad-hoc/kei/design_agent/figma_to_html_agent/block-tests/html_render_final\"*)",
"Bash(taskkill //PID 48540 //F)",
"Bash(python make_mdx03.py)",
"Bash(python run_mdx03_pipeline.py)",
"Bash(powershell -Command \"Get-Process python -ErrorAction SilentlyContinue | Select-Object Id,StartTime\")",
"Bash(grep \"class FontHierarchy\" src/*.py)",
"Bash(ls -ltr /d/ad-hoc/kei/design_agent/data/runs/*/step_*_context.json)",
"Bash(awk '{print $2}')",
"Bash(stat /d/ad-hoc/kei/design_agent/data/runs/20260414_120225/stage_*_context.json)",
"Bash(ls -la /d/ad-hoc/kei/design_agent/data/runs/20260414_120225/stage_*_context.json)",
"Bash(awk '{print $6, $7, $8, $9}')",
"Bash(ls -lt data/runs/20260414_120225/*_context.json)",
"Bash(awk '{print $6,$7,$8,$9}')",
"Bash(python -c \" import yaml with open\\('catalog.yaml'\\) as f: data = yaml.safe_load\\(f\\) blocks = data.get\\('blocks', []\\) for b in blocks: print\\(f\\\\\"{b['id']} | {b.get\\('category',''\\)} | items:{b.get\\('min_items','?'\\)}-{b.get\\('max_items','?'\\)}\\\\\"\\) \")",
"Bash(python add_tags.py)",
"Bash(python -c \"import src.block_reference; print\\('OK'\\)\")",
"Bash(python -c \"import src.block_assembler; print\\('OK'\\)\")",
"Bash(ls -t d:/ad-hoc/kei/design_agent/docs/history/PHASE-*.md d:/ad-hoc/kei/design_agent/docs/history/IMPROVEMENT-PHASE-*.md)",
"Bash(python -c \"import src.pipeline_context; import src.kei_client; import src.pipeline; print\\('모든 import OK'\\)\")",
"Bash(python -c \"import src.step_visualizer; import src.pipeline; print\\('OK'\\)\")",
"Bash(python -c \"import src.pipeline; print\\('OK'\\)\")",
"Bash(python -c \"import src.pipeline; import src.block_assembler; print\\('OK'\\)\")",
"Bash(python -c \"import src.block_assembler; import src.pipeline; print\\('OK'\\)\")",
"Bash(python -c \"import src.kei_client; print\\('OK'\\)\")",
"Bash(python -c \"import src.pipeline; import src.block_assembler; import src.pipeline_context; print\\('OK'\\)\")",
"Bash(python -c \"import src.pipeline; import src.validators; print\\('OK'\\)\")",
"Bash(python -c \"import src.validators; print\\('OK'\\)\")",
"Bash(python -c \"import src.validators; import src.pipeline; print\\('OK'\\)\")",
"Bash(python -c \"import src.pipeline; import src.section_parser; import src.block_assembler; print\\('OK'\\)\")",
"Bash(python -c \"import src.pipeline; import src.block_assembler; import src.section_parser; print\\('OK'\\)\")",
"Bash(python -c \"import src.pipeline; import src.block_assembler; import src.space_allocator; import src.pipeline_context; print\\('OK'\\)\")",
"Bash(grep -l \"pp2-grid-wrap\\\\|pp2\" templates/blocks/**/*.html)",
"Bash(echo file:///D:/ad-hoc/kei/design_agent/data/runs/20260415_110323/final.html)",
"Bash(python -c \"import src.block_reference; import src.section_parser; import src.pipeline; print\\('OK'\\)\")",
"Bash(python -c \"import src.block_assembler; import src.section_parser; import src.pipeline; print\\('OK'\\)\")",
"Bash(python -c \"import src.block_reference; import src.pipeline; import src.block_assembler; print\\('OK'\\)\")"
],
"additionalDirectories": [
"d:\\ad-hoc\\kei\\design_agent\\templates\\blocks\\new"

389
README.md
View File

@@ -1,322 +1,151 @@
# C.E.L. Slide Pipeline
MDX 기반 콘텐츠를 분석 1280x720 슬라이드 HTML로 변환하는 파이프라인입니다.
MDX 기반 콘텐츠를 분석하여 1280×720 슬라이드 HTML로 자동 변환하는 파이프라인입니다.
이 문서는 현재 코드 기준으로 이 프로젝트가 무엇을 하는지, 어떤 프로세스로 동작하는지, 현재 어디까지 와 있는지, 앞으로 무엇을 개선하려는지를 빠르게 파악하기 위한 개요 문서입니다.
## 한 줄 요약
## 무엇을 하는가
**텍스트 콘텐츠를 넣으면, 코드가 구조를 분석하고 디자인을 입혀서 슬라이드로 만들어줍니다.**
이 프로젝트는 MDX 문서를 입력으로 받아 다음 과정을 거쳐 슬라이드 결과물을 생성합니다.
---
- 콘텐츠 정규화
- 문서 의미 분석
- 슬라이드 구조 해석
- schema / recipe / block 선택
- HTML 조립
- fit / overflow 검증
- 최종 산출물 저장
## 현재 프로세스 (AS-IS)
최종 산출물은 보통 다음 형태로 저장됩니다.
```
1. MDX 입력
- MDX 파일 읽기
- 정규화 (제목/본문/이미지/표/핵심요약 분리)
- `final.html`
- `final_context.json`
- `steps/*.html`
- popup / detail HTML
2. 꼭지 추출 (AI)
- 중목차별 핵심 주제 파악
- 각 꼭지의 성격 분류
## 한눈에 보는 프로세스
3. zone 구분 (코드)
- 중목차(##) 기준으로 영역 분할 (top/bottom)
- 콘텐츠 양 기준으로 비중(weight) 산출
```text
MDX Input
Stage 0 Normalize
Stage 1A/1B AI Meaning Analysis
Phase Y Group / Schema / Recipe Parsing
Stage 1.5a Space Allocation
Stage 1.7 Block Selection
Stage 1.8 Fit / Measure / Adjustment
Stage 2 Assemble final slide HTML
Stage 4 Validate with Selenium / Vision
Stage 5 Save final artifacts
4. BEPs 디자인 매칭 (코드)
- catalog에서 tag/schema 기준으로 블록 검색
- 매칭되면 → 해당 블록에 콘텐츠 삽입
- 미매칭이면 → 기존 스타일 따라 코드가 고정 렌더 (1회)
5. 조립 (코드)
- slide-base(1280×720)에 zone별 배치
- 상세 내용은 첨부 파일로 분리
6. 검증
- overflow 측정 (Selenium)
- 구조 검증 (본문 visible, 링크 수)
7. 출력
- final.html + 첨부 HTML
```
핵심 원칙은 다음과 같습니다.
### 현재 수준
- source of truth는 `normalized.sections`
- 문서명 하드코딩보다 shape, schema, recipe, tag를 우선한다
- 흐름의 우선순위는 `구조 -> payload -> layout -> fit`
- popup / detail은 overflow를 덮는 임시 장치가 아니라 `메인 요약 + 상세 보기`의 표현 계약이다
| 대상 | 상태 |
|------|------|
| MDX 03 (3개 목표 + 비교표) | ✅ 정상 동작 |
| MDX 02 (목표 + 프로세스 + 상세표) | △ 구조 동작, 시각 품질 보정 중 |
## 타입 구조
---
현재 메인 타입 선택은 사실상 `Type A``Type B`입니다.
## 개선 프로세스 (TO-BE)
### Type A
```
1. MDX 입력
- MDX 파일 읽기
- 정규화 (제목/본문/이미지/표/핵심요약 분리)
- 본문 외에 sidebar, reference, 부록성 영역이 함께 필요한 슬라이드
- 현재는 Type B보다 덜 닫혀 있고, AI 생성 + renderer 경로 비중이 더 큼
2. zone 구분 (코드)
- 중목차(##) 기준으로 영역 분할
### Type B
3. BEPs 디자인 매칭 검토
3-1. 매칭 시
- 해당 디자인 선택
- zone 크기에 맞게 재구성
3-2. 미매칭 시
- 중목차에 대한 꼭지 정리 (AI)
- 꼭지별 유사 디자인 선택
- 해당 중목차/소목차에 맞게 redesign
- 입력 → 조정 반복 (빈 공간/overflow 자동 조절)
- top / bottom 같은 본문 zone 조합으로 해결되는 슬라이드
- 현재 가장 안정적인 메인 경로
- 최근 구조화 작업은 대부분 이 Type B 경로를 중심으로 진행됨
4. 검증
- overflow / 빈 공간 측정
- 시각 품질 검증 (정렬, 위계, 가독성)
### Type B' / B''
5. 출력
- final.html + 첨부 HTML
```
- 역사적으로 실험/호환 과정에서 나온 변형 경로
- 일부 legacy 코드와 과거 산출물에 흔적이 남아 있음
- 현재 메인 1급 타입이라기보다, 과거 흐름과 검증된 표시 계약을 담고 있는 보조 경로에 가까움
### AS-IS → TO-BE 핵심 차이
## 단계별 파이프라인
| 항목 | AS-IS | TO-BE |
|------|-------|-------|
| 꼭지 추출 시점 | zone 구분 전에 항상 실행 | 미매칭 시에만 (3-2) |
| 디자인 미매칭 | 코드 고정 렌더 1회 | redesign + 반복 조정 |
| 빈 공간 처리 | 그냥 둠 | 자동 재분배 |
| 검증 | overflow만 | 시각 품질까지 |
| 단계 | 담당 | 주요 파일 | 하는 일 | 주요 산출물 |
|---|---|---|---|---|
| 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 산출물 |
---
## 현재 메인 실행 경로
## 핵심 원칙
### Type B 메인 경로
| 원칙 | 설명 |
|------|------|
| **콘텐츠가 기준** | 디자인이 콘텐츠에 맞춤 (콘텐츠를 디자인에 맞추지 않음) |
| **코드가 판단** | AI는 꼭지 추출만, 레이아웃/디자인 선택은 규칙 기반 |
| **기존 디자인 재활용** | Figma에서 추출한 BEPs 블록을 우선 활용, 없으면 같은 스타일로 생성 |
| **넘치면 분리** | 슬라이드에 안 들어가는 상세 내용은 첨부로 분리 (자세히보기) |
| **통일이 아닌 다양성** | 같은 구조라도 콘텐츠에 따라 다른 표현 가능 (recipe = 표현 범주) |
지금 실전에서 가장 중요한 경로는 아래입니다.
---
1. Stage 0에서 MDX를 정규화
2. Stage 1A / 1B에서 AI가 문서 의미와 topic 추출
3. Phase Y에서 코드가 `normalized.sections`를 읽고 page_structure를 다시 생성
4. Stage 1.7에서 block / recipe 후보 선택
5. Stage 1.8에서 fit / overflow 검증
6. Stage 2에서 `assemble_slide_html_final()`로 최종 HTML 조립
7. Stage 4 / 5에서 측정과 산출물 저장
Type B의 핵심 파일은 아래입니다.
- `src/pipeline.py`
- `src/section_parser.py`
- `src/block_reference.py`
- `src/block_assembler.py`
- `src/space_allocator.py`
- `templates/catalog.yaml`
### Type A 경로
Type A는 현재도 살아 있지만, Type B만큼 단단하게 닫힌 상태는 아닙니다.
- AI 생성 비중이 더 큼
- `src/renderer.py` 의존도가 더 큼
- sidebar / reference 구조를 포함하는 문서에서 의미가 큼
## schema -> recipe -> block
최근 구조화에서 가장 중요한 변화 중 하나는 `schema -> recipe -> block` 레이어입니다.
### schema
콘텐츠의 의미 구조입니다.
예:
- `parallel_cluster`
- `parallel_cluster_plus_visual`
- `compare_asymmetric_paired`
- `sequence_plus_visual`
- `single_block`
### recipe
block 이름이 아니라 표현 규칙입니다.
예:
- `single_block`
- `two_col_text_visual`
- `two_col_text_detail`
- `stacked_summary_detail`
recipe는 보통 이런 계약을 가질 수 있습니다.
- left / right kind
- top / bottom kind
- ratio
- vertical align
- direct render 우선 여부
### block
실제 구현 템플릿 후보입니다.
예:
- `prerequisites-3col`
- `process-product-2col`
- `compare-detail-gradient`
- `card-icon-desc`
즉, “무슨 문서냐”보다 “무슨 구조냐”를 먼저 읽고, 그 구조에 맞는 표현 규칙을 정한 뒤, 마지막에 구현 block을 고르는 방향으로 가고 있습니다.
## popup / detail 계약
popup은 현재 다음 철학으로 정리되는 중입니다.
- 메인 슬라이드에는 zone 크기에 맞는 요약만 남긴다
- 큰 표, 시각 컴포넌트, 과다 bullet은 상세 popup으로 분리한다
- 메인에서는 `자세히보기` 링크를 제공한다
현재 popup 관련 핵심은 아래입니다.
- `PopupItem` 모델로 popup 데이터를 명시적으로 다루기 시작함
- `popup_id``popup_file` 생애주기를 분리해 관리 중
- 최종 목표는 popup 판단을 휴리스틱이 아니라 명시적 contract로 만드는 것
다만 아직 일부 구간에는 추측 로직과 이중 관리가 남아 있어, 이 부분은 계속 정리 중입니다.
## run 산출물 구조
각 실행은 `data/runs/{run_id}/` 아래에 저장됩니다.
주요 파일은 다음과 같습니다.
- `final.html`
- `final_context.json`
- `steps/*.html`
- popup / detail HTML
### final.html
- 최종 렌더 결과
- 실제 눈으로 보는 산출물
### final_context.json
- 각 단계 결과를 최종 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)
| 파일 | 역할 |
|------|------|
| `src/pipeline.py` | 전체 파이프라인 오케스트레이션 |
| `src/section_parser.py` | 중목차 추출, schema 분류, subsection typing |
| `src/block_reference.py` | BEPs 디자인 매칭 (catalog 검색) |
| `src/block_assembler.py` | 슬라이드 HTML 조립, recipe executor, direct render |
| `src/space_allocator.py` | zone 크기/비율 계산 |
| `src/pipeline_context.py` | 파이프라인 데이터 모델 (PopupItem 등) |
### 템플릿 / 카탈로그
- [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/catalog.yaml` | BEPs 블록 목록 (when/slots/template) |
| `templates/blocks/slide-base.html` | 슬라이드 기본 틀 (1280×720) |
| `templates/blocks/new/` | Figma 추출 블록 (prerequisites-3col 등) |
| `templates/blocks/redesign/` | 재디자인 블록 (process-product-2col 등) |
### 검증 / 측정
### 검증
- [src/slide_measurer.py](src/slide_measurer.py)
- [src/validators.py](src/validators.py)
- [src/step_visualizer.py](src/step_visualizer.py)
| 파일 | 역할 |
|------|------|
| `src/slide_measurer.py` | Selenium overflow 측정 |
| `src/step_visualizer.py` | 단계별 디버그 보드 생성 |
| `src/validators.py` | 구조 검증 |
### 계획 / 히스토리
### 산출물
- [PIPELINE.md](PIPELINE.md)
- [docs/history/PHASE-Y-PLAN.md](docs/history/PHASE-Y-PLAN.md)
각 실행은 `data/runs/{run_id}/` 아래에 저장됩니다.
## 현재 상태
| 파일 | 내용 |
|------|------|
| `final.html` | 최종 슬라이드 |
| `final_context.json` | 전체 파이프라인 결과 데이터 |
| `steps/*.html` | 단계별 디버그 보드 |
| `첨부*_상세*.html` | popup 상세 내용 |
### 비교적 잘 닫혀가는 것
- Type B 메인 경로
- `normalized.sections` 기반 구조 해석
- schema / recipe 기반 block selection의 골격
- redesign block 자산화
- popup / detail 2단 표현 계약의 초안 연결
### 아직 정리 중인 것
- Type A 전체 안정화
- popup을 완전한 source of truth로 정리
- tag_match 와 schema_match의 완전한 동등 점수 비교
- step 보드 인코딩 / 설명 품질
- fit loop의 공간 재분배 고도화
- legacy 경로와 문서 정리
### 최근 Type B에서 특히 중요해진 방향
- recipe direct render가 block 선택에 끌려가지 않도록 구조 계약을 더 강하게 만든다
- Type B direct render가 Type B'의 검증된 표시 계약을 최대한 재사용하도록 정리한다
- sample은 복제 대상이 아니라 evaluation rule의 기준으로 사용한다
- validation은 “같아야 한다”보다 “어긋나면 안 된다”에 초점을 둔다
---
## 향후 개선 방향
현재 이후의 개선 방향은 아래 축으로 정리됩니다.
### 1. 구조 계약 강화
- top / bottom zone의 contract를 더 구체화
- `parallel_cluster_plus_visual`, `full_text + detail_preview` 같은 recipe를 표현 범주 수준으로 강화
- recipe가 block를 끌고 가는 것이 아니라 recipe가 block를 통제하도록 정리
### 2. 표시 계약 통합
- Type B direct render가 Type B'에서 이미 검증된 bullet / indent / body-text 구조를 재사용하도록 통합
- `.rdr-*` 계열 신규 CSS를 계속 키우기보다, 기존 검증된 계약을 최대한 재사용
### 3. detail preview 개선
- popup source가 표면 `헤더 + 일부 행`
- popup source가 리스트면 `앞부분 몇 개`
- popup source가 컴포넌트면 구조화된 preview
즉 “링크만 있는 상세”가 아니라 “상세가 있다는 걸 바로 이해할 수 있는 preview”를 만드는 방향
### 4. fit loop 고도화
지금은 주로 overflow 대응 중심이지만, 앞으로는 아래까지 확장하려고 합니다.
- 빈 공간 감지
- 이미지 확대
- preview 행 수 조정
- zone 비율 재배분
### 5. 검증판과 final 일치화
- `stage_4` 보드와 실제 `final_context.json` / `final.html`의 상태 차이를 줄이기
- 검수 보드를 더 믿을 수 있는 상태로 정리
## 읽는 순서 추천
프로세스를 빠르게 파악하려면 아래 순서가 좋습니다.
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)
1. **3-2 입력→조정 반복 구현** — redesign 후 빈 공간/overflow 자동 재분배
2. **검증 강화** — overflow뿐 아니라 정렬, 위계, 가독성까지 시각 품질 검증
3. **BEPs 매칭 범위 확대** — 더 많은 Figma 블록 추출 및 catalog 등록
4. **다양한 MDX 확장** — MDX 02, 03 이후 추가 문서 유형 검증

Binary file not shown.

29
figma_to_html_agent/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Legacy / 임시 파일
block_index.faiss
block_metadata.json
figma_beps_full.json
figma_center_elements.json
figma_center_full.json
figma_frames.json
figma-analysis/
figma-assets/
figma-screenshots/
figma_ref/
previews/
FIGMA-COMPONENT-EXTRACTION-PLAN.md
FIGMA-CONVERSION-REVIEW.md
FIGMA-DESIGN-LANGUAGE.md
FIGMA-EXTRACTION.md
INSIGHT-GRADIENT.md
PHASE-FIGMA-BLOCKS.md
PLAN.md
RESEARCH.md
# Selenium 렌더 결과 / 비교 스크린샷
block-tests/_renders/
block-tests/*.png
# Python
__pycache__/
*.pyc
scripts/__pycache__/

View File

@@ -19,7 +19,7 @@ Figma 프레임을 **수학적으로 정확하게** HTML/CSS로 변환하고,
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/` 안에서 끝나며, 본체 라이브러리 이전은 사용자 수동 검수 후 사용자 본인이 수행한다.
9. **프로모션은 사용자 승인 후 진행** — 1:1 HTML 검증 완료 후, 사용자 승인을 받아 `templates/blocks/new/`에 블록을 생성하고 `templates/blocks/svg/`이미지를 정리한다. catalog.yaml 등록과 blocks_index.md 업데이트까지 에이전트가 수행한다. 단, 사용자 승인 없이 프로모션하지 않는다.
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에 모두 매칭되어야 한다는 가정으로 설계한다.
@@ -60,12 +60,18 @@ Figma 프레임을 **수학적으로 정확하게** HTML/CSS로 변환하고,
## 입출력
**입력:** Figma 파일 + 노드 ID (또는 현재 선택 노드)
**출력:**
- `block-tests/{slug}.html` — 변환 결과
**출력 (변환):**
- `block-tests/{slug}.html` 1:1 변환 결과
- `block-tests/{slug}_flat.md` — 플래튼/이상 탐지/변형 축 메모
- `assets/shared/...` — 공유 자산 캐시
- `blocks_index.md` 한 줄 추가
**출력 (프로모션, 사용자 승인 후):**
- `templates/blocks/new/{pattern}.html` — AI가 재디자인 가능한 블록 (CSS + Jinja2)
- `templates/blocks/svg/{name}.png/svg` — 블록 공용 이미지
- `templates/catalog.yaml` — 블록 등록 (content_structure, when, not_for)
- `blocks_index.md` 상태 → promoted
## 폴더 구조
```
@@ -73,9 +79,9 @@ figma_to_html_agent/ ← 에이전트 작업 영역 (staging)
├── CLAUDE.md ← 이 파일 (에이전트 명세)
├── PROCESS.md ← 10단계 운영 절차 (변환 핸드북)
├── MATH.md ← 수학 공식 레퍼런스
├── RULES.md ← CSS 보정 규칙 (R1~R12)
├── RULES.md ← CSS 보정 규칙 (R1~R16)
├── PROCESS-CONTROL.md ← "찍어맞추기 금지" 규칙
├── PLAN.md ← 현재 진행 현황
├── README.md ← 사용법 가이드
├── blocks_index.md ← 변환 완료 도서관
├── scripts/
@@ -90,22 +96,22 @@ figma_to_html_agent/ ← 에이전트 작업 영역 (staging)
│ ├── shared/ ← 해시 기반 자산 캐시 (재사용)
│ └── frame_{id}/ ← 프레임 전용 자산 (legacy)
└── templates_staging/ ← Stage 2: Jinja2 추상화
├── {pattern_id}.html.j2
└── {pattern_id}.meta.yaml ← when/slots/min_size_px 초안
└── templates_staging/ ← Stage 2: Jinja2 추상화 (legacy, 현재 미사용)
────────────────────────────────────────────────────────
🚧 프로모션 게이트 (사용자 수동 작업) 🚧
프로모션 (사용자 승인 후 에이전트가 실행)
────────────────────────────────────────────────────────
design_agent/ ← 본체 라이브러리 (에이전트 접근 금지)
── templates/
├── blocks/{category}/
│ └── {pattern_id}.html.j2 ← 사용자가 staging에서 이전
── catalog.yaml 사용자가 when/slots 등록
design_agent/templates/
── blocks/
├── new/ ← Figma 추출 블록 (에이전트가 작성)
├── svg/ ← 블록 공용 이미지 (에이전트가 정리)
── slide-base.html고정 슬라이드 배경
│ └── cards/, emphasis/, ... ← 기존 블록
└── catalog.yaml ← 블록 등록 (에이전트가 업데이트)
```
**중요:** 에이전트는 위 구분선 아래(`design_agent/templates/`)를 **절대 수정하지 않는다.** 그 영역은 사용자가 staging 결과물을 검수한 뒤 본인이 직접 프로모션한다.
**중요:** 프로모션(블록 생성, 이미지 정리, catalog 등록)은 사용자가 1:1 HTML을 검증하고 승인한 후에만 진행한다. 사용자 승인 없이 templates/ 를 수정하지 않는다.
## 금지 사항
@@ -116,6 +122,8 @@ design_agent/ ← 본체 라이브러리 (에이전트 접
- 블록 배경을 검정으로 표시 (미리보기는 항상 흰색 배경)
- **이미지 해석으로 gradient 방향 판단** (멀티모달 금지, 데이터로만 판단 — PROCESS-CONTROL.md 참조)
- **한 번에 여러 값 동시 수정** (gradient 각도와 border-radius 동시 변경 금지)
- **장식 요소를 이미지 슬롯(img src)으로 넣기** — gradient bar, ribbon, pill, badge, 오버레이 등은 CSS로 구현. AI가 색상/크기를 조정할 수 없는 이미지 의존 블록은 쓸 수 없다
- **사용자 승인 없이 templates/ 수정** — 프로모션은 사용자 승인 후에만
- **여러 프레임을 한 세션에 변환** (1세션 1프레임 원칙)
- **plus-darker 블렌드 사용** (Safari 전용 → multiply로 교체, RULES.md R10)
- **Figma 인벤토리/지문/군집 같은 사전 분류** (work-creating-work, 패턴은 bottom-up으로 발견)

View File

@@ -0,0 +1,165 @@
# Figma → HTML Agent
Figma 디자인 프레임을 **수학적으로 정확하게** HTML/CSS로 변환하고, design_agent의 **블록 라이브러리**로 축적하는 독립 에이전트.
---
## 이 에이전트는 뭘 하는가?
1. Figma 파일에서 프레임을 선택하면
2. MCP(Figma Dev Mode)로 구조/스타일/스크린샷을 가져와서
3. 수학적 계산(scale, gradient 변환, shadow 등)으로 1:1 HTML/CSS를 만들고
4. AI가 재디자인할 수 있는 블록(순수 CSS + Jinja2 슬롯)으로 변환하여
5. design_agent의 블록 라이브러리(`templates/blocks/`)에 편입한다
**핵심:** AI redesigner가 색상/크기/구조를 조정할 수 있도록, 장식 요소는 **CSS로 구현**하고 이미지는 crop/곡선/일러스트/사진만 남긴다.
---
## 폴더 구조
```
figma_to_html_agent/
├── README.md ← 이 파일 (사용법)
├── CLAUDE.md ← 에이전트 명세 (11개 원칙)
├── PROCESS.md ← 10단계 변환 절차 핸드북
├── MATH.md ← 수학 공식 (§1~9)
├── RULES.md ← CSS 보정 규칙 (R1~R16)
├── PROCESS-CONTROL.md ← 작업 규율 (찍어맞추기 금지 등)
├── blocks_index.md ← 변환 완료 도서관 + 디자인 인사이트
├── scripts/
│ └── gradient_math.py ← SVG → CSS gradient 수학 변환 도구
├── block-tests/ ← 1:1 HTML 변환물 (원본 검증용)
│ ├── {slug}.html ← Figma 원본과 100% 동일한 정적 HTML
│ ├── {slug}_flat.md ← 플래튼/이상탐지/변형축 메모
│ └── assets/ ← 이미지 에셋
│ ├── shared/ ← 해시 기반 공유 캐시
│ └── frame_{id}/ ← 프레임 전용 (legacy)
└── templates_staging/ ← Jinja2 템플릿 초안 (legacy, 현재 미사용)
```
### 산출물 최종 위치 (design_agent 본체)
```
design_agent/
└── templates/
└── blocks/
├── new/ ← Figma 추출 블록 (8개)
├── svg/ ← 블록 공용 이미지 (33개)
├── slide-base.html ← 고정 슬라이드 배경 (16:9)
└── cards/, emphasis/, headers/, media/, visuals/, BEPs/, redesign/
```
---
## 사용법
### 사전 준비
1. **Figma Desktop** 앱에서 대상 파일을 열고 **Dev Mode** 활성화
2. `.mcp.json`에 Figma Desktop SSE 서버 등록 확인:
```json
{ "url": "http://127.0.0.1:3845/sse" }
```
3. Claude Code 세션 시작
### 변환 실행 (10단계)
```
STEP 0 에이전트가 blocks_index.md 읽기 → 기존 패턴 확인
STEP 1 get_metadata — 구조 + bbox (XML)
STEP 2 get_design_context — gradient/filter/font (React+Tailwind)
STEP 3 get_screenshot — Figma 원본 PNG (검증용)
STEP 4 자산 다운로드 — block-tests/assets/shared/ 캐시
STEP 5 flat.md 작성 — bottom-up 플래튼 + 이상탐지 + 변형축
STEP 6 gradient 수학 변환 — scripts/gradient_math.py
STEP 7 HTML 작성 — 순수 CSS 우선, transform: scale() 균일 축소
STEP 8 검증 — Figma 스크린샷과 비교
STEP 9 저장 — block-tests/{slug}.html + flat.md
STEP 10 인덱스 업데이트 — blocks_index.md 1줄 추가
```
**상세:** [PROCESS.md](PROCESS.md) 참조
### 블록 프로모션 (block-tests → templates/blocks/)
1:1 HTML이 검증되면:
1. **CSS 전환** — SVG/PNG 장식 요소를 CSS gradient/border-radius/shadow로 변환
- CSS 전환 대상: gradient bar, ribbon, pill, badge, 오버레이 등
- 이미지 유지 대상: crop/프레임 배치(R16), 곡선 아크, 아이콘, 일러스트, 실사 사진
2. **블록 작성** — `templates/blocks/new/{pattern}.html`
- flex layout, width: 100%, Jinja2 슬롯
- 수학적 계산 주석 (Figma px → scale → CSS 값)
- 이미지는 `svg/` 폴더에서 참조 (`{{ slot | default('svg/파일명') }}`)
3. **이미지 정리** — `templates/blocks/svg/`에 의미 있는 이름으로 저장
4. **catalog.yaml 등록** — content_structure, when, not_for 작성
5. **blocks_index.md 상태** — `staged` → `promoted` 업데이트
---
## 핵심 원칙 요약
| # | 원칙 | 설명 |
|---|------|------|
| 1 | 수학적 계산만 | Figma 좌표 → scale → CSS. 시행착오 px 조정 금지 |
| 2 | Bottom-up | leaf 노드부터 → 2개씩 묶기 → 계층 쌓기 |
| 3 | 순수 CSS 우선 | AI가 색상/크기 조정할 수 있게. SVG는 곡선/필터만 |
| 4 | 이미지 해석 금지 | gradient 방향은 데이터로. "보니까 ~인 것 같다" 금지 |
| 5 | 하드코딩 금지 | 결과물을 고치지 말고 프로세스를 고친다 |
| 6 | 전면 재작성 금지 | 80점에서 2개 고칠 때 구조를 갈아엎지 말 것 |
| 7 | 모든 슬롯 optional | 블록이 다양한 MDX에 매칭될 수 있게 |
**상세:** [CLAUDE.md](CLAUDE.md), [PROCESS-CONTROL.md](PROCESS-CONTROL.md), [RULES.md](RULES.md)
---
## 도구
| 도구 | 용도 |
|------|------|
| Figma MCP `get_metadata` | 프레임 구조 + 절대 좌표 (XML) |
| Figma MCP `get_design_context` | gradient/filter/font (React+Tailwind 코드) |
| Figma MCP `get_screenshot` | Figma 원본 PNG |
| `scripts/gradient_math.py` | SVG linearGradient → CSS linear-gradient 수학 변환 |
| Pillow | 이미지 픽셀 분석 (gradient 색상 추출, 이미지 해석 아님) |
---
## 수학 공식 요약
| 공식 | 용도 | 상세 |
|------|------|------|
| `S = 1280 / W_figma` | 균일 축소 scale | MATH.md §1 |
| `CSS_angle = 90 - Figma_angle` | gradient 각도 변환 | MATH.md §2 |
| SVG `<linearGradient>` → CSS | gradient stop % 변환 | gradient_math.py |
| `plus-darker → multiply` | blend mode 호환 | RULES.md R10 |
| bbox 비율 ≠ 텍스트 비율 → 회전 | rotation 감지 | RULES.md R11 |
---
## 현재 상태 (2026-04-13)
### 프로모션 완료 블록 (8개)
| 패턴 | 원본 프레임 | 블록 위치 |
|------|-----------|---------|
| statement-pill-highlight | 1171281207 | blocks/new/ |
| stacked-arrow-list | 1171281180 | blocks/new/ |
| split-panel-numbered | 1171281202 | blocks/new/ |
| issues-paired-rows | 1171281194 | blocks/new/ |
| compare-vs-rows | 1171281195 | blocks/new/ |
| quadrant-2x2-issues | 1171281193 | blocks/new/ |
| cards-3col-persona | 1171281191 | blocks/new/ |
| cycle-3way-intersect | 1171281211 | blocks/new/ |
### 공용 이미지 (templates/blocks/svg/, 33개)
화살표, 아크, 뱃지, 아이콘, 배경 텍스처, 사진 등. 블록에서 `svg/파일명`으로 참조.
### 고정 슬라이드 배경
`templates/blocks/slide-base.html` — 16:9 (1280×720), 상단 제목 + 구분선 + 본문 영역 + 하단 결론 pill.

View File

@@ -111,11 +111,10 @@ Sub-patterns: **bullet-list-with-marker (R13)** — 각 컬럼의 bullet_items
---
## templates_staging 대기열
## 프로모션 완료
> 2번째 등장한 패턴이 여기에 들어옴. 사용자 검수 후 design_agent 본체로 프로모션.
(아직 없음)
> 8개 블록 모두 `templates/blocks/new/`로 프로모션 완료 (2026-04-13).
> 이미지 33개 `templates/blocks/svg/`로 통합. catalog.yaml 등록 완료.
---
@@ -252,4 +251,4 @@ Sub-patterns: **bullet-list-with-marker (R13)** — 각 컬럼의 bullet_items
| slug | frame | pattern | 위반 사항 | 처리 |
|------|-------|---------|--------|------|
| [prerequisites-3col](block-tests/prerequisites-3col.html) | 45:15 / Frame 1171281190 | 3-column-comparison (legacy) | R8 위반 (수동 ×0.60290 곱셈, transform: scale 미사용), R9 미적용 (gradient text만, div 패턴 없음) | 재변환 권장. 동일 패턴 등장 시 신규 변환물 작성 후 이 행은 archive 처리. |
| [prerequisites-3col](block-tests/prerequisites-3col.html) | 45:15 / Frame 1171281190 | prerequisites-3col | **promoted** → [block](../../templates/blocks/new/prerequisites-3col.html) + [catalog](../../templates/catalog.yaml) | 2026-04-13. legacy HTML 기반 블록화. 순수 CSS (이미지 없음). |

274
make_4plans.py Normal file
View File

@@ -0,0 +1,274 @@
"""MDX 02 최종 4안 — 처음부터 독립, CSS font-size override 금지.
글씨 크기: 블록 CSS 원래 값 그대로. 절대 변경 금지.
zone 크기: 글씨 크기에 맞게 조정 (글씨가 기준, 레이아웃이 맞춤).
안 1: 내 판단 + new/ only
상단: cards-3col-persona 하단좌: stacked-arrow-list 하단우: cards-3col-persona(3주체)
안 2: 내 판단 + new/ + 기존
상단: card-compare-3col 하단좌: card-numbered 하단우: table-simple-striped
안 3: Kei 판단 + new/ only (Kei 응답: 상단1 하단좌4 하단우2 결론6)
상단: cards-3col-persona 하단좌: split-panel-numbered 하단우: compare-vs-rows
안 4: Kei 판단 + new/ + 기존 (Kei 응답: 상단A 하단좌7 하단우H 결론6)
상단: card-compare-3col 하단좌: issues-paired-rows 하단우: compare-3col-badge
"""
import base64, re
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
BLOCKS = Path("templates/blocks")
SVG = BLOCKS / "svg"
OUT = Path("data/runs/mdx02_4plans_final")
OUT.mkdir(parents=True, exist_ok=True)
env = Environment(loader=FileSystemLoader(str(BLOCKS)), autoescape=False)
def b64(f):
p = SVG / f
if not p.exists(): return ""
ext = "svg+xml" if f.endswith(".svg") else "png"
return f"data:image/{ext};base64," + base64.b64encode(p.read_bytes()).decode()
def clean(h): return re.sub(r'<!--.*?-->', '', h, flags=re.DOTALL).strip()
img_uri = open("data/runs/mdx02_v4_test/slide_image.txt").read().strip()
title = "DX의 시행 목표 및 기대효과"
ft = '고품질의 성과품, 비용 절감, 시간 단축, 의사소통에 도움이 <em>안 되면 DX가 아니다</em>'
# slide-body: 1200×590px. 블록 글씨 크기에 맞춰 zone 크기 계산.
# 컨테이너 계산 → 글자수 예산 → overflow 방지
IW = 500 # 이미지
GAP = 10
# zone 높이: 블록 필요 높이 기반 계산
# new/ 블록(15~22px): 상단 244px, 하단 336px
# 기존 블록(13~15px): 상단 204px, 하단 310px
TH_NEW = 244
BH_NEW = 336
TH_OLD = 204
BH_OLD = 310
# 원문 텍스트
TOP = [
("안전과 품질", [
"시설물의 요구 성능을 설계-시공-운영 전 과정에서 <strong>디지털로 검증</strong>하여 <strong>안전성 확보</strong>",
"Copy &amp; Paste로 하향 평준화된 성과물의 <strong>하자 최소화</strong>로 <strong>고품질 성과물 제공</strong>"]),
("생산성 향상", [
"Analogue 기반 업무를 Digital 기반 프로세스로 전환하여 <strong>업무 속도·정확성·일관성 향상</strong>",
"건설 비용 및 유지관리비 절감, 건설 기간 단축, 인력투입 최소화를 통해 <strong>부가가치 제고</strong>"]),
("소통과 신뢰", [
"성과품과 Solution을 통한 협업 강화로 <strong>의사소통 효율 및 운영·유지관리</strong>의 <strong>편리성 증진</strong>",
"3D 모델 및 데이터 기반 검증을 통한 <strong>오류 최소화 및 Claim 예방</strong>으로 <strong>신뢰성 확보</strong>"]),
]
BL = [
("생산 방식", "수작업 의존의 반복 업무에서 벗어나, <strong>SW를 활용한 체계화된 방식</strong>으로 전환"),
("인지·검토", "2D 도면 해석 중심에서 <strong>3D 모델 기반의 직관적 인지·검토 체계</strong>로 전환"),
("협업 구조", "개별 문서 중심 협업에서 <strong>데이터 통합 기반의 정보 공유·관리 협업 환경</strong>으로 전환"),
("검증·대응", "사후 대응 중심의 문제 처리에서 <strong>사전 검증 중심의 예방적 업무 방식</strong>으로 전환"),
]
BR = {
"headers": ["구분", "발주자", "시공자", "설계자"],
"rows": [
["필요 역량", "실행 의지와 합리적 판단", "기술 투자와 운영 역량", "SW개발 투자 역량"],
["SW 기반 체계화", "행정 자동화로 생산성 향상", "체계적 공정관리로 신뢰성 확보", "설계 프로세스 체계화"],
["3D 기반 전환", "직관적 시각화로 품질 향상", "안전성 제고 및 관리 편의", "3D 검증으로 설계 오류 방지"],
["데이터 통합 협업", "원활한 의사소통으로 오류 감소", "협업 효율 및 서류 감소", "설계 신뢰도로 상호신뢰 증진"],
["사전 검증 관리", "민원·소송 등 사전 예방", "설계·시공 오류 예방", "설계 책임 리스크 감소"],
],
}
def wrap_slide(body_html, fname):
sb = clean((BLOCKS / "slide-base.html").read_text(encoding="utf-8"))
r = sb.replace('{{ title|default("슬라이드") }}', title)
r = r.replace('{{ title|default("슬라이드 제목") }}', title)
r = r.replace('{% block body %}{% endblock %}', body_html)
pill = b64("pill_scroll.png")
r = r.replace('{% if footer_text %}', '').replace('{% if footer_pill_bg %}', '')
r = r.replace('{{ footer_pill_bg }}', pill).replace('{% else %}', '')
r = r.replace('<div class="slide-footer-bg slide-footer--css"></div>', '')
li = r.rfind('{% endif %}')
if li > 0: r = r[:li] + r[li + len('{% endif %}'):]
r = r.replace('{% endif %}', '').replace('{{ footer_text|safe }}', ft)
r = r.replace('src="svg/bg_slide_texture.png"', f'src="{b64("bg_slide_texture.png")}"')
r = r.replace('src="svg/line_divider.svg"', f'src="{b64("line_divider.svg")}"')
out = OUT / fname
out.write_text(r, encoding="utf-8")
return out
def make_body(top_html, bl_html, br_html, th, bh):
"""slide-body 안에 배치. zone 높이는 블록 글씨 크기 기반 계산값."""
return f"""
<div style="height:{th}px;margin-bottom:{GAP}px;overflow:hidden;">
<div style="font-weight:700;font-size:13px;color:#1a365d;margin-bottom:4px;">DX의 궁극적 목표</div>
<div style="display:flex;gap:8px;height:{th-20}px;">
<div style="flex:1;overflow:hidden;">{top_html}</div>
<div style="width:{IW}px;flex-shrink:0;display:flex;flex-direction:column;justify-content:center;">
<img src="{img_uri}" style="width:100%;border-radius:8px;object-fit:contain;">
<div style="font-size:10px;color:#94a3b8;text-align:center;margin-top:2px;">DX의 궁극적 목표</div>
</div>
</div>
</div>
<div style="height:{bh}px;overflow:hidden;">
<div style="font-weight:700;font-size:13px;color:#1a365d;padding-bottom:4px;border-bottom:1px solid #e2e8f0;margin-bottom:4px;">DX 기반 Process 혁신에 따른 주체별 기대효과</div>
<div style="display:flex;gap:12px;height:{bh-28}px;">
<div style="flex:1;overflow:hidden;">
<div style="font-weight:700;font-size:12px;color:#1a365d;margin-bottom:4px;">업무 수행 과정(Process)의 변화</div>
{bl_html}
</div>
<div style="width:1px;background:#cbd5e1;flex-shrink:0;"></div>
<div style="flex:1;overflow:hidden;">
<div style="font-weight:700;font-size:12px;color:#1a365d;margin-bottom:4px;">DX 시행 주체별 기대효과</div>
{br_html}
</div>
</div>
</div>"""
# Jinja2 .items() 충돌 방지용 클래스
class _Cat:
def __init__(self, name, color, items): self.name=name; self.color=color; self.items=items
# ══════════════════════════════════════════
# 안 1: 내 판단 + new/ only — 글씨 크기 그대로
# ══════════════════════════════════════════
print("=== 안 1 ===")
t1 = clean(env.get_template("new/cards-3col-persona.html").render(personas=[
{"overlay_color":c,"label_line1":t,"label_color":"#1a365d","bullets":[{"text":b} for b in bs]}
for (t,bs),c in zip(TOP,["#dce8d4","#d4dce8","#e8dcd4"])
]))
# 뱃지/사진만 숨김 — 글씨 크기는 원래 값(15px body, 20px label)
t1 += "<style>.c3p-badge{display:none}.c3p-photo{display:none}.c3p-badge-label{position:relative;z-index:2;margin-bottom:4px}</style>"
b1l = clean(env.get_template("new/stacked-arrow-list.html").render(items=[
{"text":f"<strong>{k}</strong>: {v}","border_color":c}
for (k,v),c in zip(BL,["#fb5915","#e79000","#919f00","#0d6361"])
]))
# 헤더/장식만 숨김 — 글씨 크기(22px)는 원래 값
b1l += "<style>.sal-header{display:none}.sal-deco{display:none}</style>"
b1r = clean(env.get_template("new/cards-3col-persona.html").render(personas=[
{"overlay_color":c,"label_line1":h,"label_color":"#1a365d",
"bullets":[{"text":BR["rows"][ri][ci+1]} for ri in range(5)]}
for ci,(h,c) in enumerate([("발주자","#c8d8e8"),("시공자","#d8e8c8"),("설계자","#e8d8c8")])
]))
b1r = b1r.replace('block-c3p','block-c3p2').replace('c3p-','c3p2-')
b1r += "<style>.c3p2-badge{display:none}.c3p2-photo{display:none}.c3p2-badge-label{position:relative;z-index:2;margin-bottom:4px}</style>"
p1 = wrap_slide(make_body(t1, b1l, b1r, TH_NEW, BH_NEW), "plan1_new_only.html")
print(f" {p1.stat().st_size:,} bytes")
# ══════════════════════════════════════════
# 안 2: 내 판단 + mixed — 글씨 크기 그대로
# ══════════════════════════════════════════
print("=== 안 2 ===")
t2 = clean(env.get_template("cards/card-compare-3col.html").render(cards=[
{"title":t,"color":c,"bullets":bs}
for (t,bs),c in zip(TOP,["#1a365d","#15803d","#b45309"])
]))
b2l = clean(env.get_template("cards/card-numbered.html").render(items=[
{"title":k,"description":v,"color":c}
for (k,v),c in zip(BL,["#2563eb","#16a34a","#d97706","#7c3aed"])
]))
b2r = clean(env.get_template("tables/table-simple-striped.html").render(**BR))
p2 = wrap_slide(make_body(t2, b2l, b2r, TH_OLD, BH_OLD), "plan2_mixed.html")
print(f" {p2.stat().st_size:,} bytes")
# ══════════════════════════════════════════
# 안 3: Kei 판단 + new/ only (1,4,2,6)
# ══════════════════════════════════════════
print("=== 안 3 (Kei: 1,4,2,6) ===")
# 상단: 1. cards-3col-persona (안 1과 동일 블록, 동일 데이터)
t3 = clean(env.get_template("new/cards-3col-persona.html").render(personas=[
{"overlay_color":c,"label_line1":t,"label_color":"#1a365d","bullets":[{"text":b} for b in bs]}
for (t,bs),c in zip(TOP,["#dce8d4","#d4dce8","#e8dcd4"])
]))
t3 += "<style>.c3p-badge{display:none}.c3p-photo{display:none}.c3p-badge-label{position:relative;z-index:2;margin-bottom:4px}</style>"
# 하단좌: 4. split-panel-numbered (Kei 선택)
b3l = clean(env.get_template("new/split-panel-numbered.html").render(
categories=[_Cat(k,c,[v]) for (k,v),c in zip(BL,["#417d38","#008e52","#008970","#2563eb"])],
right_items=[],
))
b3l += "<style>.spn-header{display:none}.spn-right{display:none}.spn-mid{display:none}.spn-left{flex:1}</style>"
# 하단우: 2. compare-vs-rows (Kei 선택) — 3주체를 좌/우로
b3r = clean(env.get_template("new/compare-vs-rows.html").render(
main_labels={"left":"발주자","center":"구분","right":"시공자·설계자"},
rows=[{"category":r[0],"left_text":r[1],"right_text":f"{r[2]} / {r[3]}"} for r in BR["rows"]],
))
b3r += "<style>.cvr-header{display:none}.cvr-conclusion{display:none}</style>"
p3 = wrap_slide(make_body(t3, b3l, b3r, TH_NEW, BH_NEW), "plan3_kei_new.html")
print(f" {p3.stat().st_size:,} bytes")
# ══════════════════════════════════════════
# 안 4: Kei 판단 + mixed (A,7,H,6)
# ══════════════════════════════════════════
print("=== 안 4 (Kei: A,7,H,6) ===")
# 상단: A. card-compare-3col (안 2와 동일 블록)
t4 = clean(env.get_template("cards/card-compare-3col.html").render(cards=[
{"title":t,"color":c,"bullets":bs}
for (t,bs),c in zip(TOP,["#1a365d","#15803d","#b45309"])
]))
# 하단좌: 7. issues-paired-rows (Kei 선택)
b4l = clean(env.get_template("new/issues-paired-rows.html").render(
pill_bg=b64("pill_scroll.png"),
rows=[
{"left":{"label":BL[0][0],"text":BL[0][1]},"right":{"label":BL[1][0],"text":BL[1][1]}},
{"left":{"label":BL[2][0],"text":BL[2][1]},"right":{"label":BL[3][0],"text":BL[3][1]},"pills_bottom":True},
],
))
b4l += "<style>.ipr-header{display:none}</style>"
# 하단우: H. compare-3col-badge (Kei 선택)
b4r = clean(env.get_template("tables/compare-3col-badge.html").render(**BR))
p4 = wrap_slide(make_body(t4, b4l, b4r, TH_OLD, BH_OLD), "plan4_kei_mixed.html")
print(f" {p4.stat().st_size:,} bytes")
# ══════════════════════════════════════════
# Selenium 검증
# ══════════════════════════════════════════
print("\n=== Selenium 검증 ===")
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
options = Options()
options.add_argument('--headless')
options.add_argument('--window-size=1400,900')
options.add_argument('--force-device-scale-factor=2')
driver = webdriver.Chrome(options=options)
for fname in ["plan1_new_only.html","plan2_mixed.html","plan3_kei_new.html","plan4_kei_mixed.html"]:
path = OUT / fname
driver.get(f"file:///{path.resolve()}")
time.sleep(2)
driver.save_screenshot(str(path).replace(".html",".png"))
html = path.read_text(encoding="utf-8")
c = re.sub(r'data:image/[^;]+;base64,[A-Za-z0-9+/=]+','I',html)
# font-size override 확인 — !important가 있으면 위반
overrides = re.findall(r'font-size.*?!important', c)
j = re.findall(r'\{[%{].*?[%}]\}',c)
ts = ['안전','품질','생산','향상','소통','신뢰','생산 방식','인지','협업','검증',
'발주자','시공자','설계자','안 되면 DX가 아니다','DX의 궁극적 목표','Process 혁신','업무 수행']
m = [t for t in ts if t not in c]
print(f" [{fname}]")
print(f" Jinja:{len(j)} 텍스트:{len(ts)-len(m)}/{len(ts)} font-size override:{len(overrides)}")
if m: print(f" MISSING: {m}")
if overrides: print(f" OVERRIDE 위반: {overrides[:3]}")
driver.quit()
print("\n완료!")

306
make_mdx03.py Normal file
View File

@@ -0,0 +1,306 @@
"""MDX 03: slide-base + prerequisites-3col(상단) + process-product-2col(하단) + pill(결론).
블록 CSS 글씨 크기 그대로. font-size override 금지.
텍스트 원문 그대로. 요약/추론 금지.
"""
import base64, re
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
BLOCKS = Path("templates/blocks")
SVG = BLOCKS / "svg"
OUT = Path("data/runs/mdx03_final")
OUT.mkdir(parents=True, exist_ok=True)
env = Environment(loader=FileSystemLoader(str(BLOCKS)), autoescape=False)
def b64(f):
p = SVG / f
if not p.exists(): return ""
ext = "svg+xml" if f.endswith(".svg") else "png"
return f"data:image/{ext};base64," + base64.b64encode(p.read_bytes()).decode()
def clean(h): return re.sub(r'<!--.*?-->', '', h, flags=re.DOTALL).strip()
# ══════════════════════════════════════════
# 상단: prerequisites-3col — MDX 원문 그대로
# ══════════════════════════════════════════
class _Item:
def __init__(self, heading, desc): self.heading = heading; self.desc = desc
class _Col:
def __init__(self, **kw):
for k, v in kw.items(): setattr(self, k, v)
# 블록 CSS만 가져오고, HTML은 콘텐츠 items 수에 맞게 동적 생성
# prerequisites-3col의 CSS를 추출
_p3c_raw = (BLOCKS / "new" / "prerequisites-3col.html").read_text(encoding="utf-8")
_p3c_css = re.search(r'<style>(.*?)</style>', _p3c_raw, re.DOTALL).group(0)
# MDX 원문 그대로 — 각 열의 항목 수가 다름
cols_data = [
{
"name": "기술", "sub": "디지털",
"bar": "linear-gradient(180deg, #0D78D0 0%, #023056 100%)",
"hgrad": "linear-gradient(180deg, #0D78D0 0%, #134D7F 100%)",
"items": [
("Digital 기술(S/W, H/W)과 업무 Process의 통합", [
"기존 업무 프로세스에 다양한 디지털 기술을 접목하여 업무 수행",
"프로젝트 전반에 걸친 업무 프로세스의 연결 및 조율",
]),
("분야별 전문 지식(설계, 시공, 유지관리 등) 보유", [
"건설 전 단계에 대한 근본적인 이해와 지식 및 경험",
"최신 토목 기술 트랜드 및 표준 기준 등에 대한 높은 지식",
]),
],
},
{
"name": "사람", "sub": "역량",
"bar": "linear-gradient(180deg, #FF9A23 0%, #CC5200 100%)",
"hgrad": "linear-gradient(180deg, #CC5200 0%, #883700 100%)",
"items": [
("혁신적 사고방식과 창의적 문제 해결 능력", [
"기존 수행 방식과 관습적 사고 등에 의한 접근 방식 탈피",
"디지털 기술을 활용한 창의적, 혁신적인 솔루션 제시",
]),
("사용자 중심 사고와 DX 수행 경험", [
"사용자의 요구와 기대를 충족시키는 설계 및 구현",
"시행착오를 포함한 수행 경험과 사용자 경험(UX)을 반영한 해결 방안 제시",
]),
],
},
{
"name": "자연", "sub": "여건",
"bar": "linear-gradient(180deg, #39BE49 0%, #23742C 100%)",
"hgrad": "linear-gradient(180deg, #39BE49 0%, #1E6328 100%)",
"items": [
("지속적인 투자 및 실행 의지", [
"기술 도입 초기 단계에 필요한 인력·기간·비용 등의 대규모 투자",
"기술 고도화를 위한 지속적인 개선 및 투자 체계 구축",
"변화와 혁신을 통해 부가가치를 창출하려는 실행 의지와 추진력",
]),
],
},
]
# 동적 HTML 생성 — 블록 CSS 클래스 사용, items 수 동적
cols_html = ""
for col in cols_data:
items_html = ""
n = len(col["items"])
for i, (heading, bullets) in enumerate(col["items"]):
pct_h = int(95 / n)
pct_top = int(3 + i * (95 / n))
bul = "".join(f'<div class="bul">• {b}</div>' for b in bullets)
items_html += f"""
<div class="p3c-section" style="position:absolute;left:60px;right:6px;top:{pct_top}%;height:{pct_h}%;">
<div class="p3c-heading" style="background-image:{col['hgrad']}">{heading}</div>
<div class="p3c-desc">{bul}</div>
</div>"""
if i < n - 1:
line_top = pct_top + pct_h
items_html += f'<div class="p3c-mid-line" style="position:absolute;left:56px;right:0;top:{line_top}%;border-top:1.2px dashed #000;"></div>'
cols_html += f"""
<div class="p3c-col" style="flex:1;position:relative;height:100%;border-top:1.2px solid #000;border-bottom:1.2px solid #000;">
<div class="p3c-bar" style="background:{col['bar']};position:absolute;left:0;top:0;width:56px;height:100%;"></div>
<div class="p3c-vlabel-area" style="position:absolute;left:0;top:0;width:56px;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;z-index:3;">
<div class="p3c-vlabel">{col['name']}</div>
<div class="p3c-vlabel-sub">{col['sub']}</div>
</div>
{items_html}
</div>"""
top = f'<div class="block-p3c" style="display:flex;gap:12px;width:100%;height:100%;">{cols_html}</div>\n{_p3c_css}'
# ══════════════════════════════════════════
# 하단: process-product-2col — MDX 원문 그대로
# ══════════════════════════════════════════
# process-product-2col: 블록 CSS 사용 + HTML은 좌우 소제목이 같은 행에 오도록 Grid 재구성
_pp2_raw = (BLOCKS / "BEPs" / "process-product-2col.html").read_text(encoding="utf-8")
_pp2_css = re.search(r'<style>(.*?)</style>', _pp2_raw, re.DOTALL).group(0)
arrow_uri = b64("arrow_asis_tobe.png")
# 좌우 소제목을 행 단위로 대응시킴
# 행1: Analogue 기반 업무의 Digital화 (As-is→To-be) | Copy & Paste로 인해... 품질 향상
# 행2: GIS + BIM의 연계 | Analogue 기반 도서 외 Digital 기반 정보물 추가
# 행3: 사용자 중심의 Solution 제공 | Solution을 활용한 업무 효율화
# 2열 grid + ::before로 열 배경 gradient → 행 높이 동기화 + gradient 유지
bottom = f"""
<div class="pp2-grid-wrap" style="position:relative;width:100%;height:100%;">
<!-- 좌측 배경 gradient (::before 대신 absolute div) -->
<div style="position:absolute;left:0;top:0;width:50%;height:100%;background:linear-gradient(180deg,#ffffff 46%,#39311e 100%);z-index:0;"></div>
<!-- 우측 배경 gradient -->
<div style="position:absolute;left:50%;top:0;width:50%;height:100%;background:linear-gradient(0deg,#296b55 0%,#ffffff 56%);z-index:0;"></div>
<!-- 2열 grid — 행 높이 동기화 -->
<div style="position:relative;z-index:1;display:grid;grid-template-columns:1fr 1fr;width:100%;height:100%;">
<!-- 헤더 행 -->
<div class="pp2-header-bar pp2-header-bar--left" style="background:linear-gradient(270deg,#a4a096 0%,#39311e 100%);border-radius:0 24px 24px 0;display:flex;align-items:center;justify-content:center;height:30px;margin-top:4px;">
<span class="pp2-header-text pp2-header-text--left" style="font-size:13px;font-weight:900;color:#3e3523;">과정(Process)의 혁신</span>
</div>
<div class="pp2-header-bar pp2-header-bar--right" style="background:linear-gradient(90deg,#296b55 0%,#022017 100%);border-radius:24px 0 0 24px;display:flex;align-items:center;padding-left:20px;height:30px;margin-top:4px;">
<span class="pp2-header-text pp2-header-text--right" style="font-size:13px;font-weight:900;color:#ffffff;">결과(Product)의 변화</span>
</div>
<!-- 행1: Analogue Digital화 | 품질 향상 -->
<div style="padding:3px 16px;">
<div class="pp2-mid-title pp2-mid-title--left">Analogue 기반 업무의 Digital화</div>
<div style="display:flex;align-items:center;gap:4px;">
<div style="flex:1;">
<div class="pp2-body-text">• <strong>개념·문서·행정 절차 중심</strong></div>
<div class="pp2-body-text">• <strong>2D 도면, 전문가, 규정</strong></div>
<div class="pp2-body-text">• <strong>업무 구분(단절), 책임</strong></div>
</div>
<div style="flex-shrink:0;width:30px;text-align:center;"><img src="{arrow_uri}" style="width:30px;height:16px;object-fit:contain;" alt=""></div>
<div style="flex:1;">
<div class="pp2-body-text">• <strong>시각화된 목적물, 소통, 투명성 중심</strong></div>
<div class="pp2-body-text">• <strong>3D 모델, 참여자, 실체</strong></div>
<div class="pp2-body-text">• <strong>협업(융·복합), 창의성</strong></div>
</div>
</div>
</div>
<div style="padding:3px 16px;">
<div class="pp2-mid-title pp2-mid-title--right">Copy &amp; Paste로 인해 하향 평준화된 기존 성과물의 품질 향상</div>
<div class="pp2-body-text">• 과거 수작업으로 시행하면서 발생하던 오류 등의 최소화</div>
<div class="pp2-body-text">• 정확한 Data에 기반한 계획으로 고품질 성과물 도출</div>
</div>
<!-- 행2: GIS+BIM | Digital 정보물 -->
<div style="padding:2px 16px;">
<div class="pp2-mid-title pp2-mid-title--left">GIS + BIM의 연계</div>
<div class="pp2-body-text">• 지리·지형·지반 등 위치정보(GIS)와 3D모델(형상, 속성정보) 기반의 건설 정보를 포함하는 BIM의 연계를 통한 업무 프로세스의 혁신</div>
</div>
<div style="padding:2px 16px;">
<div class="pp2-mid-title pp2-mid-title--right">Analogue 기반 도서 외 Digital 기반 정보물 추가</div>
<div class="pp2-body-text">• 기존 성과물(도면, 수량, 계산서, 시방서 등)에 3D 모델, Simulation 등의 Digital 기반 정보물 추가</div>
</div>
<!-- 행3: Solution -->
<div style="padding:2px 16px;">
<div class="pp2-mid-title pp2-mid-title--left">사용자 중심의 Solution 제공</div>
<div class="pp2-body-text">• 서로 다른 S/W로 작성되어 분절화된 Analogue 방식의 성과물과 정보물을 연계할 수 있는 설계·시공 Solution 제공</div>
</div>
<div style="padding:2px 16px;">
<div class="pp2-mid-title pp2-mid-title--right">Solution을 활용한 업무 효율화</div>
<div class="pp2-body-text">• Engn. Solution을 통해 성과물에 관한 이슈를 함께 검토·논의하는 협업 환경 조성</div>
<div class="pp2-body-text">• 건설 단계별 정보를 디지털 데이터로 축적하여, 건설 전 과정을 통합관리</div>
</div>
</div>
</div>
{_pp2_css}
"""
# ══════════════════════════════════════════
# slide-base 조립
# ══════════════════════════════════════════
title = "DX 실행 체계 구축 방안"
ft = 'DX는 필요한 요건과 체계를 갖춘 후 시행해야만 그 효과를 <em>기대할 수 있다</em>'
# font_hierarchy: key_msg=14, core=12, bg=12, sidebar=11
# zone제목=13px, 블록 heading=12px, 블록 desc=11px
font_override = """<style>
/* prerequisites-3col: font_hierarchy 적용 + 높이 채움 */
.p3c-heading { font-size: 12px !important; line-height: 1.5 !important; }
.p3c-desc { font-size: 11px !important; line-height: 1.6 !important; }
.p3c-desc .bul { padding-left: 12px; text-indent: -12px; }
.p3c-vlabel { font-size: 14px !important; }
.p3c-vlabel-sub { font-size: 12px !important; }
.p3c-kanji { font-size: 16px !important; }
.p3c-col { min-height: 0 !important; height: 100% !important; }
.block-p3c { height: 100% !important; }
/* 한자 숨김 — MDX 원문에 없음 */
.p3c-kanji { display: none !important; }
/* 색상바 내 라벨 영역을 바 전체로 */
.p3c-vlabel-area { width: 56px !important; }
/* section을 바 바로 옆으로 — 들여쓰기 2단 효과 */
.p3c-section { left: 60px !important; right: 6px !important; }
.p3c-mid-line { left: 56px !important; }
/* 자연(3번째 열): heading 1개이므로 하단 숨김 */
.p3c-col:nth-child(3) .p3c-section--top { height: 90% !important; }
.p3c-col:nth-child(3) .p3c-section--bottom { display: none !important; }
.p3c-col:nth-child(3) .p3c-mid-line { display: none !important; }
/* process-product-2col: font_hierarchy 적용 + 높이 채움 */
.pp2-header-text { font-size: 13px !important; letter-spacing: 0.8px !important; }
.pp2-mid-title { font-size: 12px !important; line-height: 1.5 !important; margin-top: 6px !important; }
.pp2-mid-title:first-child { margin-top: 0 !important; }
.pp2-body-text { font-size: 11px !important; line-height: 1.6 !important; padding-left: 12px !important; text-indent: -12px !important; }
.pp2-col { min-height: 0 !important; }
.pp2-header-bar { height: 30px !important; margin-top: 4px !important; }
.pp2-body { padding: 6px 16px !important; }
.block-pp2 { height: 100% !important; }
/* 우측 헤더 텍스트: 배경 gradient와 겹쳐서 안 보임 → 흰색 */
.pp2-header-text--right { color: #ffffff !important; }
/* 좌우 본문 영역 높이 맞춤 */
.pp2-body { display: flex !important; flex-direction: column !important; }
</style>"""
body = f"""
{font_override}
<div style="height:38%;margin-bottom:1%;padding-top:8px;">
<div style="font-weight:700;font-size:13px;color:#1a365d;margin-bottom:8px;">DX 시행을 위한 필수 요건</div>
<div style="height:calc(100% - 28px);padding:0 12px 0 24px;">{top}</div>
</div>
<div style="height:60%;margin-top:12px;">
<div style="font-weight:700;font-size:13px;color:#1a365d;margin-bottom:8px;">Process의 혁신과 Product의 변화</div>
<div style="height:calc(100% - 28px);padding:0 12px 0 24px;">{bottom}</div>
</div>
"""
sb = clean((BLOCKS / "slide-base.html").read_text(encoding="utf-8"))
r = sb.replace('{{ title|default("슬라이드") }}', title)
r = r.replace('{{ title|default("슬라이드 제목") }}', title)
r = r.replace('{% block body %}{% endblock %}', body)
pill = b64("pill_scroll.png")
r = r.replace('{% if footer_text %}', '').replace('{% if footer_pill_bg %}', '')
r = r.replace('{{ footer_pill_bg }}', pill).replace('{% else %}', '')
r = r.replace('<div class="slide-footer-bg slide-footer--css"></div>', '')
li = r.rfind('{% endif %}')
if li > 0: r = r[:li] + r[li + len('{% endif %}'):]
r = r.replace('{% endif %}', '').replace('{{ footer_text|safe }}', ft)
r = r.replace('src="svg/bg_slide_texture.png"', f'src="{b64("bg_slide_texture.png")}"')
r = r.replace('src="svg/line_divider.svg"', f'src="{b64("line_divider.svg")}"')
out = OUT / "final.html"
out.write_text(r, encoding="utf-8")
print(f"저장: {out} ({out.stat().st_size:,} bytes)")
# ══════════════════════════════════════════
# Selenium 검증
# ══════════════════════════════════════════
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
options = Options()
options.add_argument('--headless')
options.add_argument('--window-size=1400,900')
options.add_argument('--force-device-scale-factor=2')
driver = webdriver.Chrome(options=options)
driver.get(f"file:///{out.resolve()}")
time.sleep(2)
driver.save_screenshot(str(out).replace(".html", ".png"))
driver.quit()
# 텍스트 검증
c = re.sub(r'data:image/[^;]+;base64,[A-Za-z0-9+/=]+', 'I', r)
overrides = re.findall(r'font-size.*?!important', c)
j = re.findall(r'\{[%{].*?[%}]\}', c)
ts = ['기술','사람','자연','디지털','역량','여건',
'Digital 기술','혁신적 사고방식','지속적인 투자',
'Process의 혁신','Product의 변화',
'Analogue','Digital','GIS','BIM','Solution',
'기대할 수 있다']
m = [t for t in ts if t not in c]
print(f"Jinja:{len(j)} font-override:{len(overrides)} 텍스트:{len(ts)-len(m)}/{len(ts)}")
if m: print(f" MISSING: {m}")
else: print(" ALL OK")

48
run_mdx03_pipeline.py Normal file
View File

@@ -0,0 +1,48 @@
"""MDX 03을 기존 파이프라인으로 Stage 1.7까지 돌린 뒤, 산출물을 저장.
파이프라인 코드 그대로 사용:
- Stage 0: mdx_normalizer
- Stage 1A: kei_client.classify_content (Kei API)
- Stage 1B: kei_client.refine_concepts + generate_structured_text
- Stage 1.5a: space_allocator (컨테이너 계산 + font_hierarchy)
- Stage 1.7: block_reference (블록 선택)
Stage 2(조립)는 여기서 하지 않음 — 산출물만 저장.
"""
import asyncio
import json
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from src.pipeline import generate_slide
async def main():
mdx_path = Path("samples/mdx/03. DX 시행을 위한 필수 요건 및 혁신 방안.mdx")
content = mdx_path.read_text(encoding="utf-8")
print(f"MDX 03: {mdx_path.name}")
print(f"내용 길이: {len(content)}")
print()
start = time.time()
async for event in generate_slide(content, base_path=str(mdx_path.parent)):
ev_type = event.get("event", "")
data = event.get("data", "")
if ev_type == "progress":
print(f" {data}")
elif ev_type == "result":
elapsed = time.time() - start
print(f"\n완료! ({elapsed:.1f}초)")
if isinstance(data, dict):
run_id = data.get("run_id", "")
print(f"run_id: {run_id}")
print(f"결과: data/runs/{run_id}/")
elif ev_type == "error":
print(f" 에러: {data}")
asyncio.run(main())

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

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