Files
C.E.L_Slide_test2/IMPROVEMENT-PHASE-T.md
kyeongmin 1f7579cf64 Phase W + V' 완료: before→filled→after 파이프라인 + 조립 로직 수정
Phase W:
- weight 비율 초기 배정 (space_allocator header 높이 반영)
- block_assembler 공통 조립 함수 (filled/assembled 통합)
- filled → Selenium 측정 → context 저장
- sidebar overflow 확장 + body 재배분
- sub_layouts 사전 계산 (이미지 누락 해결)

Phase V':
- 팝업 링크 우측상단 배치 (인라인 → position:absolute)
- 표 내용 Kei 판단 (공란 크기 계산 → 행/열 산출 → Kei 요약)
- 출처 라벨 삭제 + 이미지 아래 캡션 배치
- after 공란 제거 (결론 바로 위까지 body/sidebar 채움)

추가:
- V-10 bold 키워드: 기계적 추출 → Kei 문맥 판단
- ** 마크다운 → <strong> 변환
- [이미지:] 마커 제거 (bold 변환 전 처리)
- grid-template-rows AFTER 크기 반영 (Sonnet final)
- assemble_stage2 CSS font-size override, white-space fix
- 하드코딩 전수 검토 완료
- 본심 여러 topic 텍스트 합침

Phase X 계획 문서 작성 (동적 역할 구조)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 05:00:52 +09:00

50 KiB
Raw Blame History

Phase T: 파이프라인 기반 정비 + 폰트 위계 + 동적 컨테이너 + 디자인 선택

작성일: 2026-03-31 (초안) → 2026-04-01 (T-0~T-4 확정) 상태: 설계 확정, 실행 대기 근거: Phase S 결과물에서 (1) 누적 컨텍스트/검증 기반 구조 부재, (2) MDX 표준화 부재, (3) 단계별 검증 부재, (4) 블록 디자인 미활용, (5) 블록 간 설명 미구분, (6) 폰트 위계 역전, (7) 컨테이너 비율 고정. 이 7가지를 해결. 선행: Phase S (Claude HTML 직접 생성 + 독립 검증 시스템)


Phase T 구조 (T-0 ~ T-4)

T-0: 파이프라인 기반 구조 (PipelineContext + run_stage 패턴)
  │   모든 T-1~T-4의 기반. 이것 없이는 검증도 프롬프트 주입도 안 됨.
  │
  ├── T-1: MDX 표준화 (Stage 0 추가)
  │     깨끗한 입력이 context에 들어가야 이후 모든 Stage가 정확해짐
  │
  ├── T-2: 각 Stage에 검증 추가
  │     T-0의 validate() 패턴을 각 Stage 내부에 구현
  │
  ├── T-3: 블록 참고 디자인 + 프롬프트 정비
  │     참고 블록 선택 + 프롬프트에 context 주입 + 하드코딩 CSS 제거
  │
  └── T-4: 블록 설명 catalog.yaml 보완
        유사 블록 간 visual_diff 추가 (코드 변경 없음, yaml만)

실행 순서

  • T-0 먼저 (기반 구조)
  • T-1, T-4는 독립 실행 가능
  • T-2는 T-0 이후
  • T-3는 T-0, T-4 이후 (블록 설명이 보완된 후 프롬프트에 넣어야 정확)

의존 관계

T-0 ──→ T-1 (context에 clean_text 저장)
T-0 ──→ T-2 (validate 패턴 구현)
T-0 ──→ T-3 (context에서 프롬프트 조립)
T-4 ──→ T-3 (visual_diff가 프롬프트에 포함)

T-0. 파이프라인 기반 구조 (신규, 최우선)

목적

모든 Stage가 하나의 누적 컨텍스트 객체를 공유하고, 각 Stage가 transform → validate → update 패턴을 따르도록 기반 구조를 만든다.

1. PipelineContext 객체

각 Stage가 개별 JSON을 읽고 쓰는 대신, 하나의 context 객체가 파이프라인을 따라가며 점진적으로 풍부해진다.

@dataclass
class PipelineContext:
    # Stage 0 (T-1)
    raw_content: str = ""
    clean_text: str = ""
    title: str = ""
    images: list[dict] = field(default_factory=list)
    popups: list[dict] = field(default_factory=list)
    tables: list[dict] = field(default_factory=list)
    
    # Stage 1A
    topics: list[dict] = field(default_factory=list)
    page_structure: dict = field(default_factory=dict)
    core_message: str = ""
    
    # Stage 1B
    # → topics[].relation_type, expression_hint, source_data 병합
    
    # Stage 1.5
    preset: dict = field(default_factory=dict)
    containers: dict = field(default_factory=dict)  # 역할별 ContainerSpec
    
    # Stage 1.7 (T-3)
    references: dict = field(default_factory=dict)  # 역할별 참고 블록 HTML
    
    # Stage 2
    generated_html: dict = field(default_factory=dict)  # body/sidebar/footer
    
    # 메타
    run_id: str = ""
    run_dir: Path = None
    errors: list[str] = field(default_factory=list)
    
    def save_snapshot(self, stage_name: str):
        """디버깅용 스냅샷 저장. 기존 step*.json 역할을 대체."""
        if self.run_dir:
            path = self.run_dir / f"{stage_name}_context.json"
            path.write_text(
                json.dumps(asdict(self), ensure_ascii=False, indent=2, default=str),
                encoding="utf-8",
            )

2. run_stage() 패턴

async def run_stage(stage_fn, context, stage_name, max_retries=1):
    """모든 Stage의 공통 실행 패턴."""
    for attempt in range(max_retries + 1):
        result = await stage_fn(context)          # 변환
        errors = result.get("_errors", [])        # 검증 (각 Stage가 자체 수행)
        if not errors:
            context.update(result)                # 누적 컨텍스트에 병합
            context.save_snapshot(stage_name)      # 스냅샷 저장
            return context
        if attempt < max_retries:
            context.errors.append(f"[{stage_name}] 재시도 {attempt+1}: {errors}")
    raise StageFailure(stage_name, errors)

3. 파이프라인 러너

async def generate_slide(content: str):
    context = PipelineContext(raw_content=content, run_id=str(int(time.time()*1000)))
    
    stages = [
        ("stage_0_normalize", normalize_mdx_stage, 0),      # T-1: 코드, 재시도 없음
        ("stage_1a_classify", classify_content_stage, 2),    # Kei API, 최대 2회
        ("stage_1b_refine", refine_concepts_stage, 2),       # Kei API, 최대 2회
        ("stage_1_5_containers", calculate_containers_stage, 0),  # 코드
        ("stage_1_7_references", select_references_stage, 0),    # T-3: 코드
        ("stage_2_generate", generate_html_stage, 2),        # Sonnet, 최대 2회
        ("stage_3_render", render_stage, 0),                 # 코드
        ("stage_4_quality", quality_gate_stage, 0),          # Selenium + Opus
    ]
    
    for stage_name, stage_fn, max_retries in stages:
        context = await run_stage(stage_fn, context, stage_name, max_retries)
        yield {"event": "progress", "data": stage_name}
    
    yield {"event": "result", "data": context.generated_html}

4. context → 프롬프트 주입

각 AI Stage가 프롬프트를 구성할 때 context에서 필요한 정보를 꺼낸다:

# Stage 1A — context.clean_text만 필요
prompt = KEI_PROMPT + context.clean_text

# Stage 1B — context.clean_text + context.topics
prompt = KEI_PROMPT_B.format(
    content=context.clean_text,
    topics=format_topics(context.topics),
)

# Stage 2 — 이전 모든 단계의 누적 결과 활용
prompt = CORE_PROMPT.format(
    height=context.containers["본심"].height_px,       # 1.5에서
    content_block=context.clean_text,                   # Stage 0에서
    core_message=context.core_message,                  # 1A에서
    expression_hint=topic.expression_hint,              # 1B에서
    reference_design=context.references["본심"].html,    # 1.7에서 (T-3)
    reference_diff=context.references["본심"].visual_diff, # T-4에서
)

수정 파일

  • src/pipeline_context.py (신규) — PipelineContext 클래스
  • src/pipeline.py — run_stage 패턴 + 러너 재구성
  • 각 Stage 함수가 context를 받고 리턴하도록 시그니처 변경

T-1. Stage 0 — MDX 표준화 (신규)

목적

파이프라인 진입 전에 원본 MDX를 깨끗한 구조화 텍스트로 변환. 이후 모든 단계(1A, 1B, 2)에서 동일한 깨끗한 입력 사용.

현재 상태

  • html_generator.pynormalize_mdx() 함수가 이미 존재 (Line 401)
  • Stage 2에서만 호출 — Stage 1A/1B는 원본 MDX(JSX, frontmatter 포함)를 그대로 받음
  • Kei에게 style={{cursor: 'pointer'}} 같은 JSX 코드가 전달됨

변경 사항

위치 이동: normalize_mdx()pipeline.py 최상단에서 1회 호출, 모든 후속 단계에 전달

추가 처리:

  • title 추출: frontmatter에서 title → 별도 보관
  • 이미지 추출: ![alt](path)images[] 리스트 분리, 본문에 [이미지: alt] 마커
  • 팝업 마킹: <details><summary>제목</summary>내용</details>[팝업: 제목]\n내용\n[/팝업]
  • 핵심요약 마킹: :::note[제목][핵심요약]\n내용\n[/핵심요약]
  • 표 보존: markdown table 구조 유지

출력:

{
    "clean_text": str,       # 정규화된 순수 텍스트
    "title": str,            # frontmatter 제목
    "images": [{"alt": str, "path": str}],
    "popups": [{"title": str, "content": str}],
    "tables": [{"header": str, "rows": list}]
}

수정 파일: src/pipeline.py, src/mdx_normalizer.py (신규), src/html_generator.py


T-2. 각 Stage에 검증 추가 (신규)

목적

각 Stage 내부에서 형식 검증 + 내용 검증을 수행하고, 실패 시 구체적 피드백으로 회귀한다. 단순 숫자 체크(하드코딩)가 아니라, "이 결과가 원본 콘텐츠에 대해 적절한가?"를 판단.

현재 상태

  • Stage 2 이후에만 검증 (5층 + 품질 게이트)
  • Stage 1A, 1B, 1.5에는 검증 없음

검증 4계층 구조 (모든 Stage 공통)

각 Stage의 validate()는 4가지를 순서대로 수행:

1. 형식 검증 (코드) — 값 범위, 유효 enum, null 체크 → 즉시 판단
2. 내용 검증 (코드+대조) — 결과가 원본 콘텐츠에 대해 적절한가 → 원본과 대조
3. 실패 피드백 생성 — 무엇이 왜 잘못됐는지 구체적 문장으로 → Kei/Sonnet에게 전달
4. 회귀 실행 — 피드백을 포함하여 해당 Stage만 재실행

Stage별 검증 상세


Stage 0 검증 (MDX 표준화)

형식 검증 (코드):

  • clean_text 비어있지 않음
  • ## 섹션 최소 1개
  • 원본 대비 30% 이상 텍스트 보존 (과도한 제거 방지)

내용 검증 (코드, 원본 대조):

  • 원본의 ## 섹션 수와 정규화 후 섹션 수 비교 — 섹션이 누락되었는지
  • 원본의 이미지 참조 수와 추출된 images[] 수 비교 — 이미지가 누락되었는지
  • 원본의 <details> 수와 추출된 popups[] 수 비교

실패 피드백: "원본에 ## 섹션이 5개인데 정규화 후 3개. 누락: '용어 정의', '상호관계'" 회귀: 코드 Stage → 자동 조정 불가 시 에러 반환 (원본 MDX 자체에 문제) 비용: 0


Stage 1A 검증 (꼭지 추출)

형식 검증 (Pydantic):

  • topics 리스트 비어있지 않음
  • weight 합 0.9~1.1 범위
  • purpose/role/layer 유효 enum
  • 본심 존재, 본심 weight ≥ 0.3
  • page_structure의 topic_ids가 실제 topics에 존재

내용 검증 (코드, 원본 대조):

  • 원본의 ## 섹션 수 vs topic 수 — 차이가 크면 분류가 잘못됐을 가능성
    • 예: 원본 ## 5개인데 topic 2개 → "3개 섹션이 topic에 매핑 안 됨"
  • 각 topic의 summary 키워드가 원본 해당 섹션에 실제 존재하는지
    • 예: topic 3 summary="DX와 BIM의 비교"인데 원본에 "비교"라는 단어가 없으면 → Kei가 해석을 덧붙인 것
  • 본심 topic의 source_hint가 가리키는 원본 위치에 실제 핵심 콘텐츠가 있는지

실패 피드백 예시:

"topic 3의 summary에 '비교'가 있지만 원본 해당 섹션에는 '비교'가 없고 
'상호관계'와 '포함'이라는 표현이 있습니다. 
topic 3의 purpose가 '핵심전달'인데 실제 내용은 '포함 관계 설명'에 가깝습니다.
summary를 원본에 맞게 재작성하고, 비중을 재판단해주세요."

회귀: 피드백 + 원본의 해당 섹션 텍스트를 함께 Kei에게 재요청 (최대 2회) 비용: 0 (코드 대조)


Stage 1B 검증 (컨셉 구체화)

형식 검증 (Pydantic):

  • relation_type 유효 enum (sequence|inclusion|comparison|hierarchy|definition|cause_effect|none)
  • expression_hint 비어있지 않음
  • source_data 비어있지 않음

내용 검증 (코드, 원본 대조 + 결정 테이블):

모순 검사 (결정 테이블):

purpose 모순인 relation_type 이유
결론강조 comparison, sequence 결론은 비교나 순서가 아님
문제제기 sequence, definition 문제제기는 순서 나열이나 정의가 아님
용어정의 hierarchy, cause_effect 정의 나열은 상하위나 인과가 아님
구조시각화 none 시각화할 관계가 없으면 구조시각화가 아님

source_data 원본 대조:

  • source_data에서 명사/핵심어 추출
  • 해당 키워드가 원본 clean_text에 실제 존재하는지 확인
  • 예: source_data에 "Gartner 2023 보고서"가 있는데 원본에 "Gartner"가 없으면 → Kei가 없는 출처를 만들어낸 것

relation_type과 원본 구조 대조:

  • relation_type=comparison인데 원본에 대조/비교 구조(vs, 반면, 차이점)가 없으면 → 의심
  • relation_type=sequence인데 원본에 순서 표현(→, 이후, 다음)이 없으면 → 의심

실패 피드백 예시:

"topic 1의 relation_type이 'comparison'인데, 원본에 비교 구조(vs, 반면, 차이점)가 없습니다.
원본 텍스트: 'DX와 BIM이 개념적으로 명확히 정립되지 않은채 혼용되어 사용되고 있음'
이 문장은 '혼용 현상 기술'이지 'A vs B 비교'가 아닙니다.
cause_effect(혼용 → 오인) 또는 definition(용어 정립 필요)이 더 적절할 수 있습니다.
재판단해주세요."

회귀: 모순/불일치 topic만 피드백과 함께 Kei에게 재요청 (최대 2회) 비용: 0 (코드 대조 + 결정 테이블)


Stage 1.5 검증 (컨테이너 스펙)

형식 검증 (코드):

  • 모든 height_px > 0
  • body zone 총 할당 ≤ body zone 예산 (490px)
  • sidebar, footer도 예산 이내

내용 검증 (코드, 실현 가능성):

  • topic당 height ≥ 50px — 50px 미만이면 어떤 블록/디자인도 콘텐츠를 담을 수 없음
  • topic의 source_data 텍스트 길이 vs 할당된 높이 비교:
    • 텍스트 500자인데 높이 58px → 12px 폰트로 약 4줄(120자)만 수용 → "텍스트의 24%만 표시 가능" 경고
  • 역할별 폰트 위계 충돌:
    • 배경 컨테이너가 본심보다 크면 → 폰트 역전 위험

실패 피드백 예시:

"배경 역할에 topic 2개가 배정되어 topic당 58px.
topic 1의 source_data는 150자인데, 58px에서 12px 폰트로 약 60자만 수용 가능 (40%).
최소 80px/topic이 필요합니다.
배경 높이를 160px(80px×2)로 올리고, 본심에서 43px를 차감합니다.
차감 후 본심 309px — compare-2col-split(308px)이 들어갈 수 있는지 확인 필요."

회귀: 코드 Stage → 자동 조정:

  1. 최소 높이(80px/topic) 보장
  2. 부족분을 가장 큰 컨테이너에서 차감
  3. 차감 후 해당 컨테이너에 할당된 블록이 여전히 들어가는지 검증
  4. 안 들어가면 → weight 재조정이 필요하다는 경고를 context에 기록 비용: 0

Stage 2+ 검증: 기존 유지

  • Stage 2: 5층 검증 (텍스트 보존, 금지 콘텐츠, 구조, overflow, 시각 품질)
  • Stage 4: 품질 게이트 (30점 미만 차단)
  • 기존 content_verifier.py + slide_measurer.py 그대로

에러 분류 체계

모든 검증 에러는 3등급으로 분류:

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

수정 파일

  • src/validators.py (신규) — Pydantic 스키마 + 모순 결정 테이블 + 원본 대조 함수
  • src/pipeline.py — 각 Stage에 validate() 호출 삽입 (T-0의 run_stage 패턴)

T-3. 디자인 블록 참고 프로세스 (신규)

목적

AI가 HTML 생성 시 콘텐츠의 relation_type에 맞는 블록 디자인을 참고 자료로 제공. 다양하고 의미에 맞는 결과물 생성.

현재 상태

  • 역할별(배경/본심/첨부/결론) 고정 프롬프트 사용
  • relation_type 무관하게 같은 CSS 구조 적용
  • 38개 블록 디자인 미활용

relation_type → 참고 블록 매핑 (코드, 결정론적)

relation_type 참고 블록 (우선순위순) 근거
hierarchy venn-diagram, keyword-circle-row 포함/상하위 → 원형
inclusion venn-diagram, circle-gradient 포함/융합 → 겹침 원형
comparison compare-2col-split, compare-3col-badge 대등 비교 → 좌우 표
cause_effect callout-warning, dark-bullet-list, topic-left-right 인과 → 경고/강조
sequence flow-arrow-horizontal, process-horizontal 순서 → 화살표
definition card-numbered, dark-bullet-list 정의 나열 → 카드/불릿
none banner-gradient, topic-left-right, quote-big-mark 텍스트/강조

적용 방식

현재:

prompt = CORE_PROMPT.format(height=h, content_block=text, ...)
# relation_type 무관하게 항상 같은 CSS

변경:

# 1. relation_type에서 참고 블록 결정 (코드)
ref_blocks = RELATION_BLOCK_MAP[relation_type]

# 2. 블록 HTML 로드
ref_html = load_block_html(ref_blocks[0])

# 3. 프롬프트에 참고 디자인 첨부
prompt = CORE_PROMPT.format(
    height=h, content_block=text,
    reference_design=ref_html,       # ← 신규
    reference_block_name=ref_blocks[0],
    ...
)

프롬프트 추가 섹션:

## 참고 디자인 ({reference_block_name})
이 콘텐츠의 관계 성격({relation_type})에 적합한 디자인 참고 자료이다.
색상, 레이아웃 패턴, 시각적 구조를 참고하되 콘텐츠에 맞게 커스터마이징하라.
그대로 복사하지 마라. 참고만 하라.

{reference_design}

영역별 적용

영역 프롬프트 참고 디자인
배경 BG_PROMPT relation_type에 따라 동적
본심 CORE_PROMPT relation_type에 따라 동적 (가장 중요)
첨부 SIDEBAR_PROMPT 항상 card-numbered (용어정의)
결론 FOOTER_PROMPT 항상 banner-gradient

기존 방식 비교

방식 블록 역할 한계
Phase P~R 블록 선택 → 슬롯 채우기 경직. 슬롯에 안 맞으면 왜곡
Phase S 블록 무시, 고정 프롬프트 단조. 모든 콘텐츠 같은 모양
Phase T 블록을 참고 자료로 제공 유연. 다양한 디자인 + 콘텐츠 적응

수정 파일: src/html_generator.py, src/block_reference.py (신규), templates/catalog.yaml


T-4. 블록 설명(catalog.yaml) 보완 (신규)

목적

catalog.yaml의 블록 설명이 너무 범용적이어서 유사 블록 간 구분이 안 됨. 각 블록의 고유한 시각적 특징유사 블록과의 차이점을 명확히 기술하여, T-3에서 AI가 참고 디자인을 받았을 때 "왜 이 디자인을 참고하는지"를 이해할 수 있도록 한다.

현재 문제

유사 블록들의 설명이 구분되지 않음:

card-compare-3col:  "3열 카드. 각 카드 상단 색상 헤더 + 불릿"
card-image-3col:    "3열 카드. 각 카드 상단에 이미지 + 밑줄 제목 + 불릿"
card-icon-desc:     "2~4열. 중앙 큰 이모지 아이콘 + 제목 + 설명"

→ "3열 카드 + 불릿"이 2개. 실제 렌더링은 완전히 다르지만 텍스트만으로는 구분 불가.

추가 필드: visual_diff

각 블록에 유사 블록 그룹 내 차이점을 명시하는 visual_diff 필드를 추가.

- id: card-compare-3col
  visual: "3열 카드. 각 카드 상단 색상 헤더(제목+서브) + 이미지 + 불릿 목록."
  visual_diff: |
    유사 블록과의 차이:
    - compare-2col-split: 2개 항목을 행별 비교 (좌/기준/우 구조)
    - compare-3col-badge: VS 배지가 있는 표 형식 비교
    - 이 블록(card-compare-3col): 3개 항목이 각각 독립 카드로 분리.
      카드마다 고유 색상 헤더(빨강/파랑/회색).
      행별 대조가 아닌 카드별 독립 설명.
    
    적합: 3개 카테고리가 대등하게 병렬 나열, 각 항목에 이미지 포함
    부적합: "A의 X vs B의 X" 항목별 대조 → compare-2col-split

보완 대상 그룹 (유사 블록이 2개 이상인 그룹)

그룹 유사 블록 구분 필요한 이유
비교 계열 (5개) compare-2col-split, compare-3col-badge, comparison-2col, compare-pill-pair, card-compare-3col 전부 "비교"인데 구조가 다 다름
카드 계열 (6개) card-icon-desc, card-image-3col, card-dark-overlay, card-numbered, card-stat-number, card-tag-image 전부 "카드"인데 용도가 다름
텍스트 강조 (4개) quote-big-mark, quote-question, callout-warning, callout-solution 전부 "강조"인데 톤이 다름
흐름/프로세스 (2개) flow-arrow-horizontal, process-horizontal 둘 다 "흐름"인데 정보량이 다름
헤더 (3개) topic-left-right, topic-center, topic-numbered 전부 "제목"인데 레이아웃이 다름

20개 블록visual_diff 추가. 나머지 18개는 그룹 내 유일하므로 기존 visual만으로 충분.

작업 방식

  • 각 블록의 실제 HTML을 렌더링해서 시각적 차이를 확인한 후 기술 (추정으로 쓰지 않음)
  • visual_diff다른 블록과의 상대적 차이를 기술 (절대적 설명이 아님)
  • T-3의 build_design_brief()가 이 필드를 읽어서 프롬프트에 포함

T-3과의 연계

def build_design_brief(block: dict, catalog_entry: dict) -> str:
    brief = f"디자인 레퍼런스: {catalog_entry['name']}\n"
    brief += f"시각 패턴: {catalog_entry['visual']}\n"
    if catalog_entry.get('visual_diff'):
        brief += f"차별점: {catalog_entry['visual_diff']}\n"  # ← T-4 추가
    brief += f"적합 상황: {catalog_entry['when']}\n"
    return brief

수정 파일

  • templates/catalog.yaml — 20개 블록에 visual_diff 필드 추가

T-3과 독립 실행 가능

  • catalog.yaml만 수정하는 작업이라 코드 변경 없음
  • T-3 이전에 해도 되고 이후에 해도 됨 (하지만 T-3 이전에 하면 AI가 처음부터 정확한 설명을 볼 수 있으므로 이전 권장)

1. 문제

현재 상태

영역 중요도 있어야 할 크기 현재 실제 크기
핵심 (key-msg) 1위 가장 큼 11px (가장 작음)
본문 (core) 2위 12px
배경 (bg) 3위 보통 9-11px
첨부 (sidebar) 4위 가장 작음 14px (가장 큼)

폰트 크기가 중요도와 완전히 역전되어 있다.

원인 3가지

  1. 폰트 위계 부재: 각 프롬프트를 독립적으로 설계하면서 "이 영역 안에서 맞추기"만 했고, 슬라이드 전체에서의 시각적 위계를 고려하지 않음. 컨테이너 크기를 먼저 정하고 폰트를 끼워 맞추니 중요도가 뒤집힘.
  2. 블록 디자인 선택 부재: Phase S는 blocks/ 디렉토리의 블록 템플릿을 전혀 참조하지 않음. 4개 프롬프트에 CSS가 직접 하드코딩되어 있어, 어떤 MDX가 와도 항상 동일한 디자인이 나옴. 콘텐츠 특성에 따른 디자인 다양성이 없음.
  3. 컨테이너 비율 고정: body:sidebar = 65:35 고정. 배경+본심이 좁고 첨부가 넓어서 폰트 역전의 원인.

근본 원칙

  1. 폰트(위계)가 먼저고, 컨테이너(공간)가 따라가야 한다.
  2. 배경과 본심의 컨테이너를 키우고, 첨부의 컨테이너를 줄여야 한다. (폰트 위계에 맞추기 위해)
  3. 텍스트 양을 사전에 보고, 비율과 다단 여부를 판단한 후 프롬프트를 조립해야 한다.

2. 폰트 위계 (확정)

영역 중요도 폰트 크기 비고
핵심 (key-msg) 1위 14px bold 무조건 한 줄, 하나의 메시지
본문 (core) 2위 12px 본문 텍스트 기본
배경 (bg) 3위 10-12px 텍스트 양에 따라 조정
첨부 (sidebar) 4위 9-11px 참고 자료, 가장 작아도 됨

3. 계산 순서

[Phase 1] 텍스트 분석
    각 영역별 글자 수 측정
    ↓
[Phase 2] 폰트 위계로 필요 공간 계산
    "이 폰트 크기로 이 텍스트를 넣으려면 몇 px 필요한가?"
    ↓
[Phase 3] 컨테이너 비율 역산
    필요 공간에 맞춰 body:sidebar 비율 조정
    ↓
[Phase 4] 폰트 미세 조정
    컨테이너 확정 후, 위계 범위 내에서 최적 폰트 결정
    ↓
[Phase 5] 레이아웃 결정
    폰트 최소값으로도 안 되면 구조 변경 (2단 등)
    ↓
[Phase 6] 프롬프트에 확정된 수치 전달
    "10px, 22줄, overflow:hidden" — Claude에게 정확한 제약 조건

4. 수학적 기초

기본 상수 (Pretendard 한글, space_allocator.py 실측)

DEFAULT_AVG_CHAR_WIDTH_PX = 14.4   (at 15.2px font)
→ 글자폭 비율: font_size × 0.947
줄 높이 비율: font_size × 1.5 (본문 기준, 영역마다 조정 가능)

수용량 계산 공식

줄당 글자 수 = (가용 너비 - 들여쓰기) ÷ (font_size × 0.947)
줄 수 = (가용 높이 - padding - 헤더) ÷ (font_size × 줄높이비율)
총 수용량 = 줄당 글자 수 × 줄 수

가용 공간 계산

슬라이드: 1280 × 720px
padding: 40px × 2 = 80px (상하좌우)    ← tokens.css --spacing-page
grid gap: 20px × 2 = 40px (행/열 간격) ← tokens.css --spacing-block
header: ~66px (2rem × 1.7 + padding 8px + border 3px)

내부 영역: (1280 - 80) × (720 - 80) = 1200 × 640px
body+sidebar 행 높이: 640 - 66(header) - 60(footer) - 40(gaps) = 474px

열 너비 계산 (space_allocator.py):
  zone_width_px = int(slide_width × zone_width_pct / 100 × 0.85)
  현재 body: 1280 × 65% × 0.85 = 707px
  현재 sidebar: 1280 × 35% × 0.85 = 380px

예시 계산: sidebar (현재 35% = 380px)

텍스트: 용어 3개, 총 ~400자
폰트: 10px
줄 높이: 10 × 1.5 = 15px
카드 chrome: 3 × (title ~22px + padding 14×2=28px + margin 10px) = 3 × 60 = 180px
header "용어 정의": ~30px, outer padding: 16×2 = 32px
가용 높이: 474 - 32 - 30 - 180 = 232px
가용 줄 수: 232 ÷ 15 ≈ 15줄 (카드당 5줄)
줄당 글자: (380 - 32 - 28 - 14) ÷ (10 × 0.947) = 306 ÷ 9.47 ≈ 32자
총 수용: 15 × 32 = 480자 > 400자 → OK

만약 용어 6개, 총 ~800자라면?
카드 chrome: 6 × 60 = 360px → 가용 높이 = 474 - 32 - 30 - 360 = 52px
줄 수 = 52 ÷ 15 = 3줄 → 3 × 32 = 96자 ≪ 800자 → FAIL
→ 9px로 축소: 줄 높이 13.5px, 줄 수 = 52/13.5 = 3줄 → 여전히 FAIL
→ 2단 레이아웃 또는 body:sidebar 비율 변경 필요

예시 계산: body 배경 (현재 65% = 707px, 높이 176px)

텍스트: 혼용 설명 3문장(~150자) + 정책 사례 2건(~180자) = 총 ~330자
폰트: 11px (위계 기준 10-12px)
줄 높이: 11 × 1.4 = 15.4px
outer padding: 10×2 + 14×2 = 48px (상하+좌우)
제목 2개: 2 × 20px = 40px
사례 카드 chrome: 2 × (제목 16px + padding 12px) = 56px
가용 높이: 176 - 20 - 40 = 116px (좌측 텍스트) / 176 - 20 - 56 = 100px (우측 사례)
좌측 줄 수: 116 ÷ 15.4 ≈ 7줄
줄당 글자: (707/2 - 48 - 14) ÷ (11 × 0.947) = 291 ÷ 10.4 ≈ 28자
좌측 수용: 7 × 28 = 196자 > 150자 → OK

만약 body:sidebar = 70:30이면?
body 너비: 1280 × 70% × 0.85 = 762px
좌측 줄당 글자: (762/2 - 48 - 14) ÷ 10.4 = 319 ÷ 10.4 ≈ 30자
→ 2자 더 여유, 12px 폰트도 가능할 수 있음

5. 동적 계산이 어려운 이유

텍스트 양 → 폰트 크기 → 줄 수 → 레이아웃 → 컨테이너 크기
    ↑                                              ↓
    └──────────── 서로 영향을 줌 ────────────────────┘
  • 텍스트가 많으면 폰트를 줄여야 함
  • 폰트를 줄이면 줄 수가 늘어남
  • 줄 수가 늘어나면 높이가 부족할 수 있음
  • 높이가 부족하면 레이아웃을 바꿔야 함 (1단 → 2단)
  • 레이아웃을 바꾸면 너비가 줄어서 줄당 글자 수가 줄어듦
  • 줄당 글자 수가 줄면 다시 줄 수가 늘어남...

다단 레이아웃 판단 기준

사전에 텍스트 양을 보고, 비율과 다단 여부를 판단해야 한다. 하드코딩("22줄, 60자/줄")이 아니라 동적 판단 로직이 필요.

판단 흐름:
1. 위계 기준 폰트로 수용량 계산 (기본 폰트)
2. 텍스트 양 > 수용량 × 1.0 → 폰트 1px 축소 (위계 범위 내)
3. 최소 폰트로도 텍스트 양 > 수용량 → 레이아웃 변경 (1단→2단)
4. 2단으로도 불가 → 컨테이너 비율 조정 (sidebar 축소 → body 확대)
5. 비율 조정으로도 불가 → 텍스트 편집 필요 (경고)

다단 전환 기준:

  • sidebar: 카드 3개 이하 = 1단, 4개 이상이면서 최소 폰트로 불가 = 2단 검토
  • 배경: topic 2개 = 가로 flex (현재), topic 3개 이상 = 2행 flex 검토
  • 본문: 항상 1단 (이미지 float으로 실질 2단 효과)

해결: 보수적 추정 + 검증 1회 (하이브리드)

100% 정확한 사전 계산 대신:

  1. 보수적으로 계산 (실제보다 10-15% 작게 추정)
  2. 위계 범위 내에서 폰트 결정 (단계적 판단 로직)
  3. 다단 필요 여부 사전 판단 (텍스트 양 ÷ 수용량)
  4. Claude에게 정확한 수치 전달
  5. content_verifier + Selenium으로 1회 검증 (이미 구현됨)
  6. 실패 시 1단계 축소 후 재생성 (이미 구현됨)

6. 컨테이너 비율 조정

핵심 방향

배경과 본심의 컨테이너를 키우고 글씨 크기를 맞추고, 첨부의 컨테이너를 줄이고 글씨를 작게 한다.

현재

grid-template-columns: 65fr 35fr  (body 65% : sidebar 35%)
body 너비: 1280 × 65% × 0.85 = 707px
sidebar 너비: 1280 × 35% × 0.85 = 380px

제안: 텍스트 양에 따라 동적 조정

sidebar 텍스트가 적으면 → 70:30 또는 72:28
  body: 1280 × 70% × 0.85 = 762px  /  sidebar: 1280 × 30% × 0.85 = 326px
  body: 1280 × 72% × 0.85 = 783px  /  sidebar: 1280 × 28% × 0.85 = 305px

sidebar 텍스트가 보통이면 → 68:32
  body: 1280 × 68% × 0.85 = 741px  /  sidebar: 1280 × 32% × 0.85 = 348px

sidebar 텍스트가 많으면 → 65:35 (현재 유지)
  body: 707px  /  sidebar: 380px

판단 기준:

sidebar 글자 수 ÷ sidebar 수용량(최소 폰트 9px 기준) = 충전율

충전율 < 50% → 비율 축소 (28-30%) — body가 넓어짐, 배경 12px 가능
충전율 50-80% → 보통 (32-35%)
충전율 > 80% → 현재 유지 또는 확대, 다단 검토

비율 변경의 효과

                현재 (65:35)        변경 후 (70:30)
배경 폰트:      9-11px (작음)       11-12px (적절)
본심 폰트:      12px               12px (유지)
sidebar 폰트:   14px (너무 큼)      9-11px (적절)
위계:           ❌ 역전             ✅ 정상

7. 블록 디자인 선택 문제 (미해결, 별도 Phase 검토)

현재 상태

Phase S의 4개 프롬프트(BG/CORE/SIDEBAR/FOOTER)에 CSS가 직접 하드코딩되어 있음. blocks/ 디렉토리에 블록 템플릿이 있지만 전혀 참조되지 않음. 어떤 MDX가 와도 항상 동일한 디자인이 나옴.

Phase T에서는

폰트 위계와 컨테이너 비율에 집중. 블록 디자인 다양성은 Phase T 범위 밖. 다만, 동적 계산 시스템이 완성되면 "이 콘텐츠에 어떤 레이아웃이 적합한가"를 판단하는 기반이 되므로, Phase T가 선행되어야 함.


8. 정렬 규칙

  • "용어 정의" 라벨: 카드 좌측 시작점에 맞춰 정렬 (중앙 정렬 아님)
  • 각 영역의 텍스트 시작점이 수직으로 정렬되어야 함

9. 구현 대상

신규 함수 (위치: src/space_allocator.py 또는 신규 모듈)

def analyze_text_volume(content: str, analysis: dict) -> dict[str, int]:
    """각 영역별 글자 수 측정"""

def calculate_font_sizes(text_volumes: dict, container_specs: dict) -> dict[str, float]:
    """위계 범위 내에서 최적 폰트 크기 결정"""

def calculate_container_ratios(text_volumes: dict, font_sizes: dict) -> dict:
    """필요 공간에 맞춰 body:sidebar 비율 역산"""

def calculate_text_capacity(width_px: int, height_px: int, font_size: float, ...) -> int:
    """주어진 컨테이너에서 수용 가능한 글자 수"""

수정 대상

  • html_generator.py: 프롬프트에 동적 폰트 크기 전달 ({font_size} 변수 추가)
  • space_allocator.py: 컨테이너 비율 동적 조정
  • content_verifier.py: 폰트 크기 균형 검증 추가 (L3 확장)

10. 자기 검증 체크리스트

  • 핵심 메시지(key-msg)가 14px로 가장 큰가?
  • 본문(core)이 12px인가?
  • 배경(bg)이 10-12px 범위인가?
  • 첨부(sidebar)가 9-11px으로 가장 작은가?
  • 폰트 크기가 중요도 순서와 일치하는가? (핵심 > 본문 > 배경 > 첨부)
  • 컨테이너 비율이 텍스트 양에 맞게 조정되었는가?
  • "용어 정의" 라벨이 카드 좌측에 정렬되었는가?
  • 모든 영역에서 텍스트가 잘리지 않는가?
  • 다른 MDX(텍스트 양이 다른)에서도 위계가 유지되는가?


Phase T — 실행 계획

각 Step(T-0~T-4)별로 무엇을(큰 꼭지), 어떻게(세부 태스크) 수행하는지 정리. 각 세부 태스크의 기술/도구는 별도 조사 후 확정. 여기서는 "무엇을 왜 하는지"에 집중.


T-0. 파이프라인 기반 구조

A. PipelineContext 객체 설계

목적: 모든 Stage가 하나의 누적 객체를 통해 데이터를 주고받도록 한다.

# 태스크 설명 조사 필요
a Stage간 전달 데이터 전수 조사 현재 step*.json의 모든 키를 수집하여 어느 Stage에서 생성/소비되는지 매핑
b 필드 분류 각 필드를 생성 Stage × 소비 Stage로 정리. 불필요한 중복 필드 제거
c PipelineContext 스키마 설계 Pydantic BaseModel 또는 dataclass. 각 필드에 타입, 생성 시점, 소비 시점 명시 Pydantic vs dataclass 직렬화 성능
d save_snapshot() 구현 각 Stage 완료 시 context 전체를 JSON 직렬화하여 저장. 기존 step*.json 역할 대체 JSON 직렬화에서 Path, dataclass 등 처리
e 기존 디버깅 도구 호환성 generate_run_report.py가 새 스냅샷을 읽을 수 있는지 확인. 필요 시 수정

B. run_stage() 패턴 구현

목적: 모든 Stage의 공통 실행/검증/재시도 패턴을 정의한다.

# 태스크 설명 조사 필요
a 공통 패턴 설계 transform(context) → validate(result) → update(context) → snapshot()
b 에러 분류 체계 FATAL(중단) / RETRYABLE(재시도) / ADJUSTABLE(자동조정) 3등급
c 재시도 로직 RETRYABLE 시 피드백을 context에 기록 + 해당 Stage만 재실행. 최대 횟수는 Stage별 설정 LLM retry 시 가장 효과적인 피드백 형식
d FATAL 처리 파이프라인 중단 + 에러 상세를 context에 기록 + SSE error 이벤트
e ADJUSTABLE 처리 코드로 자동 조정 + 경고를 context에 기록. 다음 Stage는 조정된 값으로 진행
f Stage 시그니처 통일 모든 Stage: async def stage_fn(context) → dict (결과 + _errors 키)

C. 기존 pipeline.py 리팩토링

목적: 현재 generate_slide()를 T-0 패턴으로 전환한다.

# 태스크 설명 조사 필요
a 현재 Stage 호출 분석 generate_slide() 내 변수 흐름 전수 분석. 어떤 변수가 어디서 어디로
b Stage 함수 래핑 각 기존 함수를 context 입출력으로 래핑. 내부 로직은 최소 변경
c SSE 스트리밍 통합 run_stage() 완료마다 progress 이벤트 yield. 기존 SSE 구조 유지 async generator + run_stage 조합
d 기존 step*.json 코드 제거 save_snapshot()으로 대체. 기존 저장 코드 삭제

산출물: src/pipeline_context.py (신규), src/pipeline.py (수정)


T-1. MDX 표준화

A. 기존 normalize_mdx() 분석

목적: 현재 처리 범위와 한계를 정확히 파악한다.

# 태스크 설명 조사 필요
a 현재 처리 항목 전수 조사 html_generator.py:401~476의 모든 regex 패턴과 처리 결과 정리
b 미처리 패턴 수집 실제 MDX 파일을 넣어서 정규화 후 남는 JSX/HTML 잔여물 확인
c 실제 문제 사례 수집 run 로그에서 Stage 1A/1B에 raw MDX가 전달되어 발생한 문제 확인

B. 4층 레이어 MDX 파서 구현

목적: 각 도구가 가장 잘하는 것만 조합하여 정확한 파싱을 달성한다.

# 태스크 설명 조사 필요
a Layer 1: YAML 추출 python-frontmatter로 frontmatter → dict. title, sidebar 등 분리 python-frontmatter API
b Layer 2: MDX 전용 패턴 Regex로 Astro :::directive, <details>/<summary>, JSX style={{}}, import/export 처리
c 코드블록 보호 ``` 영역을 placeholder로 보호 → JSX 제거 → 복원. 순서 필수 (안 하면 코드블록 내용 파괴)
d Layer 3: AST 파싱 markdown-it-py로 제목/표/이미지/리스트 구조 추출 markdown-it-py table/image token 구조
e Layer 4: 텍스트 정리 남은 HTML 태그 정리, 빈 줄 정리, 최종 clean_text 생성

C. 파이프라인 연결

목적: Stage 0으로 파이프라인 맨 앞에 삽입한다.

# 태스크 설명 조사 필요
a mdx_normalizer.py 생성 normalize() → {clean_text, title, images[], popups[], tables[]} 반환
b pipeline.py 연결 최상단에서 호출 → context에 저장
c html_generator.py 정리 기존 normalize_mdx() 호출 제거 → context.clean_text 사용
d Stage 1A/1B 입력 변경 raw content → context.clean_text로 전환

산출물: src/mdx_normalizer.py (신규), src/pipeline.py (수정), src/html_generator.py (수정)


T-2. 각 Stage에 검증 추가

A. 검증 스키마 설계 (Pydantic)

목적: 각 Stage 출력의 형식 검증을 타입 시스템으로 보장한다.

# 태스크 설명 조사 필요
a Stage 1A 스키마 AnalysisSchema: topics[], page_structure, weight 합산, 본심 존재 검증 Pydantic v2 model_validator
b Stage 1B 스키마 ConceptSchema: relation_type enum, expression_hint 필수, source_data 필수
c ContainerSpec 스키마 height_px > 0, topic당 최소 높이
d 크로스 필드 검증 model_validator에서 weight 합 0.9~1.1, 본심 weight ≥ 0.3 등

B. 내용 검증 로직 (원본 대조)

목적: 단순 형식 체크를 넘어 "결과가 원본에 대해 적절한가"를 판단한다.

# 태스크 설명 조사 필요
a 키워드 추출 원본 clean_text에서 명사/핵심어 추출 한국어 키워드 추출: konlpy vs kiwi vs regex. 의존성 최소화 관점
b 1A 대조: summary 검증 topic summary의 키워드가 원본 해당 섹션에 존재하는지. Kei가 해석을 덧붙이지 않았는지
c 1B 대조: source_data 검증 source_data 키워드가 원본에 존재하는지. 없는 출처를 만들어내지 않았는지 (할루시네이션 방지)
d 1B 대조: relation_type 검증 relation_type에 해당하는 언어적 패턴이 원본에 있는지. comparison → "vs/반면/차이", sequence → "→/이후/다음" 등 한국어 관계 표현 패턴 수집

C. 모순 결정 테이블

목적: purpose × relation_type × layer 간 논리적 모순을 탐지한다.

# 태스크 설명 조사 필요
a 하드 모순 정의 purpose × relation_type 확실한 모순 쌍 정의 (결론강조+comparison 등)
b 소프트 경고 정의 의심 수준의 조합 (핵심전달+definition 등)
c layer × role 모순 conclusion+reference=모순, intro+reference=모순
d 피드백 문장 생성 각 모순에 "왜 모순인지" + "대안 제안" 문장 연결
e 결정 테이블 구현 데이터(dict)로 규칙, 코드로 판정. 규칙 추가/삭제가 코드 변경 없이 가능하도록

D. 실패 피드백 생성 및 회귀

목적: 검증 실패 시 구체적 피드백으로 재시도하여 품질을 올린다.

# 태스크 설명 조사 필요
a 피드백 템플릿 설계 에러별로 어떤 정보를 포함할지: 실패 필드, 현재 값, 원본 해당 부분, 대안
b Kei 재요청 방식 기존 프롬프트 + "## 이전 응답의 문제점" 섹션 추가 LLM retry 피드백 최적 형식 (Instructor/PPTAgent)
c 재시도 횟수 관리 AI Stage: 최대 2회, 코드 Stage: 자동 조정 1회
d 최선 결과 처리 재시도 초과 시 최선 결과로 진행 + 경고를 context에 기록

E. 기존 검증과 통합

목적: Stage 2 이후의 기존 검증을 T-0 패턴에 맞추어 정리한다.

# 태스크 설명 조사 필요
a 통합 설계 content_verifier.py의 5층 검증이 run_stage 패턴에 어떻게 들어가는지
b 에러 재분류 기존 검증 에러를 FATAL/RETRYABLE/ADJUSTABLE로 분류
c 중복 제거 Stage 0~1.5에서 이미 검증한 것을 Stage 2에서 다시 하지 않도록

산출물: src/validators.py (신규), src/pipeline.py (수정), src/content_verifier.py (수정)


T-3. 블록 참고 디자인 + 프롬프트 정비

A. relation_type → 참고 블록 매핑 설계

목적: 콘텐츠의 논리 관계에 맞는 블록을 코드가 결정론적으로 선택한다.

# 태스크 설명 조사 필요
a RELATION_BLOCK_MAP 정의 7개 relation_type × 참고 블록 후보 (우선순위순)
b expression_hint 2차 필터 같은 comparison이라도 "대등 비교" vs "우열 비교"에 따라 다른 블록 실제 run에서 나온 expression_hint 패턴 수집
c 매핑 근거 정리 왜 hierarchy→venn인지 (Gestalt 원칙, 시각 커뮤니케이션 이론) 관계유형→시각패턴 학술 근거
d fallback 정의 매핑에 해당 블록 없을 때 기본값
e 매핑 위치 결정 코드(결정론적) vs catalog.yaml(수정 용이). 장단점 비교 후 결정

B. 참고 블록 HTML 로딩 시스템

목적: 선택된 블록의 실제 HTML을 프롬프트에 제공할 수 있도록 한다.

# 태스크 설명 조사 필요
a load_block_html() catalog.yaml → template 경로 → HTML 파일 로드
b mtime 캐시 블록 파일 변경 시 자동 갱신. 38개 블록 ~76KB 메모리
c 시작 시 검증 validate_block_references() — 매핑된 블록 전체 존재 확인
d build_design_brief() catalog의 visual + when + visual_diff(T-4) → 자연어 브리프

C. 프롬프트 재설계

목적: 하드코딩 CSS를 제거하고, context의 누적 정보를 체계적으로 프롬프트에 주입한다.

# 태스크 설명 조사 필요
a 하드코딩 CSS 제거 BG/CORE/SIDEBAR/FOOTER_PROMPT에서 고정 CSS 섹션 제거
b 참고 블록 주입 {reference_design} 변수로 블록 HTML + design brief 삽입
c "참고/복사금지" 프레이밍 참고할 것(색상, 레이아웃) / 참고하지 말 것(구조 복사, 항목 수 맞추기) 명시 few-shot reference 시 LLM 적응 vs 복사 경향
d expression_hint 지시 "이 콘텐츠의 관계는 {relation_type}이다. 시각적으로 드러나도록"
e context 누적 정보 주입 core_message(1A), containers.height_px(1.5), relation_type(1B) 등 체계적 삽입
f 공통 규칙 일관성 텍스트 보존, inline style 등 중복 규칙을 상수로 추출, 영역별 차이만 분리

D. html_generator.py 수정

목적: generate_slide_html()이 context 기반으로 동작하도록 전환한다.

# 태스크 설명 조사 필요
a context에서 references 꺼내기 generate_slide_html()이 context.references 사용
b 프롬프트 조립 로직 context.containers + context.references + 공통규칙 + 영역별규칙 → 최종 프롬프트
c 기존 함수 전환 _map_sections_for_role(), _get_definitions() 등이 context.clean_text 사용

산출물: src/block_reference.py (신규), src/html_generator.py (수정)


T-4. 블록 설명 catalog.yaml 보완

A. 유사 블록 그룹 정의

목적: visual_diff가 필요한 블록 20개를 특정한다.

# 태스크 설명 조사 필요
a 그룹핑 38개 블록을 시각적 유사성 기준으로 그룹. 비교(5), 카드(6), 강조(4), 흐름(2), 헤더(3) = 20개
b 그룹 내 유일 블록 제외 유일한 블록은 기존 visual만으로 충분

B. 실제 렌더링 기반 차이점 확인

목적: "추정"이 아닌 "실측"으로 차이점을 기술한다.

# 태스크 설명 조사 필요
a 블록 독립 렌더링 각 블록을 샘플 데이터로 렌더링 → 스크린샷 캡처 Selenium/Playwright로 블록 독립 렌더링 스크립트
b 그룹 내 시각 비교 같은 그룹 블록들을 나란히 놓고 차이점 확인
c 실측 높이 검증 height_cost 분류가 실제와 맞는지 확인. 불일치 시 catalog 수정

C. visual_diff 작성 및 반영

목적: 각 블록의 고유한 차이점을 catalog.yaml에 추가한다.

# 태스크 설명 조사 필요
a visual_diff 작성 그룹 내 다른 블록과의 차이. "이 블록만의 특징", "적합/부적합 상황" 구체적 예시 포함
b catalog.yaml 반영 20개 블록에 visual_diff 필드 추가
c T-3 연동 확인 build_design_brief()가 visual_diff를 읽는지 확인
d FAISS 인덱스 재빌드 catalog 변경 후 build_block_index.py 재실행 필요 여부 확인

산출물: templates/catalog.yaml (수정)


전체 태스크 요약

Step 큰 꼭지 세부 태스크 수 핵심 산출물
T-0 A(Context) + B(run_stage) + C(리팩토링) 16개 pipeline_context.py, pipeline.py
T-1 A(분석) + B(파서) + C(연결) 12개 mdx_normalizer.py
T-2 A(스키마) + B(내용검증) + C(모순) + D(피드백) + E(통합) 18개 validators.py
T-3 A(매핑) + B(로딩) + C(프롬프트) + D(수정) 17개 block_reference.py, html_generator.py
T-4 A(그룹) + B(렌더링) + C(작성) 9개 catalog.yaml
합계 15개 꼭지 72개 태스크 5개 파일

조사 항목 (전체 완료 — 2026-04-01)

Step 항목 결과 요약 상태
T-0 Pydantic vs dataclass Pydantic BaseModel 채택. model_dump_json() 직렬화, validate_assignment=True 타입 검증
T-0 async generator + run_stage Approach C 채택. Stage가 coroutine이든 async generator이든 run_stage()가 통합 처리
T-1 python-frontmatter v1.1.0. parse() → (dict, str) 튜플. 의존성 43KB. frontmatter 없는 문서도 안전 처리
T-1 markdown-it-py v4.0 js-default. table 기본 포함. flat token list로 table/image 추출 간결 (~20줄). 한국어 문제 없음
T-1 코드블록 보호 backtick 10→3 순서 매칭. 중첩/inline 모두 검증됨
T-2 한국어 키워드 추출 kiwipiepy 채택. Java 불필요, Windows 즉시 동작, pip 한 줄. konlpy 부적합(Java 의존)
T-2 관계 표현 패턴 7개 relation_type별 15개+ regex 패턴 수집 완료. 건설/BIM/DX 도메인 기준
T-2 LLM retry 피드백 형식 Self-Refine(NeurIPS 2023) 채택. localization + evidence + instruction. VASCAR Scorer+Suggester 분리
T-3 expression_hint 패턴 10개 고유값 → 5개 시각적 유형 분류. definition 내 2차 구분 필수 확인
T-3 few-shot reference LLM 경향 구조 70-90% 복사. "디자인 레퍼런스" 프레이밍 최적. HTML+구조주석 > CSS만
T-3 관계유형→시각패턴 근거 Gestalt 폐합→벤, 근접→좌우, 연속→화살표. PPTAgent(EMNLP 2025) 학술 입증
T-4 블록 독립 렌더링 Selenium 사용 (이미 존재). 브라우저 1회→42회 navigate. ~1분. scripts/capture_block_screenshots.py

아키텍처 검토 결과 발견 사항 (2026-04-01)

조사 완료 후 아키텍처 문서(ARCHITECTURE-PHASE-T.md)와 실제 코드를 대조하여 발견된 보완 사항.

설계 보완 (아키텍처 반영 완료)

# 발견 사항 해결
1 Stage 1.5의 순환 의존 — design_budget이 block_schema 필요하지만 블록은 1.7에서 선택 1.5a→1.7→1.5b 분리. 1.5a는 폰트 위계+비율만, 1.5b는 블록 선택 후 디자인 예산 재계산
2 expression_hint 매칭이 정확한 문자열 매칭 → 실제 hint는 긴 문장 키워드 포함(substring) 매칭으로 변경. VISUAL_TYPE_KEYWORDS dict
3 폰트 위계가 아키텍처에 명시 안 됨 — Phase T 핵심인데 Section 1.2에 폰트 위계 + Stage 1.5a에 역산 로직 추가
4 동적 컨테이너 비율이 없음 — 고정 65:35 Stage 1.5a에 텍스트 양 기반 비율 역산 추가
5 Stage 1A/1B 검증 누락 Stage 1A: Pydantic + 원본 대조, 1B: 모순 탐지 + source_data 할루시네이션 감지 추가
6 Stage 1.7 fallback 미정의 카테고리별 fallback 블록 테이블 정의
7 재시도 총 예산 불분명 Stage별 최대 횟수 + 전체 300초 타임아웃 명시
8 마이그레이션 맵 없음 Section 8: 신규/수정/삭제 파일 맵 추가

사실 오류 (아키텍처 수정 완료)

# 오류 수정
1 웹 프레임워크 Flask → FastAPI 수정
2 글자폭 비율 0.75 → 0.947 수정
3 relation_type 6개 → 7개 (none 추가) 수정
4 카탈로그 카테고리 5개 → 6개 (tables, visuals 추가) 수정
5 weight 합 "= 100%" → "0.9~1.1 범위" 수정
6 Stage 0 출력에 popups, tables 누락 추가
7 Stage 1A 출력에 source_hint 누락 추가