# Phase V (Verification) — 콘텐츠-컨테이너 적합성 검증 + Kei 에스컬레이션

> 작성일: 2026-04-02
> 근거: Phase T' 디버깅 과정에서 발견된 파이프라인 구조적 결함
> 선행: Phase T (파이프라인 구조), Phase T' (시각 품질)

---

## 배경

### 발견된 구조적 문제

Phase T' 디버깅 중 step-by-step 시각 검토를 진행하면서 다음이 드러남:

1. **컨테이너 크기가 콘텐츠 분량과 무관하게 결정됨**
   - Stage 1.5a에서 weight(0.6, 0.2, 0.1, 0.1) 고정 배분
   - 배경에 꼭지 2개(220자)가 배정되었으나 117px밖에 안 됨 → 넘침
   - 본심은 345px인데 실제 필요 260px → 85px 남음

2. **"들어가는지" 검증 단계가 없음**
   - 블록 선택(Stage 1.7) 후 바로 HTML 생성(Stage 2)으로 넘어감
   - 콘텐츠가 컨테이너에 실제로 들어가는지 아무도 확인하지 않음
   - Sonnet이 넘치는 내용을 받아서 overflow/스크롤/잘림 발생

3. **안 될 때 판단하는 주체가 없음**
   - 공간 부족 시 옵션(합치기, 축약, 팝업 이동, 구조 변경)을 생성하고
   - Kei 페르소나에게 결정을 요청하는 프로세스가 없음
   - 현재는 그냥 Sonnet에게 "넣어라"만 함

4. **영역당 블록 1개만 선택됨**
   - 배경에 꼭지 2개가 있어도 블록 1개(callout-warning)만 선택
   - 1꼭지 = 1블록 원칙이 지켜지지 않음

---

## 절대 원칙

1. **하드코딩 금지** — font-size 외 모든 수치는 동적 계산. 어떤 MDX가 들어와도 동일하게 동작
2. **스크롤 절대 금지** — overflow:auto/scroll 어떤 영역에서도 불허
3. **1꼭지 = 1블록** — 컨테이너에 꼭지 N개면 블록 N개가 개별 선택
4. **콘텐츠 분량 → 컨테이너 크기** — weight 고정이 아니라 콘텐츠 필요 높이 기반 배분
5. **AI가 옵션 생성, Kei가 결정** — 안 될 때 하드코딩 대응이 아니라 Kei 판단 요청

---

## 개선된 파이프라인

```
기존:
  1A → 1B → 1.5a(weight고정) → 1.5b → 1.7(영역당1블록) → 2(HTML) → 3 → 4
                                                          ↑
                                              여기서 넘치거나 잘림

개선:
  1A → 1B → 1.7(꼭지별1블록) → 1.8★(적합성검증) → 2(HTML) → 3 → 4
                                  │
                                  ├ 필요 높이 계산
                                  ├ 컨테이너 재배분
                                  └ 안 되면 → Kei 에스컬레이션
```

### 변경 사항 요약

| Stage | 기존 | 개선 |
|-------|------|------|
| 1.5a | weight 고정 배분 | 삭제 — 1.8에서 콘텐츠 기반 계산 |
| 1.7 | 영역당 블록 1개 | **꼭지당 블록 1개** |
| **1.8 (신규)** | 없음 | **적합성 검증 + 재배분 + Kei 에스컬레이션** |

---

## Stage 1.8: 적합성 검증 (신규)

### 입력

- 꼭지 목록 + 영역 배정 (Stage 1A/1B)
- 꼭지별 선택된 블록 + 블록 최소 높이 (Stage 1.7)
- 슬라이드 크기 (1280×720), padding, gap 등 고정 스펙

### 처리 흐름 (AI가 자동 — 하드코딩 아님)

```
Step 1: 필요 높이 계산
  각 컨테이너별로:
    - 배정된 꼭지들의 텍스트 분량(자수) 파악
    - 해당 영역 font-size + line-height로 필요 줄 수 계산
    - 선택된 블록의 padding, 제목, 마진 등 오버헤드 합산
    - → 필요 최소 높이(px) 산출

Step 2: 슬라이드 공간 배분
  720px 에서:
    - header(고정) + footer(고정) + gap 빼기
    - 남은 공간을 각 영역의 필요 높이 비율로 배분
    - sidebar는 body와 같은 row이므로 sidebar 높이 = body 영역 합계

Step 3: 적합성 검증
  각 컨테이너별로:
    - 배분된 높이 ≥ 필요 높이 → 통과
    - 배분된 높이 < 필요 높이 → Step 4로

Step 4: 재배분 시도
  여유 있는 영역에서 부족한 영역으로 공간 이동:
    - 각 영역의 (배분 높이 - 필요 높이) = 여유분 계산
    - 여유분 > 0인 영역에서 부족 영역으로 재분배
    - 재배분 후 모든 영역이 필요 높이 이상 → 통과
    - 아직 부족 → Step 5로

Step 5: Kei 에스컬레이션
  AI가 현황 + 시도 결과 + 옵션을 정리하여 Kei에게 요청:

    [현황]
    - 어떤 영역이 몇 px 부족한지
    - 어떤 영역에 여유가 있는지

    [시도 결과]
    - 재배분으로 해결 가능한지/불가능한지
    - 해결 가능하면 어떤 영역에서 얼마를 가져오는지

    [옵션]
    A. 꼭지 합치기 — 여러 꼭지를 하나의 블록 안에서 흐름으로 연결
    B. 인라인 축약 — 사례 등을 괄호 한 줄로 축약
    C. 팝업 이동 — 상세 내용을 팝업으로 빼고 링크만 남김
    D. 컨테이너 재조정 — 다른 영역에서 공간을 가져옴
    E. 그리드 구조 변경 — 배경 전체폭 등 레이아웃 자체 변경
    F. 기타 (Kei 판단)

    [결정 요청]
    위 옵션 중 선택하거나 다른 방향을 제시해주세요.
```

### Stage 1.8 내부 루프

```
Step 1: 부족/여유 검증 (calculate_fit)
Step 2: 재배분 시도 (redistribute)
Step 3: 부족 시 → Kei 에스컬레이션 (call_kei_fit_escalation)
Step 4: 여유 시 → 보충 콘텐츠 탐색 (analyze_enhancements)
  ├ 관련 팝업에 구조화 콘텐츠(표/비교) 있으면 제안
  ├ 영역 핵심 결론 → 강조 블록 제안
  └ 텍스트 핵심 키워드 → bold 목록 생성
Step 5: Kei 확인 (AI가 제안, Kei가 승인/수정)
Step 6: 보충 블록 선택 + fit 재검증
  ├ Kei가 승인한 보충 콘텐츠에 맞는 블록을 catalog에서 선택
  ├ 추가 블록의 높이가 여유 공간에 들어가는지 재검증
  └ 안 들어가면 축소 (행 수 줄이기) 또는 제외
Step 7: 세부 컨테이너 배치 계산
  ├ 메인 컨테이너 안에서 세부 컨테이너 배치 (SVG/텍스트/표/key-msg)
  ├ 각 세부 컨테이너 크기를 콘텐츠에서 동적 계산
  ├ 빈 공간 측정 → 보충 콘텐츠 크기 결정 (표 행 수 등)
  ├ 세부 컨테이너 간 정렬 (좌우 높이 다르면 짧은 쪽 중앙맞춤)
  └ 최종 overflow 검증
Step 8: 확정 출력
```

### 출력

- 확정된 컨테이너 크기 (재배분 반영)
- 각 컨테이너별 꼭지-블록 매핑 (Kei 결정 반영)
- 보충 블록 목록 (여유 공간에 추가된 블록)
- 강조 블록 목록 (핵심 결론용)
- bold 키워드 목록 (Stage 2 프롬프트에 전달)
- 콘텐츠 정리 방향 (합치기/축약/팝업 등 — Kei 결정 반영)

---

## 태스크 목록

### V-1: Stage 1.7 수정 — 꼭지별 블록 선택

- **현재:** `select_reference_block()`이 영역(배경/본심/첨부/결론) 단위로 1개 블록 선택
- **변경:** 각 영역 내 꼭지마다 개별적으로 블록 선택
- **파일:** `src/block_reference.py`, `src/pipeline.py` (Stage 1.7 호출부)
- **완료 기준:** 배경에 꼭지 2개 → 블록 2개 선택. 꼭지 1개면 블록 1개.

### V-2: Stage 1.8 신규 구현 — 적합성 검증

- **파일:** `src/fit_verifier.py` (신규)
- **내용:**
  - Step 1~3: 필요 높이 계산 + 공간 배분 + 적합성 검증
  - 모든 계산은 동적 (font-size, line-height, 블록 padding 등에서 도출)
- **완료 기준:** 배경 117px → 부족 감지 → 재배분 시도

### V-3: Stage 1.8 재배분 로직

- **파일:** `src/fit_verifier.py`
- **내용:**
  - Step 4: 여유 영역 → 부족 영역으로 공간 재분배
  - 재배분 후 결과를 Stage 2에 전달
- **완료 기준:** 배경 117→151px, 본심 345→311px 자동 재배분

### V-4: Stage 1.8 Kei 에스컬레이션

- **파일:** `src/fit_verifier.py`, `src/kei_client.py`
- **내용:**
  - Step 5: 재배분으로도 안 될 때 옵션 생성 + Kei API 호출
  - Kei 응답 파싱 → 결정에 따라 컨테이너/콘텐츠 조정
- **의존성:** V-2, V-3
- **완료 기준:** Kei에게 옵션 전달 → Kei 결정 수신 → 파이프라인 계속

### V-5: Stage 1.5a 리팩터

- **파일:** `src/space_allocator.py`, `src/pipeline.py`
- **내용:**
  - 기존 weight 고정 배분 로직을 V-2의 콘텐츠 기반 계산으로 교체
  - 또는 1.5a를 삭제하고 1.8이 컨테이너 계산을 전담
- **완료 기준:** weight 하드코딩 제거. 콘텐츠 분량 기반 동적 배분.

### V-6: 통합 검증 — 완료

- 전수 하드코딩 스캔: 레이아웃 하드코딩 0개
- 통합 테스트: 31/31 통과
- step-by-step HTML: step1~step3 생성 + 시각 검토

---

## Phase V-2: 콘텐츠 품질 강화 (Step 3 디버깅에서 발견)

> Step 3 시각 검토에서 발견된 4가지 개선 사항.
> 모두 "AI가 분석, Kei가 확인"하는 동일 프로세스.

### V-7: 주종 관계 블록 내 종속 꼭지 처리

- **발견:** 배경에 꼭지1(intro)+꼭지2(supporting) → 블록 1개로 합쳤지만, 종속 꼭지를 어떻게 표현할지 미정
- **규칙 (동적):**
  - 종속 꼭지 콘텐츠 분량 확인 (fit_verifier의 텍스트 분량 계산 활용)
  - 짧으면 (팝업 참조 1~2줄) → 인라인 (주 블록 안에 한 줄)
  - 길거나 구조 있으면 → 하위 블록 (블록 안의 블록)
  - 독립성 있으면 → 보조 블록 (나란히)
- **판단 기준:** 종속 꼭지의 source_data 길이 + 팝업 참조 여부 + purpose
- **Kei 확인:** AI가 "인라인/하위블록/보조블록" 중 제안 → Kei가 확인
- **파일:** `src/fit_verifier.py`, `src/block_reference.py`

### V-8: 여유 공간 콘텐츠 보충

- **발견:** 본심 컨테이너에 ~53px 여유. 관련 팝업(DX vs BIM 비교표 1135자)이 있는데 활용 안 됨
- **규칙 (동적):**
  - 재배분 후 여유 공간 감지 (shortfall < -threshold)
  - 해당 영역의 꼭지에 관련 팝업이 있는지 확인 (source_data에 [팝업:] 참조)
  - 팝업에 구조화 콘텐츠(표, 비교, 목록)가 있으면
  - 여유 공간에 맞는 블록 추가 선택 (catalog에서)
  - 팝업 핵심만 요약하여 배치 제안
- **Kei 확인:** "53px 여유. 비교표 핵심 3행을 넣을까요?" → Kei가 확인
- **파일:** `src/fit_verifier.py`

### V-9: 영역 핵심 결론 강조 블록

- **발견:** 배경의 "체계적 정립 필요"가 단순 불릿과 동급. 시각적 강조 없음
- **규칙 (동적):**
  - 각 영역의 꼭지 purpose에서 핵심 결론 추출
  - purpose=문제제기 → 마지막 문장이 결론적 패턴("~필요", "~해야")이면 강조 블록
  - purpose=핵심전달 → core_message와 관련된 문장이면 강조 블록
  - 강조 블록: highlight-strip, callout 내 강조 div 등 catalog에서 선택
- **Kei 확인:** "이 문장을 강조 블록으로 처리할까요?" → Kei가 확인
- **파일:** `src/fit_verifier.py`, `src/block_reference.py`

### V-10: 텍스트 핵심 키워드 bold

- **규칙 (동적):**
  - source_data에서 핵심 용어 추출 (꼭지 title에 포함된 키워드, **bold** 마크된 텍스트)
  - 해당 키워드를 HTML 생성 시 bold 처리하도록 Sonnet에게 전달
- **파일:** `src/html_generator.py` (Stage 2 프롬프트에 키워드 목록 포함)

---

## 의존 관계

```
Phase V-1 (완료):
  V-1 → V-2 → V-3 → V-4 → V-5 → V-6 ✅

Phase V-2 (신규):
  V-7 (종속 꼭지 처리) ← fit_verifier 활용
  V-8 (여유 공간 보충) ← fit_verifier 확장
  V-9 (강조 블록) ← purpose 분석
  V-10 (bold 키워드) ← source_data 분석
  → 전체 통합 검증 (step-by-step HTML 재생성)
```

---

## 하드코딩 전수 점검 결과

### 반드시 제거 (Phase V에서 동적 계산으로 교체)

| # | 파일 | 라인 | 값 | 문제 | 교체 방향 |
|---|------|------|-----|------|----------|
| 1 | `space_allocator.py` | 161 | `474` | body zone 높이 고정 | 슬라이드에서 header+footer+gap 빼고 동적 계산 |
| 2 | `space_allocator.py` | 160 | `0.35*0.85` | sidebar 비율+패딩 고정 | container_ratio + 실제 padding에서 계산 |
| 3 | `html_generator.py` | 543 | `720-80-66-footer_h-40` | body zone 높이 산술 하드코딩 | containers에서 받은 height_px 사용 |
| 4 | `html_generator.py` | 604,921 | `380` | sidebar width fallback | containers에서 받은 width_px 사용 (fallback 제거) |
| 5 | `html_generator.py` | 621 | `1200` | footer width fallback | containers에서 받은 width_px 사용 |
| 6 | `design_director.py` | 314 | `490` | FRAME_AVAILABLE_HEIGHT 고정 | 슬라이드 스펙에서 동적 계산 |
| 7 | `design_director.py` | 328-366 | `budget_px` 다수 | 프리셋별 zone budget 고정 | Stage 1.8에서 콘텐츠 기반 재계산 |
| 8 | `space_allocator.py` | 301,583 | `0.85` | 패딩 비율 고정 | `(slide_width - padding*2) / slide_width` 로 계산 |

### fallback 값 (정상 흐름에서는 도달하면 안 됨)

| # | 파일 | 라인 | 값 | 비고 |
|---|------|------|-----|------|
| 9 | `space_allocator.py` | 299 | `490` | zone_budget 기본값 — preset에서 반드시 와야 함 |
| 10 | `space_allocator.py` | 304,313 | `0.25` | weight 기본값 — Kei가 반드시 제공해야 함 |
| 11 | `pipeline.py` | 960 | `490` | budget_px fallback |

### 교체 원칙

- **모든 px 값:** 이전 Stage의 계산 결과(containers, font_hierarchy 등)에서 받아 사용
- **비율 값(0.85 등):** 실제 padding/gap에서 역산하여 계산
- **fallback:** 정상 흐름에서 절대 도달하지 않도록 이전 Stage에서 반드시 값을 제공
- **font-size만 예외:** 디자인 토큰으로 정의된 텍스트 크기는 하드코딩 허용

---

## 이전 Phase와의 관계

- **Phase T:** 파이프라인 Stage 0~5 구조 완성 → Phase V는 Stage 1.7~1.8 개선
- **Phase T' (TP-1~6):** 시각 품질 개선 (프롬프트, 후처리) → Phase V 적용 후 재검증 필요
- **Phase T' 후처리:** sidebar width:100%, overflow 제거, bold 변환, 폰트 캡 → 유지
