# 파이프라인 프로세스 재검토 — 검증 시점 문제 진단

> Phase I 실행 완료 후 실제 구동 중 발견된 프로세스 구조 문제.
> Phase I의 코드 변경(14개 항목)은 유효하나, **검증이 배치된 시점**이 부적절.

---

## 현재 프로세스 흐름 (as-is)

```
[1단계] Kei 실장 — 콘텐츠 분석 + 스토리라인 설계
  ├ 1-A: 꼭지 추출 (Kei API)
  ├ 1-B: 컨셉 구체화 (Kei API)
  ├ 제목 중복 검증 (코드)
  └ 이미지 크기 측정 (Pillow)
      ↓
[2단계] 디자인 팀장 — 레이아웃 + 블록 매핑
  ├ Step A: 프리셋 선택 (규칙 기반)
  ├ Step A-2: Opus 블록 추천 (Kei API)
  ├ Step B: Sonnet 블록 매핑
  └ 블록 검증 (코드): 미등록 교체, zone 교정, pill-pair, 높이 예산 체크
      ↓
[2.5단계] ⚠️ Kei 넘침 판단 — 예상 높이 기반
      ↓
[3단계] Kei 편집자 — 텍스트 채움 (Kei API)
      ↓
[4단계] 디자인 실무자 — CSS 조정 + HTML 렌더링 (Sonnet + Jinja2)
      ↓
[5단계] 디자인 팀장 — 재검토 + 조정 루프 (Sonnet, 최대 2회)
      ↓
미리보기 + HTML 다운로드
```

---

## 각 시점에서 알 수 있는 정보

| 시점 | 원본 텍스트 | 꼭지 분석 | 블록 배치 | 실제 텍스트 | 렌더링 HTML | 실제 높이 |
|------|:---------:|:--------:|:--------:|:---------:|:----------:|:--------:|
| 1단계 후 | O | O | - | - | - | - |
| 2단계 후 | O | O | O | - | - | 예상만 |
| 2.5단계 | O | O | O | **없음** | **없음** | 예상만 |
| 3단계 후 | O | O | O | **O** | - | - |
| 4단계 후 | O | O | O | O | **O** | 측정 가능 |
| 5단계 | O | O | O | O | O | 측정 가능 |

---

## 문제 진단 (6건)

### 문제 1: 내용 없이 넘침 판단

**위치:** Stage 2.5
**현상:** Kei에게 "이 zone이 넘친다"고 전달하지만, 실제 텍스트가 없는 상태. 블록 타입의 예상 높이(medium=150px, large=250px)만으로 판단 요청.
**문제:** Kei가 "trim할까 restructure할까"를 결정하려면 실제 콘텐츠를 봐야 하는데 볼 수 없음. 판단 근거가 부족한 상태에서 판단을 요청.

---

### 문제 2: 예상 높이 초과 → 판단 주체 잘못됨

**위치:** Stage 2.5
**현상:** Sonnet에게 이미 "zone 예산 490px, height_cost 확인해서 초과하지 마라"고 프롬프트로 지시함. 그런데도 예상 높이가 초과하면 그건 **Sonnet이 지시를 안 따른 것**.
**문제:** Sonnet의 지시 불이행을 Kei에게 물어볼 문제가 아님. Sonnet을 다시 호출하거나 프롬프트를 개선할 문제. 판단 주체와 해결 주체가 불일치.

---

### 문제 3: 실제 HTML이 있는데 넘침을 안 봄

**위치:** Stage 5
**현상:** 렌더링된 HTML이 있고, 각 블록의 실제 텍스트 양도 알 수 있는 시점. 그러나 현재 Stage 5의 점검 항목은 "빈 블록, 채움 불균형, 정보량, HTML 구조"만.
**문제:** 정작 "컨테이너에 실제로 넘치는가"는 점검 항목에 없음. 넘침을 확인할 수 있는 최적의 시점에서 확인하지 않음.

---

### 문제 4: 넘침 판단에 Kei가 없음

**위치:** Stage 5
**현상:** Stage 5 재검토는 Sonnet이 단독으로 수행. 조정도 expand/shrink/rewrite를 Sonnet이 결정.
**문제:** 넘침 발생 시 "뭘 줄이고 뭘 팝업으로 분리할지"는 **콘텐츠 중요도 판단** — Kei가 해야 할 일. 현재 Stage 5에 Kei 참여 경로가 없음.

---

### 문제 5: 실제 렌더링 높이 측정 수단 없음

**위치:** 전체 파이프라인
**현상:** 파이프라인 어디에서도 렌더링된 HTML의 실제 px 높이를 측정하지 않음.
- Stage 2: 블록 타입 기반 예상 높이 (HEIGHT_COST_PX: compact=70, medium=150, large=250, xlarge=400)
- Stage 5: Sonnet이 HTML 코드를 읽고 눈대중으로 판단
**문제:** 예상 높이와 실제 높이는 다를 수 있음. 텍스트 양, CSS 조정, 폰트 크기에 따라 실제 높이가 달라지는데 이를 측정하는 코드가 없음.

---

### 문제 6: 넘침이 재검토 루프에 포함 안 됨

**위치:** Stage 5 루프
**현상:** Stage 5는 `재검토 → 조정 → fill_content(Stage 3) → render(Stage 4) → 재검토` 루프가 있음 (최대 2회).
**문제:** 이 루프 안에 넘침 판단이 없음. 조정 후에도 여전히 넘칠 수 있는데, expand 조정으로 텍스트가 늘어나서 오히려 더 넘칠 수도 있음. 루프가 넘침을 감지하지 못함.

---

## 문제 요약 매트릭스

| # | 문제 | 위치 | 핵심 원인 | 영향 |
|---|------|------|----------|------|
| 1 | 내용 없이 넘침 판단 | 2.5 | 텍스트 채움 전에 판단 | Kei 판단 근거 부족 → 부정확한 결정 |
| 2 | 예상 높이 초과 → Kei에게 물음 | 2.5 | 판단 주체 잘못됨 | Sonnet 지시 불이행을 Kei가 해결할 수 없음 |
| 3 | HTML 있는데 넘침 안 봄 | 5 | 점검 항목 누락 | 실제 넘침 감지 못함 |
| 4 | 넘침 판단에 Kei 없음 | 5 | Sonnet만 참여 | 콘텐츠 중요도 무시한 조정 |
| 5 | 실제 높이 측정 없음 | 전체 | 측정 수단 부재 | 예상과 실제의 차이 감지 불가 |
| 6 | 넘침이 루프에 없음 | 5 루프 | 넘침 체크 미포함 | 조정 후 넘침 악화 가능 |

---

## 원인 관계

```
근본 원인: Stage 2.5의 넘침 판단 위치가 기존 DOWNGRADE_MAP 위치를 그대로 따름
    ↓
메커니즘만 변경(DOWNGRADE → Kei), 시점은 재검토 안 함
    ↓
내용 없이 판단(문제 1) + 주체 잘못됨(문제 2)
    ↓
실제 넘침이 감지되는 시점(Stage 4 이후)에는 검증 없음(문제 3, 4, 6)
    ↓
애초에 실제 높이 측정 수단도 없음(문제 5)
```

---

## 해결 방안 조사 결과

### 방안 1: 실제 렌더링 높이 측정 (문제 5 해결)

현재 파이프라인에는 렌더링된 HTML의 실제 px 높이를 측정하는 수단이 없음.

| 도구 | 정확도 | 속도 | CSS Grid | CSS 변수 | 커스텀 폰트 | 설치 상태 |
|------|--------|------|----------|----------|------------|----------|
| **Playwright** | 픽셀 정확 | 20~50ms/요소 | 완전 지원 | 완전 지원 | 완전 지원 | 미설치 |
| Selenium | 픽셀 정확 | 50~150ms/요소 | 완전 지원 | 완전 지원 | 완전 지원 | **설치됨** (4.34.0) |
| WeasyPrint | 제한적 | 200~500ms | 부분 지원 | 제한적 | 지원 | **설치됨** (65.1) |
| 텍스트 추정 | ±15~30% 오차 | <1ms | 불가 | 불가 | 불가 | — |

**권장: Playwright** — 가장 정확하고 빠름. 비동기 지원. headless Chromium 자동 설치.
**차선: Selenium** — 이미 설치됨. 동기식이라 약간 느리지만 충분히 사용 가능.

**측정 방식:**
```python
# Playwright 예시
async with async_playwright() as p:
    browser = await p.chromium.launch()
    page = await browser.new_page(viewport={"width": 1280, "height": 720})
    await page.set_content(html)

    # 각 zone의 실제 높이 측정
    body_box = await page.locator("[data-zone='body']").bounding_box()
    actual_height = body_box["height"]  # 실제 렌더링 px

    # overflow 감지: scrollHeight > clientHeight
    overflow = await page.evaluate("""
        el => el.scrollHeight > el.clientHeight
    """, await page.query_selector("[data-zone='body']"))
```

---

### 방안 2: Stage 2.5 → Stage 5로 이동 (문제 1, 2, 3, 4, 6 해결)

**현재:** Stage 2.5에서 텍스트 없이 Kei 판단 → 근거 부족
**개선:** Stage 4(렌더링) 이후, Stage 5(재검토) 안에서 넘침 판단

```
현재:
  Stage 2 → [2.5 Kei 넘침 판단] → Stage 3 → Stage 4 → Stage 5(Sonnet만)

개선:
  Stage 2 → Stage 3 → Stage 4 → Stage 5(Sonnet 감지 + Kei 판단)
```

**Stage 5 역할 확장:**
1. **Sonnet이 감지**: 렌더링된 HTML + zone 예산 정보를 보고 넘침 여부 판단
2. **넘침이면 Kei에게 전달**: 실제 콘텐츠가 있는 상태에서 Kei가 판단
3. **Kei가 결정**: trim(텍스트 축약) 또는 restructure(팝업 분리)
4. **Sonnet이 실행**: CSS 조정 + 재렌더링

**Sonnet + Kei 협업 모델:**
```
Sonnet: "body zone이 520px인데 예산 490px. 30px 초과."
    ↓
Kei: "꼭지 3의 부연 설명을 축약하면 됨. 핵심은 유지."  (trim)
  또는
Kei: "12행 비교표는 팝업으로 분리. 슬라이드엔 요약만."  (restructure)
    ↓
Sonnet: CSS 조정 + 재렌더링
```

---

### 방안 3: Stage 2 구조적 검증은 유지하되 역할 한정 (문제 2 해결)

Stage 2의 `_validate_height_budget()`는 **구조적 검증만** 담당:
- 금지 블록 교체 (BODY_FORBIDDEN_MAP) — 유지
- pill-pair 단독 금지 (I-7) — 유지
- 예상 높이 초과 — **경고만** (Kei 호출 안 함, Stage 5에서 처리)

```python
# Stage 2: 경고만 출력, overflow 정보는 Stage 5에서 활용
if total > budget:
    logger.warning(f"[예상 높이 초과] {area}: {total}px > {budget}px (Stage 5에서 검증)")
    # Kei 호출 안 함. 실제 렌더링 후 Stage 5에서 정확히 감지.
```

**Sonnet 프롬프트(STEP_B_PROMPT) 개선:**
- 현재: height_cost 매핑을 설명하지만 구체적 예시 없음
- 개선: 계산 예시 추가 + "초과 시 reason 필드에 설명" 명시

---

### 방안 4: 넘침을 Stage 5 재검토 루프에 통합 (문제 6 해결)

**현재 Stage 5 루프:**
```
재검토(Sonnet) → 조정(expand/shrink/rewrite) → 재편집(Kei 편집자) → 재렌더링 → 재검토
```

**개선 Stage 5 루프:**
```
재검토(Sonnet, 넘침 포함)
  → 넘침 있으면: Kei 판단(trim/restructure)
  → 조정 적용(expand/shrink/rewrite/trim/restructure)
  → 재편집(Kei 편집자) → 재렌더링 → 재검토
```

**Stage 5 프롬프트에 추가할 점검 항목:**
```
6. 높이 제약: 각 zone이 예산을 초과하는가?
   - 자동 조정(shrink)으로 해결 가능 → shrink
   - 불가능 → overflow_detected (Kei 판단 필요)
```

**_apply_adjustments()에 추가할 action:**
- `overflow_detected` → Kei API 호출 → trim/restructure 적용

---

## 해결 방안 매트릭스

| 방안 | 해결하는 문제 | 필요 기술 | 구현 난이도 |
|------|-------------|----------|------------|
| 1. 실제 높이 측정 | 문제 5 | Playwright 또는 Selenium | 중 (의존성 추가) |
| 2. 넘침 판단 Stage 5로 이동 | 문제 1, 2, 3, 4 | 코드 리팩토링 | 중 (Stage 2.5 제거, Stage 5 확장) |
| 3. Stage 2 경고만 | 문제 2 | 코드 수정 | 소 (Kei 호출 제거, 경고만) |
| 4. 넘침을 루프에 통합 | 문제 6 | Stage 5 프롬프트 + 코드 | 중 (새 action + Kei 연동) |

**방안 1은 선택적** — Playwright/Selenium 없이도 Sonnet이 HTML을 읽고 넘침을 추정할 수 있음. 정확도는 떨어지지만 현실적.
**방안 2+3+4는 필수** — 프로세스 구조 자체의 문제이므로 반드시 수정.

---

## 실행 계획: 프로세스 재설계 (방안 2+3+4)

> 충돌/회귀/오류 검토 완료. Phase I 산출물 전부 재사용. 변경 파일 `pipeline.py`만.
> Sonnet 신규 투입 0건. Kei API 호출 위치만 이동. 하드코딩/단발성 없음.

### 변경 전후 프로세스 비교

```
[변경 전]
  Stage 1 → Stage 2 → [2.5 Kei 넘침 판단 ⚠️] → Stage 3 → Stage 4 → Stage 5(Sonnet만)

[변경 후]
  Stage 1 → Stage 2(경고만) → Stage 3 → Stage 4 → Stage 5(Sonnet 감지 + Kei 판단)
```

### 변경 상세 (5건, pipeline.py만)

#### P-1: Stage 2.5 제거

**위치:** `pipeline.py` 91~136행 (46행)
**작업:** 전체 삭제
**영향:** 없음. overflow 키는 layout_concept에 남아 Stage 5에서 참고.

**Phase I 회귀 검토:**
- `call_kei_overflow_judgment()` — 함수 삭제 안 함. 호출 위치만 Stage 5로 이동.
- `_downgrade_fallback()` — 삭제 안 함. Stage 5에서 비상용.
- `KEI_OVERFLOW_PROMPT` — 삭제 안 함. Stage 5에서 사용.

---

#### P-2: `_review_balance()` 시그니처 + 프롬프트 확장

**위치:** `pipeline.py` 297~363행
**작업:**
1. 시그니처: `(html, layout_concept, content)` → `(html, layout_concept, content, analysis)` 추가
2. 프롬프트에 zone 예산 정보 + overflow 힌트 추가
3. 점검 항목 6번 추가: "높이 초과 — overflow_detected"
4. 출력 format에 `overflow_detected` action 추가

**변경 내용:**
```python
# 시그니처 변경
async def _review_balance(
    html: str,
    layout_concept: dict[str, Any],
    content: str,
    analysis: dict[str, Any],  # 추가
) -> dict[str, Any] | None:

# 프롬프트 추가
# 1. zone 예산 정보 (select_preset + LAYOUT_PRESETS에서)
preset_name = select_preset(analysis)
preset = LAYOUT_PRESETS.get(preset_name, {})
zone_budget_lines = [
    f"- {name}: ~{z['budget_px']}px (너비 {z['width_pct']}%)"
    for name, z in preset.get("zones", {}).items()
]

# 2. Stage 2 예상 overflow 힌트 (있으면)
overflow_hint = layout_concept.get("overflow", [])

# 3. 점검 항목 6번
"6. 높이 초과: 각 zone의 블록+텍스트가 예산을 초과하는가?\n"
"   - shrink로 해결 가능 → shrink\n"
"   - 불가능 (콘텐츠가 본질적으로 큼) → overflow_detected\n"

# 4. action 추가
"- overflow_detected: 높이 초과로 Kei 판단 필요. 해당 zone과 초과 블록 명시.\n"
```

**충돌:** 기존 5개 점검 + 3개 action 변경 없음. 추가만.
**Sonnet 역할:** 넘침 **감지만**. 판단은 Kei.

---

#### P-3: Stage 5 루프에 Kei 넘침 판단 통합

**위치:** `pipeline.py` 155~180행
**작업:** 루프 내에서 overflow_detected 시 Kei 호출 추가

```python
for review_round in range(MAX_REVIEW_ROUNDS):
    review_result = await _review_balance(html, layout_concept, content, analysis)

    if not review_result or not review_result.get("needs_adjustment"):
        break

    # overflow_detected가 있으면 Kei에게 판단 요청
    overflow_adjs = [
        adj for adj in review_result.get("adjustments", [])
        if adj.get("action") == "overflow_detected"
    ]
    if overflow_adjs:
        # 실제 콘텐츠가 있는 상태에서 Kei 판단
        overflow_context = _build_overflow_context(layout_concept, overflow_adjs)
        kei_judgment = await call_kei_overflow_judgment(
            overflow_context, content, analysis
        )

        if kei_judgment is None:
            logger.warning("[DOWNGRADE 비상] Kei API 실패")
            for page in layout_concept.get("pages", []):
                _downgrade_fallback(page.get("blocks", []), overflow_context)
        else:
            # Kei 판단을 adjustments에 반영 (overflow_detected → kei_trim/restructure)
            _convert_kei_judgment(review_result, kei_judgment, analysis)

    # 모든 조정 적용 (기존 expand/shrink/rewrite + 신규 kei_trim)
    layout_concept = await _apply_adjustments(layout_concept, review_result, content)
    html = render_slide(layout_concept)
```

**호출되는 함수:** 모두 Phase I에서 만든 것 재사용
- `call_kei_overflow_judgment()` — kei_client.py (변경 없음, Kei API만 사용)
- `_downgrade_fallback()` — design_director.py (변경 없음)

**신규 헬퍼 함수 2개:**
- `_build_overflow_context()` — overflow_adjs + layout_concept에서 실제 블록 데이터 추출
- `_convert_kei_judgment()` — Kei의 trim/restructure 결정을 review_result.adjustments에 반영

---

#### P-4: `_apply_adjustments()` — kei_trim action 추가

**위치:** `pipeline.py` 366~410행
**작업:** 기존 elif 체인에 kei_trim 분기 추가

```python
# 기존 expand/shrink/rewrite 로직 변경 없음
# 아래 elif만 추가:

elif action == "kei_trim":
    max_chars = adj.get("max_chars", 200)
    if "char_guide" not in block:
        block["char_guide"] = {}
    for key in block.get("char_guide", {}):
        block["char_guide"][key] = min(block["char_guide"][key], max_chars)
    if not block["char_guide"]:
        block["char_guide"] = {"text": max_chars}
    logger.info(f"조정: {area} → kei_trim max_chars={max_chars}")

elif action == "kei_restructure":
    block["detail_target"] = True
    if "data" in block:
        del block["data"]
    block["reason"] = f"재구성: {adj.get('detail', 'Kei 판단 팝업 분리')}"
    logger.info(f"조정: {area} → kei_restructure (detail_target)")
```

**충돌:** 없음. 기존 3개 action 변경 0행. 새 elif 추가만.

---

#### P-5: 호출부 수정

**위치:** `pipeline.py` 156행
```python
# 현재:
review_result = await _review_balance(html, layout_concept, content)
# 변경:
review_result = await _review_balance(html, layout_concept, content, analysis)
```

**영향:** 이 함수의 호출부는 pipeline.py 156행 1곳만. 다른 파일에서 호출하지 않음.

---

### 변경 파일 총괄

| 파일 | 변경 | Phase I 코드 영향 |
|------|------|------------------|
| `pipeline.py` | Stage 2.5 제거 + Stage 5 확장 + 헬퍼 2개 + action 2개 | Phase I 함수 재사용, 삭제 0건 |
| `design_director.py` | **변경 없음** | — |
| `kei_client.py` | **변경 없음** | — |
| `content_editor.py` | **변경 없음** | — |
| `sse_utils.py` | **변경 없음** | — |

### 검증 매트릭스

| 항목 | 결과 |
|------|------|
| Phase I 회귀 | **없음** — I-1~I-14 전부 유지, 함수/상수 삭제 0건 |
| Kei API 사용 | **유지** — `call_kei_overflow_judgment()` 호출 위치만 Stage 5로 이동 |
| Sonnet이 Kei 역할 대체 | **없음** — Sonnet은 감지만, 판단은 Kei만 |
| 하드코딩 | **없음** — trim max_chars는 Kei가 결정 |
| 단발성 수정 | **없음** — 범용 구조 (어떤 overflow에도 동작) |
| 기존 코드 충돌 | **없음** — overflow 키가 중간 단계에서 무시되는 것 확인 |
| DOWNGRADE 비상용 | **유지** — Stage 5에서 Kei 실패 시 동일하게 작동 |

### 실행 순서

1. P-1: Stage 2.5 제거 (pipeline.py 91~136행 삭제)
2. P-2: `_review_balance()` 시그니처 + 프롬프트 확장
3. P-3: Stage 5 루프에 Kei 연동 + 헬퍼 함수 2개
4. P-4: `_apply_adjustments()` kei_trim/kei_restructure action 추가
5. P-5: 호출부 `analysis` 파라미터 추가

---

## 이력

| 날짜 | 내용 |
|------|------|
| 2026-03-26 | Phase I 실행 완료 후 프로세스 검증 중 발견. 6개 문제 진단. |
| 2026-03-26 | 해결 방안 4개 조사. Playwright 높이 측정 + Stage 5 넘침 통합 방향 도출. |
| 2026-03-26 | **실행 계획 확정.** 충돌/회귀/오류 검토 완료. P-1~P-5 5건, pipeline.py만 변경. Phase I 산출물 전부 재사용. |
