# 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 변환, 폰트 캡 → 유지