# Phase W — 실행 계획 (Task별 방향 + 방법)

> 작성일: 2026-04-03
> 상위 문서: PHASE-W.md

---

## W-1: space_allocator — weight 비율 초기 배정

### W-1-1: zone_budget을 weight 비율로 계산

**현재:** `zone_budget = zone_info.get("budget_px")` → 프리셋 490px 고정
**방향:** `zone_budget = total_available × zone_weight_sum / all_weight_sum`
**파일:** `src/space_allocator.py` — `calculate_container_specs()` 내부
**방법:**
- 전체 가용 높이 = slide_height - padding×2 - gap×2 - header
- 각 zone의 weight 합을 구함 (body zone = 배경+본심 weight, sidebar = 첨부 weight 등)
- 전체 weight 합 대비 비율로 zone_budget 계산
- 이전에 구현했던 코드를 다시 적용 (173113 run에서 동작 확인됨)
**검증:** weight 합 1.0일 때 모든 컨테이너 높이 합 출력하여 전체 가용의 95% 이상인지 확인

### W-1-2: 전체 공간 100% 사용

**방향:** W-1-1이 해결되면 자동으로 해결
**검증:** `stage_1_5a_context.json`에서 모든 컨테이너 height_px 합산 ≥ 전체 가용 × 0.95

### W-1-3: 시선 흐름 배치 좌표

**현재:** `_calc_coords()`가 배경→상단좌, 본심→중앙좌, 첨부→우측, 결론→하단으로 배치
**방향:** 현재 코드 유지 (이미 올바름)
**검증:** `stage_1_8_filled.html`에서 배경이 상단, 본심이 중앙, 첨부가 우측, 결론이 하단에 위치

---

## W-2: block_assembler — 공통 조립 함수 완성

### W-2-1: font_hierarchy override

**현재:** `_override_font()` 함수가 블록 CSS의 font-size를 font_hierarchy로 조정
**방향:** 현재 코드 유지
**검증:** filled HTML에서 첨부 영역의 font-size가 sidebar 값(9-11px)을 초과하지 않음

### W-2-2: 팝업 링크 인접 배치

**현재:** `_parse_structured_text()`에서 `[팝업: 제목]`을 이전 불릿 텍스트에 `[제목→]`으로 붙임
**방향:** 현재 코드 유지
**검증:** filled HTML에서 `[혼용 대표 사례→]`가 별도 줄이 아니라 텍스트 옆에 붙어있음

### W-2-3: sidebar 상단 라벨

**현재:** `_assemble_card_numbered()`에서 `topic.title`을 라벨로 추가
**방향:** 현재 코드 유지
**검증:** filled HTML의 첨부 영역 상단에 꼭지 title이 보임

### W-2-4: 카드 indent 파싱

**현재:** `_assemble_card_numbered()`에서 indent=0만 카드 제목, indent=1은 설명
**방향:** 현재 코드 유지
**검증:** 첨부에 건설산업/BIM/DX 3개 카드가 분리되고, 하위 설명이 각 카드 안에 있음

### W-2-5: 카드 불릿 간격

**현재:** CSS override에서 `white-space: pre-line → normal` 변환
**방향:** 현재 코드 유지
**검증:** 첨부 카드 내 불릿과 불릿 사이에 빈 줄(엔터)이 없음

### W-2-6: 실제 이미지 사용

**현재:** `has_real_image` 분기로 실제 이미지 있으면 SVG 레이아웃, 없으면 텍스트만
**방향:** 수정 필요 — 현재 `_assemble_svg_layout()`이 `design_reference_html`에서 SVG를 추출. 이걸 `ctx.slide_images`의 실제 이미지(base64)로 교체
**파일:** `src/block_assembler.py` — `_assemble_svg_layout()`
**방법:**
- `ctx.slide_images`에서 해당 이미지의 base64 데이터를 가져옴
- `<img src="data:image/png;base64,{b64}" />` 형태로 삽입
- SVG viewBox/gradient 하드코딩 대신 실제 이미지 사용
**검증:** filled HTML에 `<img src="data:image/png;base64,` 패턴이 있고, SVG 태그가 없음

### W-2-7: filled/assembled 통일

**현재:** `_gen_stage_1_8_filled()`가 `assemble_slide_html()` 호출
**방향:** assembled(stage_2_code_assembled)도 같은 함수 호출하도록 `assemble_stage2.py` 수정 또는 제거
**검증:** filled와 assembled가 같은 HTML 구조를 가짐 (diff로 확인)

### W-2-8: 팝업 마크다운 테이블 변환

**현재:** `mdx_normalizer.py`의 `_extract_popup()`에서 `_convert_md_table_to_html()` 호출
**방향:** 현재 코드 유지
**검증:** `stage_0_context.json`의 popups에서 "DX와 BIM의 구분" 팝업에 `<table>` 태그가 있고 `|` 마크다운이 없음

---

## W-3: filled → Selenium 측정 연결

### W-3-1: .slide 클래스

**현재:** `assemble_slide_html()`의 최외곽 div에 `class="slide"` 있음
**방향:** 현재 코드 유지
**검증:** filled HTML에서 `class="slide"` 존재 확인

### W-3-2: area-* 클래스

**현재:** 각 역할 컨테이너에 `area-body`, `area-sidebar`, `area-footer` 클래스 있음
**방향:** 현재 코드 유지
**검증:** filled HTML에서 `area-body`, `area-sidebar`, `area-footer` 존재 확인

### W-3-3: Selenium 측정 정상 동작

**현재:** 173113 run에서 `{'error': 'slide not found'}` 발생 (당시 .slide 클래스 없었음)
**방향:** W-3-1, W-3-2가 해결되면 자동 해결
**방법:** filled HTML을 `measure_rendered_heights()`에 넣고 정상 결과 반환 확인
**검증:** 반환값에 `zones.sidebar.scrollHeight`, `zones.body.scrollHeight` 등이 있고 `error` 키가 없음

### W-3-4: 시각화 순서 (before → filled → after)

**현재:** `step_visualizer.py`의 dispatch에서 blocks → filled → fit_before → fit_after 순서
**방향:** before(빈 컨테이너 크기) → filled(블록+텍스트 채운 상태) → after(조정된 크기) 순서
**파일:** `src/step_visualizer.py` — `generate_step_html()`
**방법:**
- `stage_1_8` dispatch 순서를 `fit_before → filled → fit_after`로 변경
- fit_before는 빈 컨테이너 크기만 보여줌 (부족/여유 판단 없이)
- filled는 블록+텍스트 채운 상태
- fit_after는 조정 후 컨테이너 크기
**검증:** steps 폴더에 3개 파일이 순서대로 있고, before의 크기 → filled의 넘침 → after의 변경이 시각적으로 확인 가능

---

## W-4: 측정 결과 기반 조정 판단

### W-4-1: sidebar overflow → 확장

**현재:** pipeline.py Stage 1.8에 sidebar 확장 코드 있음
**방향:** 현재 코드 유지 (Selenium 측정이 동작하면 자동으로 발동)
**검증:** sidebar scrollHeight > clientHeight일 때 `stage_1_8_context.json`의 첨부 height_px가 scrollHeight 이상으로 증가

### W-4-2: body overflow → 재배분

**현재:** `redistribute()` 함수가 body zone 내에서 배경↔본심 재배분
**방향:** 현재 코드 유지
**검증:** body overflow 시 배경 또는 본심의 height_px가 변경됨

### W-4-3: 재배분 후에도 overflow → Kei 에스컬레이션

**현재:** `needs_escalation=True`일 때 `call_kei_fit_escalation()` 호출
**방향:** 현재 코드 유지
**검증:** `enhancement_result.kei_decisions`에 Kei 응답이 저장됨

### W-4-4: Kei trim/popup 결정 실제 적용

**현재:** Kei 결정을 받지만 실제 반영 안 됨
**방향:** 새로 구현
**파일:** `src/pipeline.py` Stage 1.8 내부 + 새 함수

**trim 구현 방법:**
- Kei가 `{"action": "trim", "detail": "150자로 축약"}`을 반환하면
- 해당 role의 topic structured_text를 **Kei/Sonnet에게 축약 요청** (AI 판단 — 어떤 문장이 덜 중요한지는 AI만 알 수 있음)
- 프롬프트: "다음 텍스트를 N자 이내로 축약하라. 불릿 구조 유지. 핵심 85% 보존."
- 축약된 텍스트로 structured_text 교체
- 하드코딩 없음 — 어떤 콘텐츠든 AI가 판단
- **도구:** anthropic SDK (이미 있음), Kei API /api/direct

**popup 구현 방법:**
- Kei가 `{"action": "popup", "detail": "상세 정의를 팝업으로"}`를 반환하면
- 해당 role의 structured_text를 **Kei/Sonnet에게 분리 요청** ("요약 vs 상세" 판단)
- 프롬프트: "다음 콘텐츠를 슬라이드 요약(2-3줄)과 팝업 상세로 분리하라."
- 요약은 structured_text에, 상세는 별도 팝업 HTML로 저장
- 슬라이드에는 요약 + `[상세보기→]` 링크
- 하드코딩 없음 — 어떤 콘텐츠든 AI가 요약/상세를 판단
- **도구:** anthropic SDK, 팝업 HTML 템플릿 (pipeline.py Stage 5에 이미 있음)

**검증:** trim 후 structured_text 길이가 줄어들고, popup 후 팝업 HTML 파일이 생성됨

### W-4-5: Kei restructure → 컨테이너 직접 변경

**현재:** `redistribute()` 재실행만 됨
**방향:** Kei가 "본심에 363px 보장"하면 직접 height_px 변경
**파일:** `src/pipeline.py` Stage 1.8 내부
**방법:**
- Kei 결정에서 구체적 px 값을 파싱 (정규식으로 숫자 추출)
- 해당 role의 height_px를 직접 설정
- 다른 role에서 부족분을 **weight 역비례**로 차감 (중요도 낮은 곳에서 더 많이)
- 최소 높이(60px) 보장
- 총합이 전체 가용 초과하지 않도록 검증
- 하드코딩 없음 — 순수 산술, 어떤 role이든 동작
- **도구:** Python 산술 (외부 라이브러리 불필요)

**검증:** restructure 후 해당 role의 height_px가 Kei가 지정한 값으로 변경되고, 총합이 전체 가용 이하

### W-4-6: after 컨테이너 저장

**현재:** `stage_1_8_context.json`에 containers 저장됨
**방향:** 현재 코드 유지 (W-4-1~5의 결과가 containers에 반영되면 자동 저장)
**검증:** `stage_1_8_context.json`의 containers가 before와 다름

### W-4-7: Kei 보강 검토 호출

**현재:** `call_kei_enhancement_review()` 함수 있고 pipeline.py에서 호출
**방향:** 현재 코드 유지
**검증:** `enhancement_result`에 Kei 보강 검토 결과가 저장됨 (approve/modify/reject)

---

## W-5: after 기반 최종 조립 + 검증

### W-5-1: stage_2가 after 컨테이너 사용

**현재:** stage_2_context.json의 containers == stage_1_8_context.json의 containers (확인됨)
**방향:** 현재 코드 유지
**검증:** 두 JSON의 containers 비교 — 일치

### W-5-2: overflow 없음 확인

**현재:** Stage 4에서 Selenium 측정. Vision 모델 ID 404 에러
**방향:** Vision 모델 ID를 `claude-sonnet-4-20250514`로 변경 (vision 지원, 비용 효율)
**파일:** `src/kei_client.py` — 3곳
**방법:** 모델 ID 문자열 교체
**검증:** Stage 4에서 모든 zone의 excess_px ≤ 0

### W-5-3: 텍스트 85% 보존 검증

**현재:** 검증 로직 없음
**방향:** 새로 구현
**파일:** `src/pipeline.py` Stage 4 또는 Stage 5
**방법:**
- final.html에서 HTML 태그 제거하여 순수 텍스트 추출 (Python stdlib `html.parser`)
- 각 role의 structured_text와 문자 3-gram 겹침 비교
- 85% 이상이면 PASS
- 하드코딩 없음 — 문자열 비교만, 어떤 콘텐츠든 동작
- **도구:** Python stdlib만 (html.parser, re). 외부 NLP 불필요

**검증:** 검증 함수가 각 role별 보존율을 반환하고, 모든 role이 85% 이상

---

## 의존 관계

```
W-1-1 → W-1-2 (자동)
W-1 + W-2 → W-3 (filled 생성 + 측정)
W-3 → W-4 (측정 결과로 판단)
W-4 → W-5 (after 기반 최종)

W-2 내부: 1~8 독립적으로 병행 가능
W-4 내부: 1→2→3→4/5 순차, 6/7 독립
```

---

## 필요 도구/라이브러리

| 도구 | 용도 | 상태 |
|------|------|------|
| Selenium + Chrome headless | filled 측정 (W-3) | ✅ 설치됨, 동작 확인 |
| anthropic SDK | Kei trim/popup (W-4-4), Vision (W-5-2) | ✅ 설치됨 |
| httpx | Kei API 호출 | ✅ 설치됨 |
| Kei API (localhost:8000) | 에스컬레이션, 보강 검토 | ✅ 동작 확인 |
| Python stdlib (html.parser, re) | 텍스트 보존 검증 (W-5-3) | ✅ 내장 |
| Jinja2 | 블록 템플릿 렌더링 | ✅ 설치됨 |

**추가 설치 필요 없음.**

---

## 실행 순서

```
Phase 1: W-1 (weight 비율) — 기반
Phase 2: W-2 (공통 조립 함수) — W-1과 병행 가능
Phase 3: W-3 (Selenium 연결) — W-1 + W-2 필요
Phase 4: W-4 (판단 로직) — W-3 필요
Phase 5: W-5 (최종 검증) — W-4 필요

각 Phase 완료 후 파이프라인 실행하여 검증.
```
