# 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 객체가 파이프라인을 따라가며 점진적으로 풍부해진다. ```python @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() 패턴 ```python 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. 파이프라인 러너 ```python 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에서 필요한 정보를 꺼낸다: ```python # 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.py`에 `normalize_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]` 마커 - 팝업 마킹: `
제목내용
` → `[팝업: 제목]\n내용\n[/팝업]` - 핵심요약 마킹: `:::note[제목]` → `[핵심요약]\n내용\n[/핵심요약]` - 표 보존: markdown table 구조 유지 **출력:** ```python { "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[] 수 비교 — 이미지가 누락되었는지 - 원본의 `
` 수와 추출된 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 | 텍스트/강조 | ### 적용 방식 **현재:** ```python prompt = CORE_PROMPT.format(height=h, content_block=text, ...) # relation_type 무관하게 항상 같은 CSS ``` **변경:** ```python # 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` 필드를 추가. ```yaml - 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과의 연계 ```python 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 또는 신규 모듈) ```python 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`, `
/`, 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 누락 | 추가 |