# Design Agent 전체 구조 파악 리포트 **작성일:** 2026-03-27 **목표:** design_agent의 아키텍처, 파이프라인, 코드 구조를 체계적으로 이해 --- ## 📌 Design Agent란? **목적:** 텍스트/MDX 콘텐츠를 **시각적으로 구조화된 슬라이드 HTML**(1280×720px, 16:9)로 변환하는 독립 AI 에이전트. **핵심 특징:** - ✅ **콘텐츠 기반 동적 비중** — Kei가 매번 콘텐츠마다 본심/배경/첨부/결론의 비중을 판단 (고정값 없음) - ✅ **텍스트 우선 설계** — 디자인이 텍스트에 맞춤 (텍스트를 자르거나 비틀지 않음) - ✅ **전문가 판단** — Kei 실장이 꼭지 추출, 디자인 팀장이 레이아웃, 편집자가 텍스트 정리, 실무자가 디자인 조정 - ✅ **Kei API 필수** — 하드코딩 없음. 모든 판단은 AI의 사고. fallback 없음. 성공할 때까지 무한 재시도. - ✅ **블록 라이브러리** — 38개 블록, 6개 카테고리 (headers, cards, tables, visuals, emphasis, media) - ✅ **중간 산출물 추적** — 각 단계별 결과가 JSON으로 저장 (`data/runs/{timestamp}/`) --- ## 🏗️ 파이프라인: 5단계 + 중간 단계 ``` ┌─────────────────────────────────────────────────────────────┐ │ [입력] 텍스트 콘텐츠 (텍스트 붙여넣기 또는 파일 업로드) │ └────────────────────┬────────────────────────────────────────┘ ↓ ┌────────────────────────────┐ │ [1단계] Kei 실장 │ (Kei API / Opus) │ 꼭지 추출 + 정보구조 분석 │ │ ───────────────────────── │ │ 1A: 핵심 메시지, 꼭지 5개 │ │ 1B: 컨셉 구체화 │ │ 출력: step1_analysis.json │ │ step1b_concepts.json │ └────────┬───────────────────┘ ↓ ┌────────────────────────────┐ │ [O-1] 컨테이너 계산 │ (코드 / 결정론적) │ 비중 → px 확정 │ │ ───────────────────────── │ │ 출력: step1c_containers... │ └────────┬───────────────────┘ ↓ ┌────────────────────────────┐ │ [2단계] 디자인 팀장 │ │ 레이아웃 설계 + 블록 배치 │ │ ───────────────────────── │ │ Step A: 프리셋 선택 (규칙) │ │ reference 있음 │ │ → sidebar-right │ │ Step B: 블록 매핑 (Sonnet) │ │ 각 블록에 텍스트 │ │ 글자수 가이드 │ │ 출력: step2_layout.json │ └────────┬───────────────────┘ ↓ ┌────────────────────────────┐ │ [O-3] 블록 스펙 확정 │ (코드 / 결정론적) │ 항목수, 글자수, 폰트 │ │ ───────────────────────── │ │ 출력: step2c_block_specs.. │ └────────┬───────────────────┘ ↓ ┌────────────────────────────┐ │ [3단계] 텍스트 편집자 │ (Kei API / Opus) │ 각 슬롯에 텍스트 정리 │ │ ───────────────────────── │ │ • 팀장의 글자 수 가이드 │ │ • 의미 우선 │ │ • 원본 보존 + 편집 │ │ 출력: step3_filled_blocks. │ └────────┬───────────────────┘ ↓ ┌────────────────────────────┐ │ [4단계] 디자인 실무자 │ (Sonnet + Jinja2) │ CSS 조정 + HTML 조립 │ │ ───────────────────────── │ │ • 텍스트에 맞게 디자인 │ │ • 이미지/표 처리 │ │ • HTML 렌더링 │ │ 출력: step4_rendered.html │ └────────┬───────────────────┘ ↓ ┌────────────────────────────┐ │ [Phase L] 렌더링 측정 │ (Selenium) │ 실제 높이 측정 │ │ ───────────────────────── │ │ • overflow 감지 │ │ • 피드백 루프 (미완성) │ │ 출력: step4_measurement_.. │ └────────┬───────────────────┘ ↓ ┌────────────────────────────┐ │ [5단계] Kei 최종 검수 │ (Opus 멀티모달) │ 스크린샷 보고 검증 │ │ ───────────────────────── │ │ 출력: step5_screenshot.txt │ └────────┬───────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ [출력] 완성 슬라이드 HTML (final.html) │ │ + 시각화 리포트 (report.html) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## 👥 5가지 역할 (Role) ### 1️⃣ Kei 실장 (Opus) **역할:** 전략과 콘텐츠 분석 **소유 단계:** 1A, 1B, 3, 5 | 단계 | 하는 일 | 출력 | |------|--------|------| | **1A** | 꼭지 5개 추출, 핵심 메시지, 본심/배경/첨부/결론 비중 판단 | `step1_analysis.json` | | **1B** | 각 꼭지의 relation_type, expression_hint, source_data | `step1b_concepts.json` | | **3** | 팀장의 글자 수 가이드를 참고하며 원본 보존하며 텍스트 정리 | `step3_filled_blocks.json` | | **5** | 최종 슬라이드의 스크린샷로 보고 최종 검수 | `step5_screenshot.txt` | **원칙:** - 비중이 모든 것을 결정 (본심 60%, 배경 20%, 첨부 10%, 결론 10% 등) - 콘텐츠마다 비중은 달라짐 (고정값 없음) - 하드코딩 없음 - 매번 사고하여 판단 --- ### 2️⃣ 디자인 팀장 (Sonnet) **역할:** 레이아웃과 공간 배분 **소유 단계:** 2 (Step B), 2 (Step A는 코드) | 단계 | 하는 일 | 출력 | |------|--------|------| | **2A** | 규칙 기반 프리셋 선택 (코드가 수행) | 프리셋 CSS grid | | **2B** | 프리셋 내에서 블록 매핑, 글자 수 가이드 (Sonnet) | `step2_layout.json` | **원칙:** - 블록 타입 변경 불가 (Kei가 선택한 것 유지) - zone 배치만 담당 (body/sidebar/footer) - 텍스트 내용 건드리지 않음 --- ### 3️⃣ 텍스트 편집자 (Opus) **역할:** 콘텐츠 정리 및 편집 **소유 단계:** 3 (별도 호출) **원칙:** - 팀장의 글자 수 가이드 참고 (하지만 의미가 우선) - 원본 텍스트 최대 보존 - 개조식(불릿, 번호) 사용 - 슬롯 빈칸 금지 --- ### 4️⃣ 디자인 실무자 (Sonnet + 코드) **역할:** 디자인 조정 및 HTML 조립 **소유 단계:** 4 (O-3 스펙 확정 후) **원칙:** - 텍스트를 자르지 않음. 디자인으로 텍스트 맞추기 - 이미지는 원본 그대로, 크기만 조절 - 표는 표로 유지 (다른 형태 전환 X) --- ### 5️⃣ Code (결정론적 알고리즘) **역할:** 계산 및 측정 **소유 단계:** O-1 컨테이너 계산, O-3 블록 스펙, Phase L 측정 | 단계 | 하는 일 | |------|--------| | **O-1** | Kei 비중 → px 확정. 고정식 계산. | | **O-3** | 컨테이너 크기 → 블록별 항목수/글자수/폰트 계산. 결정론적. | | **Phase L** | Selenium으로 실제 렌더링 높이 측정. 브라우저 엔진 기반. | --- ## 📂 소스 코드 구조 ``` src/ ├── main.py ← FastAPI 서버, /api/generate 엔드포인트 ├── config.py ← 설정 (API key, Kei API URL, 슬라이드 크기) │ ├── ─ 파이프라인 핵심 ─ ├── pipeline.py ← 메인 파이프라인 (5단계 + 중간 단계) │ • generate_slide() — 비동기 제너레이터, SSE 이벤트 방출 │ • _retry_kei() — Kei API 무한 재시도 │ ├── ─ 단계별 모듈 ─ ├── kei_client.py ← 1단계: Kei API 호출 │ • classify_content() — 꼭지 추출 + 분석 (1A) │ • refine_concepts() — 컨셉 구체화 (1B) │ • call_kei_overflow_judgment() — Phase L 피드백 │ • call_kei_final_review() — 최종 검수 (5) │ ├── space_allocator.py ← O-1, O-3: 컨테이너 & 블록 스펙 계산 │ • calculate_container_specs() — 비중 → px │ • finalize_block_specs() — 항목수/글자수/폰트 │ • calculate_trim_chars() — overflow px → 삭제 글자 │ ├── design_director.py ← 2단계: 레이아웃 설계 │ • select_preset() — 프리셋 규칙 선택 (Step A) │ • create_layout_concept() — 블록 매핑 (Step B, Sonnet) │ • LAYOUT_PRESETS — 4개 프리셋 (sidebar-right, two-column, hero-detail) │ • BLOCK_SLOTS — 모든 블록의 슬롯 정의 │ ├── content_editor.py ← 3단계: 텍스트 편집 │ • fill_content() — 각 슬롯에 텍스트 정리 (Kei API) │ ├── renderer.py ← 4단계: HTML 렌더링 │ • render_slide() — Jinja2로 HTML 생성 │ • _load_catalog_map() — catalog.yaml 블록 매핑 │ ├── ─ 보조 모듈 ─ ├── block_search.py ← FAISS 기반 블록 검색 │ • search_blocks_for_topics() — 콘텐츠 적합 블록 후보 │ ├── slide_measurer.py ← Phase L: Selenium 렌더링 측정 │ • measure_rendered_heights() — 실제 높이 측정 │ • format_measurement_for_kei() — 측정값 → 피드백 포맷 │ • capture_slide_screenshot() — 멀티모달용 스크린샷 │ ├── image_utils.py ← 이미지 처리 │ • get_image_sizes() — 이미지 크기 측정 │ • embed_images() — 이미지 데이터 URI 변환 │ ├── svg_calculator.py ← SVG 다이어그램 계산 │ └── sse_utils.py ← SSE 스트리밍 유틸 • stream_sse_tokens() — 토큰 스트리밍 처리 ``` --- ## 📊 데이터 흐름 ### 입력: SlideRequest ```json { "content": "텍스트/MDX 콘텐츠 (붙여넣기 또는 파일 업로드)", "base_path": "/path/to/images" (선택, 이미지 폴더 경로) } ``` ### 1단계 출력: step1_analysis.json ```json { "title": "슬라이드 제목", "core_message": "핵심 메시지 한 줄", "total_pages": 1, "info_structure": "정보 구조 설명", "page_structure": { "본심": {"topic_ids": [3], "weight": 0.60}, "배경": {"topic_ids": [1, 2], "weight": 0.20}, "첨부": {"topic_ids": [4], "weight": 0.10}, "결론": {"topic_ids": [5], "weight": 0.10} }, "topics": [ { "id": 1, "title": "개념 혼용의 현실", "purpose": "문제제기", "role": "flow", "emphasis": false, "layer": "intro" } ] } ``` ### 2단계 출력: step2_layout.json ```json { "preset": "'header header' 'body sidebar' 'footer footer'", "blocks": [ { "area": "body", "type": "callout-warning", "topic_id": 1, "purpose": "문제제기", "reason": "..." } ], "overflow": [ { "area": "body", "overflow_px": 100, "budget_px": 490 } ] } ``` ### 3단계 출력: step3_filled_blocks.json ```json { "blocks": [ { "type": "callout-warning", "topic_id": 1, "data": { "icon": "⚠️", "title": "DX와 BIM의 개념적 혼용 문제", "description": "건설산업에서 DX와 BIM이 명확히 정립되지 않은 채 혼용..." }, "char_count": 124 } ] } ``` ### 최종 출력: final.html 완성된 슬라이드 HTML (1280×720px, 16:9 비율) --- ## 🎨 프리셋 시스템 (2단계 Step A) 규칙 기반 자동 선택: | 프리셋 | 조건 | CSS Grid | 사용 | |--------|------|---------|------| | **sidebar-right** | reference 꼭지 1개 이상 | `"title title" "body sidebar" "footer"` (65fr 35fr) | 용어 정의, 참조 정보가 있을 때 | | **two-column** | 모든 flow 꼭지가 대등한 비교 | `"title title" "left right" "footer"` (1fr 1fr) | 좌우 비교 구조 | | **hero-detail** | 고강조 꼭지 1개 + 나머지 보조 | `"title" "hero" "detail" "footer"` | 하나의 큰 내용 + 상세 | | **single-column** | 모든 꼭지가 flow, 순차적 | `"title" "body" "footer"` (1fr) | 일반적인 순차 흐름 | --- ## 🏷️ 블록 라이브러리 (38개) | 카테고리 | 개수 | 용도 | 예시 | |---------|------|------|------| | **headers** | 5 | 타이틀, 꼭지 헤더 | section-title-with-bg, topic-left-right | | **cards** | 10 | 항목 나열, 카드 그리드 | card-image-3col, card-compare-3col | | **tables** | 3 | 비교표, 데이터 테이블 | data-table, comparison-table | | **visuals** | 6 | SVG 다이어그램 | venn-diagram, flow-process, relationship | | **emphasis** | 10 | 강조, 인용, 결론 | callout-warning, quote-block, banner-gradient | | **media** | 5 | 이미지/사진 | image-frame, full-width-image | 각 블록은 `catalog.yaml`에 정의됨: - `id`: 블록 ID - `template`: HTML 템플릿 경로 - `height_cost`: 크기 등급 (compact/medium/large/xlarge) - `slots`: 필수/선택 슬롯 - `when`: 사용 조건 - `not_for`: 금지 조건 --- ## 🔧 기술 스택 ### Backend (Python) ``` FastAPI ≥0.115 — 웹 서버 uvicorn ≥0.30 — ASGI 서버 Jinja2 ≥3.1 — HTML 템플릿 Pydantic ≥2.0 — 데이터 검증 Anthropic ≥0.40 — Claude API httpx ≥0.27 — HTTP 클라이언트 sse-starlette ≥2.0 — SSE 스트리밍 Selenium — Headless Chrome 제어 (Phase L) Pillow ≥10.0 — 이미지 처리 PyYAML ≥6.0 — YAML 파싱 (catalog.yaml) ``` ### Frontend (React + Vite) ```javascript React 18 — UI 프레임워크 Vite — 번들러 Tailwind CSS — 스타일링 ``` ### Design Assets ``` static/ ├── base.css — 슬라이드 기본 스타일 ├── tokens.css — 디자인 토큰 (색상, 폰트, 간격) ├── index.html — 프론트엔드 UI templates/ ├── catalog.yaml — 블록 라이브러리 정의 ├── slide-base.html — 슬라이드 기본 템플릿 └── blocks/ — 6개 카테고리 × 38개 블록 HTML ├── headers/ — 5개 ├── cards/ — 10개 ├── tables/ — 3개 ├── visuals/ — 6개 ├── emphasis/ — 10개 └── media/ — 5개 ``` --- ## 🔄 Kei API 연동 ### 무한 재시도 메커니즘 ```python async def _retry_kei(fn, *args, **kwargs): """성공할 때까지 무한 재시도""" while True: result = await fn(*args, **kwargs) if result is not None: return result await asyncio.sleep(10) # 10초 대기 후 재시도 ``` **특징:** - fallback 없음 (모든 판단을 Kei가 함) - 타임아웃 없음 (10분이든 1시간이든 기다림) - 성공 또는 명시적 실패만 가능 ### 호출 파이프라인 ``` design_agent (Sonnet) ↓ HTTP POST persona_agent (Opus) ↓ (구조화된 프롬프트) Kei 페르소나 (고급 OS 모델) ↓ (판단) JSON 응답 (역직렬화) ↓ design_agent 계속 진행 ``` --- ## 📁 폴더 구조 ``` design_agent/ ├── CLAUDE.md ← 프로젝트 규칙 & 아키텍처 (이 파일) ├── README.md ← 사용자 가이드 ├── PLAN.md ← 실행 계획 (태스크 목록) ├── PROGRESS.md ← 진행 상황 추적 ├── pyproject.toml ← Python 의존성 정의 ├── package.json ← Node.js 의존성 (프론트 관련) │ ├── src/ ← Python 소스 코드 (백엔드) │ ├── main.py — FastAPI 서버 │ ├── pipeline.py — 파이프라인 메인 로직 │ ├── kei_client.py — Kei API 호출 │ ├── design_director.py — 2단계 레이아웃 │ ├── space_allocator.py — O-1, O-3 계산 │ ├── content_editor.py — 3단계 텍스트 정리 │ ├── renderer.py — 4단계 HTML 렌더링 │ ├── block_search.py — FAISS 블록 검색 │ ├── slide_measurer.py — Phase L 측정 │ ├── image_utils.py — 이미지 처리 │ ├── sse_utils.py — SSE 스트리밍 │ └── config.py — 설정 │ ├── templates/ ← HTML 템플릿 & 블록 정의 │ ├── catalog.yaml — 38개 블록 라이브러리 │ ├── slide-base.html — 슬라이드 기본 구조 │ └── blocks/ — 블록별 HTML │ ├── headers/ — 5개 헤더 블록 │ ├── cards/ — 10개 카드 블록 │ ├── tables/ — 3개 테이블 블록 │ ├── visuals/ — 6개 다이어그램 블록 │ ├── emphasis/ — 10개 강조 블록 │ └── media/ — 5개 미디어 블록 │ ├── static/ ← CSS & 프론트엔드 │ ├── base.css — 슬라이드 스타일 │ ├── tokens.css — 디자인 토큰 │ └── index.html — 프론트엔드 UI │ ├── data/ ← 중간 산출물 & 인덱스 │ ├── runs/{timestamp}/ — 각 실행의 단계별 결과 │ │ ├── step1_analysis.json │ │ ├── step1b_concepts.json │ │ ├── step1c_containers.json │ │ ├── step2_layout.json │ │ ├── step2c_block_specs.json │ │ ├── step3_filled_blocks.json │ │ ├── step4_rendered.html │ │ ├── step4_measurement_round*.json │ │ ├── step5_screenshot.txt │ │ └── final.html │ ├── block_index.faiss — FAISS 인덱스 │ └── block_metadata.json — 메타데이터 │ ├── docs/ ← 문서 │ ├── BLOCKS.md — 블록 라이브러리 설명 │ └── OUTPUTS.md — 산출물 구조 │ ├── scripts/ ← 유틸리티 스크립트 │ ├── build_block_index.py — FAISS 인덱스 빌드 │ └── generate_run_report.py — 실행 리포트 생성 │ └── .env — 환경 변수 (API key, Kei API URL) ``` --- ## 🚀 실행 흐름 (전체) ``` 1. 사용자 입력 POST /api/generate { "content": "콘텐츠...", "base_path": "/path/to/images" } 2. SSE 스트리밍 시작 event: progress data: "1/5 Kei 실장이 꼭지를 추출 중..." 3. 파이프라인 실행 (pipeline.py) ├─ [1단계] kei_client.py: classify_content() │ → Kei API 호출, 꼭지 5개 추출 │ ├─ [1B] kei_client.py: refine_concepts() │ → Kei API, 컨셉 구체화 │ ├─ [O-1] space_allocator.py: calculate_container_specs() │ → 결정론적 계산, 비중 → px │ ├─ [2] design_director.py: │ ├─ select_preset() — 규칙 기반 프리셋 선택 │ └─ create_layout_concept() — Sonnet: 블록 매핑 │ ├─ [O-3] space_allocator.py: finalize_block_specs() │ → 블록별 스펙 확정 │ ├─ [3] content_editor.py: fill_content() │ → Kei API: 텍스트 정리 │ ├─ [4] renderer.py: render_slide() │ → Jinja2: HTML 생성 │ ├─ [Phase L] slide_measurer.py: measure_rendered_heights() │ → Selenium: 실제 높이 측정 │ └─ [5] kei_client.py: call_kei_final_review() → Opus 멀티모달: 최종 검수 4. 최종 출력 event: result data: { "html": "...", "runs_id": "1774588279782" } 5. 프론트엔드 ├─ iframe에 HTML 미리보기 ├─ 다운로드 버튼 (final.html) └─ 리포트 버튼 (report.html) ``` --- ## 🔍 핵심 원칙 | 원칙 | 설명 | 구현 | |------|------|------| | **비중 우선** | 비중이 모든 것을 결정 | Kei가 page_structure 판단 → space_allocator 계산 | | **텍스트 기준** | 디자인이 텍스트에 맞춤 | renderer.py에서 폰트/여백 조정 | | **Kei API 필수** | fallback 없음, 무한 재시도 | pipeline.py _retry_kei() | | **결정론적 계산** | 규칙 기반 자동화 | space_allocator.py, design_director.py Step A | | **역할 분리** | 각 역할이 자신의 영역만 담당 | CLAUDE.md Table "역할 분리" | | **원본 보존** | 콘텐츠 의미 손상 X | content_editor.py 원칙 | | **중간 산출물** | 모든 단계를 추적 가능하게 | _save_step() 함수 | --- ## 📊 Phase별 상태 | Phase | 내용 | 상태 | |-------|------|------| | **A~D** | 슬라이드 품질 핵심 | ✅ 완료 | | **G** | Kei API 통신 정상화 | ✅ 완료 | | **H** | 스토리라인 설계 기반 전환 | ✅ 완료 | | **I** | 정합성 복구 + 넘침 처리 | ✅ 완료 | | **J** | 블록 선택 권한 구조 재정의 | ✅ 완료 | | **K** | communicative role 기반 위계 | ✅ 완료 | | **K-1** | 파이프라인 스텝별 중간 산출물 저장 | ✅ 완료 | | **L** | Selenium 렌더링 측정 + 피드백 루프 | ⚠️ 진행 중 | | **M** | Kei 비중 시스템 강화 | ✅ 완료 | | **N** | 4대 핵심 문제 해결 | ✅ 완료 | | **O** | 컨테이너 기반 레이아웃 시스템 | 🔄 진행 중 | --- ## 🎯 결론 **Design Agent는:** - ✅ 전문가 사고 기반 (Kei 실장, 디자인 팀장, 편집자, 실무자의 역할 분리) - ✅ 결정론적 계산과 AI 판단의 조합 - ✅ 모든 단계를 추적 가능하게 설계 - ✅ 텍스트 우선, 디자인은 그에 맞춤 - ✅ 비중 기반 동적 레이아웃 - ✅ Kei API 필수, fallback 없음 현재 진행 상태: - **Core 완성:** 1~4단계, O-1, O-3 명확히 작동 - **진행 중:** Phase L (피드백 루프), Phase O (세부 개선) - **테스트 필요:** BF-4, BF-5, BF-6, BF-7 (렌더링 버그)