# 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 → 별도 보관
- 이미지 추출: `` → `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 누락 | 추가 |