# 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 데이터를 가져옴 - `` 형태로 삽입 - SVG viewBox/gradient 하드코딩 대신 실제 이미지 사용 **검증:** filled HTML에 `` 태그가 있고 `|` 마크다운이 없음 --- ## 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 완료 후 파이프라인 실행하여 검증. ```