Files
C.E.L_Slide_test2/docs/history/ARCHITECTURE-PHASE-T.md
kyeongmin 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

37 KiB
Raw Blame History

Design Agent Architecture — Phase T

MDX 원본 문서 → 고정 크기 HTML 슬라이드(1280×720px) 자동 생성 파이프라인 폰트 위계가 먼저, 컨테이너가 따라간다 — 텍스트 보존 · 폰트 위계 강제 · 디자인 요소 크기를 수학적으로 역산


1. 핵심 설계 원칙

1.1 AI vs 코드 역할 분리

역할 담당 해당 Stage
콘텐츠 판단 · 분류 AI (Kei Persona API / Opus) 1A, 1B
폰트 위계 확정 + 컨테이너 비율 역산 코드 (결정론적 수학) 1.5a
블록 선택 · 변형 결정 코드 (키워드 매칭 + 룩업 테이블) 1.7
블록 schema 기반 디자인 예산 역산 코드 (결정론적 수학) 1.5b
HTML 생성 AI (Claude Sonnet 4) 2
텍스트·구조 검증 (L1~L3) 코드 (kiwipiepy + regex) 2 직후
실측 렌더링 (L4) Selenium (headless Chrome) 3 직후
시각 품질 평가 (L5) AI (Opus Vision) 4
HTML 조립 · 서빙 코드 3, 5

AI가 공간을 볼 수 없는 근본적 한계를 코드(수학적 예산 역산)로 보완하는 구조. LLM이 참고 HTML 구조를 70~90% 복사하는 경향을 장점으로 활용 — "디자인 레퍼런스" 프레이밍.

1.2 폰트 위계 (Phase T 핵심 — 이것이 모든 계산의 출발점)

Phase S에서 폰트 크기가 중요도와 완전히 역전됨 (sidebar 14px > key-msg 11px). Phase T는 위계를 먼저 확정하고, 컨테이너가 위계에 맞춰지는 방향으로 전환.

영역 중요도 폰트 범위 강제 규칙
핵심 (key-msg) 1위 14px bold 무조건 한 줄, 슬라이드에서 가장 큰 폰트
본문 (core) 2위 12px 본문 텍스트 기본
배경 (bg) 3위 10-12px 텍스트 양에 따라 범위 내 조정
첨부 (sidebar) 4위 9-11px 참고 자료, 가장 작아도 됨

검증 기준: font_size(핵심) > font_size(본문) ≥ font_size(배경) > font_size(첨부) — 위반 시 에러.

1.3 파이프라인 운영 패턴

누적 컨텍스트 객체 (Pydantic BaseModel)

각 Stage가 독립 JSON을 읽고 쓰는 대신, PipelineContext 하나가 파이프라인을 따라가며 점진적으로 확장. T-0 조사 결과 Pydantic BaseModel 채택 (dataclass 아님) — model_dump_json() 직렬화, validate_assignment=True 타입 검증.

context.normalized.clean_text                           # Stage 0
context.normalized.title                                # Stage 0
context.normalized.images                               # Stage 0
context.normalized.popups                               # Stage 0
context.normalized.tables                               # Stage 0
context.analysis.core_message                           # Stage 1A
context.analysis.topics[0].source_hint                  # Stage 1A
context.analysis.page_structure                         # Stage 1A
context.topics[0].relation_type                         # Stage 1B
context.topics[0].expression_hint                       # Stage 1B
context.topics[0].source_data                           # Stage 1B
context.font_hierarchy                                  # Stage 1.5a
context.container_ratio                                 # Stage 1.5a (동적 body:sidebar 비율)
context.containers["본심"].text_budget                   # Stage 1.5a
context.references["본심"].block_id                      # Stage 1.7
context.references["본심"].design_reference_html         # Stage 1.7
context.containers["본심"].design_budget                 # Stage 1.5b (블록 선택 후 재계산)
context.generated_html                                  # Stage 2
context.rendered_html                                   # Stage 3
context.measurement                                     # Stage 4
context.quality_score                                   # Stage 4

각 Stage 공통 실행 패턴

async def run_stage(stage_fn, context, stage_name, max_retries=1):
    for attempt in range(max_retries + 1):
        result = await stage_fn(context)
        errors = result.get("_errors", [])
        if not errors:
            # Pydantic: model_copy(update=...) 사용
            context = context.model_copy(update=result)
            context.save_snapshot(stage_name)
            return context
        context.errors.append({"stage": stage_name, "attempt": attempt, "errors": errors})
        if attempt < max_retries:
            context.retry_feedback = build_retry_feedback(stage_name, errors)
    raise StageFailure(stage_name, errors)

에러 3등급 분류

등급 의미 대응
FATAL 복구 불가 (원본 문제, JSON 파싱 실패) 파이프라인 중단
RETRYABLE AI 재시도로 해결 가능 (분류 오류, 누락) Self-Refine 피드백 포함 재요청 (최대 2회)
ADJUSTABLE 코드로 자동 조정 가능 (높이 부족, 비율 초과) 자동 조정 후 경고 기록

스냅샷 저장

data/runs/{run_id}/step{N}_context.json — run_id는 YYYYMMDD_HHMMSS timestamp. Pydantic model_dump_json()으로 직렬화. diff step1a_context.json step1b_context.json으로 추적.


2. 파이프라인 (11 Stage)

Stage 0: MDX 표준화

  • 담당: 코드
  • 신규 파일: src/mdx_normalizer.py
  • 라이브러리: python-frontmatter + markdown-it-py + mdit-py-plugins (총 ~1MB)
  • 입력: 원본 MDX 텍스트
  • 처리 (4-Layer 파서):
    • Layer 1: python-frontmatter.parse()(metadata_dict, body_str) 분리. title 추출.
    • Layer 2: 코드블록 보호 (backtick 10→3 순서로 fenced block → placeholder) → MDX 전용 패턴 처리:
      • Astro :::directive[핵심요약]...[/핵심요약] 마커
      • <details><summary>제목</summary>내용</details> → popups[] 추출
      • JSX style={{}}, import/export 제거
    • Layer 3: markdown-it-py AST 파싱 (js-default 프리셋, table 기본 포함):
      • heading 토큰 → 섹션 구조 추출 (tag, level, content, source line)
      • image 토큰 → images[] 추출 (alt, src)
      • table 토큰 → tables[] 추출 (header, rows)
      • 코드블록 placeholder 복원
    • Layer 4: 텍스트 정리 — 남은 HTML 태그 제거, 빈 줄 정리, 최종 clean_text
  • 출력:
    {
        "clean_text": str,         # 정규화된 순수 텍스트
        "title": str,              # frontmatter 제목
        "images": [{"alt": str, "path": str}],
        "popups": [{"title": str, "content": str}],
        "tables": [{"header": list, "rows": list}],
        "sections": [{"level": int, "title": str, "content": str}]  # ## 기준 섹션 분리
    }
    
  • 검증:
    • clean_text 비어있지 않음
    • ## 섹션 최소 1개
    • 원본 대비 30% 이상 텍스트 보존 (과도한 제거 방지)
    • images[] 수 = 원본 ![ 패턴 수
    • popups[] 수 = 원본 <details> 패턴 수
  • 주의: 기존 normalize_mdx()r"^## \d+\.\s*"r"^## \d+\.\s+" 수정 (공백 1개 이상 필수)
  • 저장: context.normalized.*

Stage 1A: Kei 꼭지 추출

  • 담당: AI (Kei Persona API, localhost:8000, Opus — SSE 스트리밍)
  • 입력: context.normalized.clean_text (Stage 0에서 정규화된 텍스트)
  • 처리: Kei가 콘텐츠를 읽고 꼭지 분류 + 스토리라인 설계
  • 출력:
    • topics[] — id, title, purpose, role, layer, weight, source_hint (원본 MDX 섹션 참조)
    • page_structure — { "본심": {topic_ids, weight}, "배경": {...}, "첨부": {...}, "결론": {...} }
    • core_message — 슬라이드 핵심 메시지 한 줄
  • 검증 (Pydantic + 코드 대조):
    • 형식: weight 합 0.9~1.1 범위, 본심 weight ≥ 0.3, 필수 필드 존재, topics > 0
    • 내용 대조: 원본 ## 섹션 수 vs topic 수 비교 — 차이가 크면 분류 오류 가능성
    • 내용 대조: topic summary 키워드가 원본 해당 섹션에 실제 존재하는지 (kiwipiepy)
    • 실패 시 RETRYABLE → Self-Refine 피드백 포함 재요청 (최대 2회)
  • 저장: context.analysis, context.topics, context.page_structure

Stage 1B: 컨셉 구체화

  • 담당: AI (Kei Persona API, Opus — SSE 스트리밍)
  • 입력: context.normalized.clean_text + context.topics (Stage 1A 결과)
  • 처리: 각 꼭지에 관계 유형, 표현 힌트, 원본 텍스트 참조 부여
  • 출력: topics에 아래 필드 병합
    • relation_type7개 enum: hierarchy / cause_effect / comparison / sequence / definition / inclusion / none
    • expression_hint — 디자인 방향 힌트 (3문장 구조: 관계 선언 + 콘텐츠 설명 + 시각 지침)
    • source_data — 원본 텍스트 참조
  • 검증 (Pydantic + 코드 대조 + 모순 탐지):
    • 형식: relation_type이 7개 enum 중 하나, expression_hint 비어있지 않음, source_data 비어있지 않음

    • 모순 결정 테이블:

      purpose 모순인 relation_type 이유
      결론강조 comparison, sequence 결론은 비교나 순서가 아님
      문제제기 sequence, definition 문제제기는 순서 나열이나 정의가 아님
      용어정의 hierarchy, cause_effect 정의 나열은 상하위나 인과가 아님
      구조시각화 none 시각화할 관계가 없으면 구조시각화가 아님
    • source_data 원본 대조: source_data 키워드가 원본 clean_text에 실제 존재하는지 (kiwipiepy). 없는 출처 감지 → 할루시네이션

    • relation_type 원본 대조: 한국어 관계 표현 패턴으로 검증

      relation_type 원본에 있어야 하는 패턴 (일부)
      comparison vs, 반면, 차이점, 에 비해, 와 달리, 상이, 구분
      sequence →, 이후, 단계, 먼저, 점진적, 과정, 를 거쳐
      hierarchy 상위, 하위, 속하, 범주, 구성요소, 체계, 계층
      inclusion 포함, 융합, 통합, 결합, 내포, 포괄, 연계
      cause_effect 때문에, 따라서, 결과, 로 인해, 초래, 야기, 기인
      definition 이란, 정의, 의미, 을 말한다, 라 함은, 용어
    • 실패 시 RETRYABLE → 모순/불일치 topic만 피드백 포함 재요청 (최대 2회)

  • 저장: context.topics[].relation_type, .expression_hint, .source_data

Stage 1.5a: 폰트 위계 확정 + 컨테이너 비율 역산

  • 담당: 코드 (AI 아님, 결정론적 수학)
  • 입력: page_structure weight + 각 영역의 source_data 텍스트 양
  • 핵심 원칙: 폰트가 먼저, 컨테이너가 따라간다

(1) 폰트 위계에서 필요 공간 계산

FONT_HIERARCHY = {
    "핵심":  {"min": 14, "max": 14, "weight": "bold"},
    "본심":  {"min": 12, "max": 12},
    "배경":  {"min": 10, "max": 12},
    "첨부":  {"min": 9,  "max": 11},
}

def calculate_required_space(role, content, font_size):
    """이 폰트 크기로 이 텍스트를 넣으려면 몇 px 필요한가?"""
    char_width_px = font_size * 0.947   # Pretendard 한글 실측 비율
    line_height_px = font_size * 1.5    # 본문 기준
    chars_per_line = available_width // char_width_px
    total_lines = len(content) // chars_per_line
    required_height = total_lines * line_height_px + padding
    return required_height

(2) 동적 body:sidebar 비율 역산

고정 65:35가 아니라 텍스트 양에서 역산:

def calculate_container_ratio(roles_text_volume, font_hierarchy):
    """폰트 위계를 지키면서 모든 텍스트가 들어가는 비율을 역산"""
    # 1. 각 역할의 위계 기준 폰트로 필요 공간 계산
    sidebar_need = calculate_required_space("첨부", sidebar_text, font_hierarchy["첨부"]["max"])
    body_need = sum(calculate_required_space(r, t, font_hierarchy[r]["max"])
                    for r, t in body_roles)

    # 2. sidebar 충전율로 비율 결정
    sidebar_capacity_at_35 = estimate_capacity(slide_width * 0.35, font_hierarchy["첨부"]["max"])
    fill_rate = len(sidebar_text) / sidebar_capacity_at_35

    if fill_rate < 0.5:
        ratio = (72, 28)   # sidebar 텍스트 적음 → body 확대
    elif fill_rate < 0.8:
        ratio = (68, 32)   # 보통
    else:
        ratio = (65, 35)   # 현재 유지

    return ratio  # (body_pct, sidebar_pct)

(3) 텍스트 예산 계산

비율 확정 후, 각 영역의 텍스트 예산:

def calculate_text_budget(container, content, font_size):
    char_width_px = font_size * 0.947
    line_height_px = font_size * 1.5
    inner_width = container.width_px - padding * 2
    inner_height = container.height_px - padding * 2

    chars_per_line = int(inner_width / char_width_px)
    max_lines = int(inner_height / line_height_px)
    max_chars = chars_per_line * max_lines

    source_chars = len(content)
    needs_compression = source_chars > max_chars

    return TextBudget(
        font_size=font_size,
        chars_per_line=chars_per_line,
        max_lines=max_lines,
        max_chars=max_chars,
        source_chars=source_chars,
        needs_compression=needs_compression,
    )

(4) 다단 레이아웃 판단

위계 범위 내 최소 폰트로도 텍스트가 안 들어가면 구조 변경:

1. 위계 기준 폰트(max)로 수용량 계산
2. 텍스트 양 > 수용량 → 폰트 1px 축소 (위계 min까지)
3. 최소 폰트로도 불가 → 레이아웃 변경 (1단→2단)
4. 2단으로도 불가 → 비율 조정 (sidebar 축소 → body 확대)
5. 비율 조정으로도 불가 → 텍스트 편집 필요 경고 (context.warnings에 기록)
  • 검증: height_px 합 ≤ 전체 높이, 폰트 위계 유지, 음수 없음
  • 저장: context.font_hierarchy, context.container_ratio, context.containers[].text_budget

Stage 1.7: 참고 블록 선택 + 변형 결정

  • 담당: 코드 (키워드 매칭 + 룩업 테이블, AI 아님)
  • 입력: 1B의 relation_type + expression_hint + 1.5a의 컨테이너 스펙 + catalog.yaml
  • 처리 4단계:

(1) relation_type → 블록 후보 (1차 필터)

catalog.yaml의 relation_types 필드로 필터:

candidates = [b for b in catalog.blocks
              if relation_type in b.relation_types or not b.relation_types]

(2) expression_hint → 블록 세분화 (2차 필터 — 키워드 포함 여부)

expression_hint는 긴 문장이므로 정확한 문자열 매칭이 아니라 키워드 포함(substring) 매칭:

VISUAL_TYPE_KEYWORDS = {
    "인과": {"keywords": ["인과", "현상->결과", "야기", "원인"], "blocks": ["callout-warning", "dark-bullet-list"]},
    "나열_병렬": {"keywords": ["독립적 나열", "병렬 나열", "개별 증거"], "blocks": ["dark-bullet-list", "card-icon-desc"]},
    "나열_정의": {"keywords": ["독립적 정의", "용어", "참조용"], "blocks": ["card-numbered"]},
    "포함_계층": {"keywords": ["상위-하위", "포함 관계", "계층적"], "blocks": ["venn-diagram", "keyword-circle-row"]},
    "강조_결론": {"keywords": ["핵심 메시지 강조", "임팩트", "한 줄 강조"], "blocks": ["banner-gradient", "quote-big-mark"]},
    "비교": {"keywords": ["대등 비교", "좌우 대조", "vs"], "blocks": ["compare-2col-split", "compare-3col-badge"]},
    "순서": {"keywords": ["시간 순서", "단계별", "A->B->C"], "blocks": ["flow-arrow-horizontal", "process-horizontal"]},
}

def match_visual_type(expression_hint: str) -> str:
    """expression_hint에서 키워드를 찾아 시각적 유형 반환"""
    for vtype, spec in VISUAL_TYPE_KEYWORDS.items():
        if any(kw in expression_hint for kw in spec["keywords"]):
            return vtype
    return "default"

시각 매핑 근거 (Gestalt 원칙):

  • 폐합(Closure) → hierarchy/inclusion → 원형(벤 다이어그램)
  • 근접(Proximity) → comparison → 좌우 표/비교
  • 연속(Continuity) → sequence → 화살표 흐름
  • 유사(Similarity) → definition → 동일 형태 카드 반복
  • PPTAgent(EMNLP 2025): "참고 기반 생성"의 효과를 학술 입증

(3) 컨테이너 크기 적합성 검사

candidates = [b for b in candidates
              if b.min_height_px <= container.height_px]

(4) 블록 변형(variant) + 레이아웃 자동 선택

def select_block_variant(block, container, content):
    if not block.variants or len(block.variants) <= 1:
        return block.id, "default"

    for variant in block.variants:
        if variant.id == "compact" and container.height_px < 150:
            return block.id, "compact"
        if variant.id == "wide" and container_ratio[0] >= 70:  # body 70% 이상
            return block.id, "wide"

    return block.id, "default"

(5) fallback 정의

모든 필터를 통과하는 후보가 없을 때의 카테고리별 기본 블록:

카테고리 fallback 블록 이유
cards card-numbered 가장 범용, compact~xlarge 대응
emphasis dark-bullet-list 텍스트 중심, 높이 유연
visuals venn-diagram N개 자동 배치 가능
tables compare-2col-split 가장 기본적 비교
media image-side-text 텍스트+이미지 조합

디자인 레퍼런스 HTML 생성

Jinja 변수를 샘플 데이터로 치환한 완성된 HTML + 구조 의도 주석. LLM이 이 구조를 70~90% 복사 → 레이아웃을 "발명"하지 않고 검증된 구조를 따름.

def generate_design_reference(block, variant, catalog_entry):
    template = load_template(block.template)
    sample_data = build_sample_data(catalog_entry.slots)
    rendered = template.render(**sample_data)

    # 구조 의도 주석 추가 (LLM이 의도를 정확히 파악)
    annotated = f"<!-- {block.id}: {catalog_entry.visual} -->\n"
    if catalog_entry.get("visual_diff"):
        annotated += f"<!-- 차별점: {catalog_entry.visual_diff} -->\n"
    annotated += rendered

    return annotated
  • 출력:
{
  "block_id": "dark-bullet-list",
  "variant": "default",
  "visual_type": "인과",
  "schema": {
    "title": {"max_lines": 1, "font_size": 16, "max_chars": 30},
    "bullet_item": {"max_lines": 1, "font_size": 14, "max_chars": 86},
    "max_bullets": 5
  },
  "design_reference_html": "<!-- dark-bullet-list: 다크 배경 + 파란 제목 + 흰 불릿 -->\n<div ...>..."
}
  • 검증: 선택된 블록이 catalog.yaml에 실제 존재, min_height_px ≤ container.height_px
  • 저장: context.references["본심"].*

Stage 1.5b: 디자인 예산 재계산 (블록 선택 후)

  • 담당: 코드 (AI 아님)
  • 입력: Stage 1.7에서 선택된 블록의 schema + Stage 1.5a의 컨테이너 스펙
  • 목적: 텍스트 영역 확보 후 남은 공간 = 디자인 요소 예산. 텍스트를 줄이는 것이 아니라 도형·이미지·CSS 요소의 크기를 맞추는 방향.
def calculate_design_budget(container, text_budget, block_schema):
    # 블록 schema에서 텍스트 슬롯별 높이 합산
    text_height = 0
    for slot_name, spec in block_schema.items():
        if slot_name.startswith("max_"):
            continue
        slot_lines = spec.get("max_lines", 1)
        slot_font = spec.get("font_size", 14)
        text_height += slot_lines * (slot_font * 1.6)

    remaining_height = container.height_px - text_height - padding
    remaining_width = container.width_px - padding

    return DesignBudget(
        available_height_px=remaining_height,
        available_width_px=remaining_width,
        max_circle_diameter=min(remaining_height, remaining_width) - 4,
        max_img_width=remaining_width * 0.4,
        max_img_height=remaining_height,
        fits=remaining_height >= 0,
    )
  • 검증: available_height_px ≥ 0 (음수 = 블록이 컨테이너에 안 맞음 → Stage 1.7 재선택 또는 ADJUSTABLE)
  • 저장: context.containers["본심"].design_budget

Stage 2: HTML 생성

  • 담당: AI (Claude Sonnet 4, Anthropic API 직접, 현재 모델: claude-sonnet-4-20250514)
  • 입력: 원본 텍스트 + 누적 컨텍스트 전체
  • 처리: 영역별(배경/본심/첨부/결론) 각각 개별 호출로 HTML 생성

프롬프트 구성 — 모든 수치를 구체적으로 전달 (Phase S 교훈: 추상적 프롬프트는 실패):

출처 포함 내용
Stage 0 clean_text (원본 텍스트 — "이 텍스트를 그대로 사용하라")
Stage 1A core_message
Stage 1B expression_hint, relation_type
Stage 1.5a 확정된 폰트 크기, 줄 수, 글자 수, 컨테이너 px
Stage 1.5b 디자인 요소 크기 제약 (max_circle_px, max_img_width 등)
Stage 1.7 디자인 레퍼런스 HTML + visual_diff 설명

프롬프트 예시:

[디자인 레퍼런스]
아래 HTML의 구조와 색상 패턴을 따르되 콘텐츠를 교체하세요.
<!-- dark-bullet-list: 다크 배경 + 파란 제목 + 흰 불릿 -->
<!-- 차별점: 같은 다크 계열 callout-warning과 달리 경고 아이콘 없음. 순수 나열용. -->
<div style="background:#1a2332; padding:20px; border-radius:6px;">
  <!-- SLOT: title (1줄, 16px bold, max 30자) -->
  <h3 style="color:#4a9eff; font-size:16px; font-weight:700;">샘플 제목</h3>
  <!-- SLOT: bullets (1줄씩, 14px, max 86자, max 5개) -->
  <ul style="list-style:none; padding:0;">
    <li style="color:#e2e8f0; font-size:14px; padding:4px 0;">• 샘플 항목 1</li>
  </ul>
</div>

[수치 제약 — 반드시 준수]
- 컨테이너: 너비 707px, 높이 176px
- 폰트: 11px (배경 영역 위계)
- 줄당 최대 68자
- 최대 10줄
- 디자인 요소 예산: 높이 84px, 너비 707px

[원본 텍스트 — 축약/변형 금지]
"DX와 BIM이 개념적으로 명확히 정립되지 않은채 혼용되어 사용되고 있음..."

[필수 규칙]
- inline style만 사용, <style> 블록 금지
- overflow:hidden 금지
- 디자인 레퍼런스의 구조를 따르되 콘텐츠에 맞게 커스텀
- 개조식 통일 (서술형 ~하다/~이다 → 개조식 ~에 해당/~인식되는 중)
  • 이미지 처리: Stage 0에서 추출된 images[]의 경로와 크기 정보를 프롬프트에 포함. Stage 5에서 base64 인라인 변환.
  • 팝업 처리: Stage 0에서 추출된 popups[]<details>/<summary> HTML로 변환 지시.
  • 출력: {body_html, sidebar_html, footer_html}
  • 저장: context.generated_html

분산 검증 시스템

5층 검증을 한 곳에 집중하지 않고, 각 Layer가 적합한 시점에 분산 실행. 재시도 프롬프트는 Self-Refine(NeurIPS 2023) 패턴: localization + evidence + instruction. VASCAR(2024)의 Scorer+Suggester 분리: 점수 매기기와 피드백 생성을 분리.

Stage 2 직후: L1 + L2 + L3 (코드 검증)

Layer 도구 검증 내용
L1 kiwipiepy + regex 키워드 보존율 80% 이상
L2 regex Kei 메모("간결한 문제 제기용" 등)가 출력에 포함 안 됐는지
L3 regex overflow:hidden 없는지, 폰트 위계 위반 없는지, inline style만 사용했는지

실패 시 → Self-Refine 프롬프트로 Stage 2 재실행 (최대 2회):

[재생성 요청 - 시도 2/3]
이전 생성의 문제:
1. L1: 키워드 보존율 65%. 누락: {'BIM', '혼용', '설계오류'}
2. L3: overflow:hidden 감지

수정 지시 (Self-Refine):
- localization: 키워드 보존 실패, 구조 위반
- evidence: 원본 핵심 키워드 3개 누락, overflow:hidden 존재
- instruction: 누락 키워드 포함, overflow:hidden 제거, 나머지 제약 동일

Stage 3: 렌더링 (조립)

  • 담당: 코드 (AI 아님)
  • 입력: L1~L3 통과한 body/sidebar/footer HTML + 프리셋 grid + 동적 비율
  • 처리:
    • tokens.css + base.css 인라인 병합
    • CSS Grid 프레임 구성 — 동적 비율 적용 (예: 72fr 28fr or 65fr 35fr)
    • 각 영역 HTML을 <div class="area-body"> 등에 삽입
    • Pretendard 폰트 CDN 링크 포함
  • 출력: 완전한 단독 실행 HTML
  • 저장: context.rendered_html

Stage 3 직후: L4 (Selenium 실측)

def validate_after_stage3(context, rendered_html):
    measurements = selenium_measure(rendered_html)
    errors = []
    for area, m in measurements.items():
        if m.scroll_height > m.client_height:
            overflow = m.scroll_height - m.client_height
            errors.append({
                "layer": "L4", "severity": "RETRYABLE",
                "localization": f"{area} overflow {overflow}px",
                "evidence": f"scrollHeight {m.scroll_height} > clientHeight {m.client_height}",
                "instruction": f"이 영역의 디자인 요소를 {overflow+10}px 줄이거나 bullet 1개 제거"
            })
    return errors  # 실패 → 해당 영역만 Stage 2로 (최대 2회)

Stage 4: 품질 게이트 (L5)

  • 담당: Selenium (스크린샷 캡처) + Opus Vision (품질 판정, 현재 모델: claude-opus-4-0-20250514)
  • 처리:
    • 전체 페이지 스크린샷 캡처 → Opus Vision에 base64 전송
    • 5가지 평가 기준 (VASCAR 방식):
      1. 콘텐츠 겹침/잘림 없는가?
      2. 본심 영역이 시각적으로 가장 두드러지는가?
      3. 폰트가 읽을 수 있는 크기인가? 폰트 위계가 유지되는가?
      4. 한국어 비즈니스 프레젠테이션으로서 적절한가?
      5. 블록 유형에 다양성이 있는가?
    • 0~100점 평가:
      • 30점 미만 → 출력 차단 (FATAL)
      • 30~60점 → Opus 피드백으로 Stage 2 재실행
      • 60점 이상 → Stage 5로
  • L4와의 차이: L4는 영역 단위 px 실측(Stage 3 직후), L5는 조립 후 전체 페이지 시각적 평가
  • 저장: context.measurement, context.quality_score

Stage 5: 서빙

  • 담당: 코드
  • 처리:
    • 이미지 경로 → base64 인라인 변환 (다운로드 HTML에서도 이미지 표시)
    • <details> 인쇄 시 자동 펼침 JS 삽입 (window.onbeforeprint)
    • final.html 저장
  • 저장: data/runs/{run_id}/final.html

3. 검증 흐름 요약

Stage 1A (Kei 분석)
    ↓
  1A 검증 (Pydantic + 원본 대조) ──실패──→ Kei 재요청 (최대 2회)
    ↓ 통과
Stage 1B (컨셉 구체화)
    ↓
  1B 검증 (모순 탐지 + 원본 대조) ──실패──→ Kei 재요청 (최대 2회)
    ↓ 통과
Stage 1.5a → 1.7 → 1.5b (코드, 결정론적)
    ↓
Stage 2 (HTML 생성)
    ↓
  L1+L2+L3 ──실패──→ Self-Refine → Stage 2 재실행 (최대 2회)
    ↓ 통과
Stage 3 (조립)
    ↓
  L4 ──실패──→ 실패 영역만 Stage 2로 (최대 2회)
    ↓ 통과
Stage 4 (L5 최종 판정)
    ↓
  30점 미만 → 차단 (FATAL)
  30~60점  → Opus 피드백으로 Stage 2 재실행 (최대 1회)
  60점 이상 → Stage 5

재시도 총 예산

지점 최대 재시도 대상
Stage 1A 2회 Kei 전체
Stage 1B 2회 실패 topic만
L1~L3 2회 실패 영역만
L4 2회 실패 영역만
L5 1회 전체
최악 합계 Stage 2 최대 5회 영역별 독립이므로 1영역 기준

전체 파이프라인 타임아웃: 300초. 초과 시 최선 결과 반환 + 경고.


4. 카탈로그 시스템 (catalog.yaml)

4.1 블록 구조

38개 블록, 6개 카테고리:

카테고리 블록 수 용도
headers 5 꼭지/섹션 제목
cards 9 항목 나열/비교
tables 3 비교 표, 스트라이프 표
visuals 6 벤 다이어그램, 프로세스, 흐름
emphasis 10 강조/콜아웃/배너/불릿
media 5 이미지 배치

별도 섹션: 4개 레이아웃 프리셋 (sidebar-right, two-column, hero-detail, single-column)

4.2 블록 메타데이터 (현재 상태 + Phase T 추가)

- id: block-id
  name: 한글 이름
  category: headers | cards | tables | visuals | emphasis | media
  template: blocks/category/block-id.html
  height_cost: compact | medium | large | xlarge
  min_height_px: 80
  relation_types: [comparison, cause_effect]   # 빈 배열 = 모든 relation에 가능
  min_items: 2                                  # 19/38 블록에 존재
  max_items: 5                                  # 19/38 블록에 존재
  visual: "시각적 설명"
  when: "사용 적합 상황"
  not_for: "부적합 상황"
  purpose_fit: [핵심전달, 문제제기]
  zone: full-width-only                         # 4/38 블록에 존재 (선택)
  slots:
    required: [title, description]
    optional: [icon, source]
  # --- Phase T 필수 추가 ---
  schema:                                        # ★ 현재 19/38 → 38/38 완성 필요
    title: {max_lines: 1, font_size: 16, ref_chars: {body: 30, sidebar: 20}}
    description: {max_lines: 3, font_size: 14, ref_chars: {body: 150, sidebar: 90}}
  visual_diff: |                                 # ★ 신규 (T-4), 유사 블록 20개에 추가
    유사 블록과의 차이: ...
  variants:                                      # 현재 4/38 → 필요 시 확장
    - id: default
    - id: compact

4.3 expression_hint → 블록 매핑 (키워드 포함 매칭)

시각적 유형 매칭 키워드 매핑 블록
인과 "인과", "현상->결과", "야기" callout-warning, dark-bullet-list
나열_병렬 "독립적 나열", "병렬", "개별 증거" dark-bullet-list, card-icon-desc
나열_정의 "독립적 정의", "참조용", "용어" card-numbered
포함_계층 "상위-하위", "포함 관계", "계층적" venn-diagram, keyword-circle-row
강조_결론 "핵심 메시지 강조", "임팩트" banner-gradient, quote-big-mark
비교 "대등 비교", "좌우 대조", "vs" compare-2col-split, compare-3col-badge
순서 "시간 순서", "단계별", "A->B->C" flow-arrow-horizontal, process-horizontal

4.4 Phase T에서 필요한 개선

항목 현재 목표 우선순위
schema 완성 19/38 38/38 높음 (Stage 1.5b 필수)
visual_diff 추가 0/38 20/38 (유사 블록 그룹) 중간 (T-3 프롬프트 품질)
ref_chars ↔ 컨테이너 폭 정합성 검증 없음 시작 시 자동 검증 높음
블록 독립 렌더링 스크린샷 없음 38개 PNG 중간 (visual_diff 근거)

5. 데이터 흐름 요약

[원본 MDX]
    │
    ▼
Stage 0 ─── 코드 ──→ context.normalized
    │       (python-frontmatter + markdown-it-py)
    │       {clean_text, title, images[], popups[], tables[], sections[]}
    ▼
Stage 1A ── Kei ──→ context.analysis
    │               {topics[], page_structure, core_message}
    │               ✓ Pydantic 검증 + 원본 대조
    ▼
Stage 1B ── Kei ──→ context.topics[].relation_type, .expression_hint, .source_data
    │               ✓ 모순 탐지 + 원본 패턴 대조
    ▼
Stage 1.5a ─ 코드 ──→ context.font_hierarchy, .container_ratio, .containers[].text_budget
    │                   (폰트 위계 확정 → 비율 역산 → 텍스트 예산)
    ▼
Stage 1.7 ── 코드 ──→ context.references[]
    │                   (relation_type 1차 + expression_hint 2차 → 블록+변형)
    │                   (Jinja 치환 → 디자인 레퍼런스 HTML)
    ▼
Stage 1.5b ─ 코드 ──→ context.containers[].design_budget
    │                   (블록 schema 기반 디자인 요소 크기 역산)
    ▼
Stage 2 ── Sonnet ──→ context.generated_html
    │                   (디자인 레퍼런스 + 구체적 수치 + 원본 텍스트)
    │                   ✓ L1+L2+L3 코드 검증
    ▼
Stage 3 ── 코드 ──→ context.rendered_html
    │                 (동적 비율 grid 조립)
    │                 ✓ L4 Selenium 실측
    ▼
Stage 4 ── Opus ──→ context.quality_score
    │                (스크린샷 기반 시각 평가, 30점 미만 차단)
    ▼
Stage 5 ── 코드 ──→ final.html
                     (이미지 base64 변환, details JS 삽입)

6. 에러 핸들링

실패 지점 등급 복구 전략
Stage 0 검증 실패 FATAL 원본 MDX 자체 문제 — 사용자에게 에러 반환
Stage 1A Pydantic 실패 RETRYABLE Self-Refine 피드백 포함 Kei 재요청 (최대 2회)
Stage 1B 모순 탐지 RETRYABLE 모순 topic만 피드백 포함 재요청 (최대 2회)
Stage 1.5a 수치 이상 FATAL 결정론적이므로 재시도 무의미 — 입력 점검 필요
Stage 1.7 적합 블록 없음 ADJUSTABLE 카테고리별 fallback 블록 선택 + 경고 기록
Stage 1.5b 음수 예산 ADJUSTABLE 폰트 1px 축소 or 블록 재선택
L1~L3 실패 RETRYABLE Self-Refine 프롬프트로 Stage 2 재실행 (최대 2회)
L4 overflow RETRYABLE 실패 영역만 Stage 2로 + 구체적 px 피드백 (최대 2회)
L5 30점 미만 FATAL 출력 차단 + 에러 기록
L5 30~60점 RETRYABLE Opus 피드백으로 Stage 2 재실행 (최대 1회)
타임아웃 (300초) FATAL 최선 결과 반환 + 경고

7. 기술 스택 (Phase T)

구성 요소 기술 비고
웹 서버 FastAPI + uvicorn 포트 8001, SSE 스트리밍
파이프라인 런타임 Python (async) Pydantic BaseModel (PipelineContext)
MDX 파싱 python-frontmatter + markdown-it-py + mdit-py-plugins ~1MB 추가
콘텐츠 판단 Kei Persona API (localhost:8000, Opus, SSE) httpx streaming
HTML 생성 Claude Sonnet 4 (Anthropic API) 영역별 개별 호출
한국어 키워드 추출 kiwipiepy L1 검증 + Stage 1A/1B 원본 대조
관계 표현 패턴 regex 7종 (relation_type별) Stage 1B 검증 보조
시각 품질 평가 Opus Vision (Anthropic API) L5, 스크린샷 기반
실측 렌더링 Selenium headless Chrome L4, 1280×920 viewport
블록 카탈로그 catalog.yaml (38개 블록) schema 38/38 완성 필요
템플릿 엔진 Jinja2 블록 HTML 렌더링
디자인 토큰 tokens.css + base.css Pretendard Variable
HTTP 클라이언트 httpx Kei API SSE 통신
스냅샷 저장 JSON (Pydantic model_dump_json) data/runs/{run_id}/

8. 마이그레이션 맵 (현재 코드 → Phase T)

신규 생성

파일 역할
src/pipeline_context.py PipelineContext Pydantic 모델
src/mdx_normalizer.py Stage 0 MDX 파서 (4-Layer)
src/validators.py Stage 1A/1B Pydantic 스키마 + 모순 탐지 + 원본 대조
src/block_reference.py Stage 1.7 블록 선택 + 디자인 레퍼런스 생성
scripts/capture_block_screenshots.py 38개 블록 독립 렌더링 스크린샷

수정

파일 변경 내용
src/pipeline.py run_stage 패턴 + 11-Stage 러너 + PipelineContext 기반
src/html_generator.py 프롬프트에 context 기반 수치+레퍼런스 주입, 하드코딩 CSS 제거
src/space_allocator.py 폰트 위계 + 동적 비율 역산 + design_budget 계산
src/content_verifier.py L1에 kiwipiepy 추가, L3에 폰트 위계 검증 추가
templates/catalog.yaml schema 19개 추가 완성 + visual_diff 20개 추가

미사용 (Phase S에서 이미 미사용, 삭제 후보)

파일/함수 이유
src/block_selector.py Phase R'에서 제거됨. Stage 1.7의 block_reference.py로 대체
src/content_editor.py Phase S에서 별도 텍스트 편집 Stage 제거됨
src/design_director.py Step B 프롬프트 제거됨. 프리셋 선택 로직만 space_allocator로 이동

9. Phase T 범위 vs Phase ZZ 예고

Phase T (현재) — 폰트 위계 + 파이프라인 안정화

  • 11-Stage 파이프라인 전체 구현 (PipelineContext + run_stage 패턴)
  • Stage 0: MDX 4-Layer 파서
  • Stage 1A/1B: Pydantic 검증 + 모순 탐지 + 원본 대조
  • Stage 1.5a: 폰트 위계 확정 + 동적 비율 역산 (Phase T 핵심)
  • Stage 1.7: 블록 참고 선택 (키워드 매칭 + fallback)
  • Stage 1.5b: 블록 schema 기반 디자인 예산 역산
  • catalog.yaml schema 38/38 완성 + visual_diff 20개
  • 분산 검증 (L1~L5) + Self-Refine 재시도
  • 합격 기준: 어떤 MDX에서도 폰트 위계 유지, overflow 없음, 품질 60점+

Phase ZZ (최종 전환) — 판단 체계 전환 + 워크플로우

  • Kei Persona API → Opus 직접 + Gitea 위키 판단 기준 전환 (비교 평가 후 결정)
  • 청크별 보존율 차등화 (verbatim / summary / core_80)
  • Stage 1.5a → 1A/1B 역방향 협상 루프 (weight 재조정 요청)
  • Gitea 이슈 기반 워크플로우 전환
  • Starlight .astro 임베딩
  • 반응형 전환 여부 판단