Files
C.E.L_Slide_test2/IMPROVEMENT-PHASE-N.md
kyeongmin b0bcffc0f6 Phase N+O: 컨테이너 기반 레이아웃 + Step B 제거 + 전면 정리
- Phase N: catalog 개선, fallback 전면 제거, Kei API 무한 재시도, topic_id 버그 수정
- Phase O: 컨테이너 스펙 계산(비중→px), 블록 스펙 확정, 렌더러 container div
- Step B(Sonnet) 제거: Kei(A-2)+코드로 대체. STEP_B_PROMPT/fallback/DOWNGRADE_MAP 삭제
- Selenium: container div 감지 추가
- catalog.yaml: ref_chars 구조 변환 + FAISS 재빌드
- 문서 전면 갱신: README, PROGRESS, IMPROVEMENT, Phase I~O md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 15:20:51 +09:00

24 KiB

Phase N: 4대 핵심 문제 진단 + 해결 방안

작성일: 2026-03-27 상태: 완료 — catalog 개선, fallback 전면 제거, topic_id 버그 수정, 무한 재시도 체계 구축


오답 노트 (절대 반복 금지)

아래는 이미 실패가 증명된 접근법이다. 어떤 상황에서도 다시 사용하지 않는다.

# 실패 패턴 왜 실패했나 교훈
X-1 Sonnet에게 블록 선택을 맡김 Kei 추천을 무시하고 자기 맘대로 바꿈. 프롬프트로 제어 불가 블록 선택은 Kei 권한. 코드 레벨 강제.
X-2 Sonnet fallback (Kei 실패 시 Sonnet 대체) Sonnet이 대체해봤자 품질이 안 나옴. 결과물이 무의미 Kei API는 필수 인프라. 실패 시 파이프라인 중단. fallback 자체가 없음.
X-3 max-height + overflow:hidden으로 CSS 사후 자르기 텍스트가 잘리는데 측정기가 "정상"이라고 판단. 근본적 결함 콘텐츠는 렌더링 전에 맞춰야 함. CSS로 사후에 자르지 않음.
X-4 HTML 텍스트를 읽고 시각 검수 Kei가 HTML 소스를 읽어봤자 렌더링 결과를 알 수 없음. 10분 낭비 시각 검수는 스크린샷(이미지)으로.
X-5 "안전망/fallback"이라는 명목으로 실패 패턴 재도입 실패한 방법을 "비상용"이라고 다시 넣으면 결국 그게 돌아감 실패한 것은 비상용으로도 안 됨. 오답 노트에 기록하고 근절.
X-6 프롬프트만으로 LLM 행동 강제 "반드시 존중하라"고 써도 LLM은 안 지킴 강제는 코드로. 프롬프트는 가이드일 뿐.

문제 전체 요약

# 문제 원인 위치 심각도
N-1 블록 선택이 콘텐츠 전달 방식과 안 맞음 design_director.py Step B 치명
N-2 사이드바에 섹션 제목이 없음 kei_client.py + renderer.py 중간
N-3 max-height CSS가 콘텐츠를 잘라먹음 renderer.py 229-235행 치명
N-4 Stage 5가 HTML 텍스트를 읽어서 무용지물 kei_client.py + pipeline.py 치명

N-1. 블록 선택이 콘텐츠 전달 방식과 안 맞음

현상

  • Kei 실장(Opus)이 1단계에서 expression_hint, relation_type을 판단함
  • 2단계 Step A-2에서 Kei가 블록을 추천함 (_opus_block_recommendation())
  • 그런데 Step B에서 Sonnet이 Kei 추천을 무시하고 자기 맘대로 블록을 바꿈
  • 프롬프트에 "Opus 추천 존중" 규칙을 넣어도 Sonnet이 안 지킴

원인 (코드 레벨)

design_director.py — Step B 흐름:

Step A: rule-based preset 선택 (sidebar-right 등)
Step A-2: Kei API로 블록 추천 받음 → opus_blocks[]
Step B: Sonnet이 zone 배치 + char_guide 결정
         ↑ 여기서 Sonnet이 블록 타입을 바꿔버림

STEP_B_PROMPT에 "Opus가 추천한 블록을 존중하라"고 적어놨지만, 프롬프트는 강제가 아니다. Sonnet은 "더 적절하다"고 판단하면 얼마든지 다른 블록을 선택한다.

해결 방안: Kei가 블록을 결정, Sonnet은 zone + char_guide만

핵심 원칙: 블록 선택 = Kei 권한. 코드 레벨 강제. 프롬프트 의존 안 함.

변경 대상: design_director.py

현재 흐름:
  Step A: preset 선택
  Step A-2: Kei 블록 추천 (참고용)
  Step B: Sonnet이 블록 + zone + char_guide 전부 결정

변경 후:
  Step A: preset 선택
  Step A-2: Kei가 블록 확정 (topic_id → block_type 매핑)
  Step B: Sonnet은 zone 배치 + char_guide만 결정 (block_type 변경 금지)

구체적 변경:

  1. Step A-2 (_opus_block_recommendation): Kei API 응답에서 받은 블록을 "추천"이 아닌 "확정"으로 처리

    • 반환값: {topic_id: block_type} 딕셔너리
    • 이 딕셔너리를 Step B에 읽기 전용으로 전달
  2. Step B 프롬프트 변경: STEP_B_PROMPT에서 블록 선택 지시 제거

    • "각 꼭지에 맞는 블록을 선택하라" → 삭제
    • "아래 확정된 블록의 zone 배치와 글자 수 가이드만 결정하라"로 변경
  3. Step B 후처리 (코드 강제):

    # Sonnet 응답 후, 블록 타입을 Kei 확정값으로 덮어쓰기
    for block in sonnet_blocks:
        tid = block.get("topic_id")
        if tid in kei_confirmed_blocks:
            block["type"] = kei_confirmed_blocks[tid]  # 코드 레벨 강제
    
    • Sonnet이 어떤 블록을 응답하든, topic_id에 매칭되는 Kei 확정 블록으로 강제 교체
    • Sonnet의 zone, char_guide, reason만 살림
  4. Kei API는 필수 의존성: 실패 시 fallback 없음. 파이프라인 중단 + 에러 반환.

    • Kei API(localhost:8000)는 항상 떠 있어야 하는 로컬 인프라
    • 안 되면 그건 버그. 대체 경로가 아니라 수정 대상.

사용 기술:

  • 기존 Kei API (_opus_block_recommendation) — 이미 존재
  • Python dict 매핑으로 코드 레벨 강제 — 새 도구 불필요
  • STEP_B_PROMPT 프롬프트 축소 — zone + char_guide만

N-2. 사이드바에 섹션 제목이 없음

현상

  • 사이드바에 "용어 정의" 같은 콘텐츠가 배치되는데
  • 그게 뭔지 알려주는 섹션 제목이 없음
  • 독자가 사이드바가 무엇인지 맥락을 모름

원인 (코드 레벨)

  1. Kei 1단계 (KEI_PROMPT): role: "reference" + purpose: "용어정의"는 출력하지만, section_title 필드가 없음
  2. design_director.py Step B: sidebar zone에 블록을 배치할 때 섹션 제목 블록을 안 넣음
  3. renderer.py: area div를 렌더할 때 영역 라벨 없이 바로 블록 HTML만 출력

해결 방안: Kei가 section_title 판단 + 렌더러가 표시

변경 대상: kei_client.py, design_director.py, renderer.py

  1. Stage 1 Kei 프롬프트 (KEI_PROMPT) 확장:

    • 기존 topic 필드에 section_title 추가
    • role: "reference"인 꼭지에 Kei가 "용어 정의", "참고 자료" 등 섹션 제목을 부여
    • 출력 JSON 예시:
      {"id": 4, "title": "용어 혼용 정리", "purpose": "용어정의",
       "role": "reference", "section_title": "용어 정의"}
      
  2. Step B 블록 배치에 section label 블록 자동 삽입:

    • sidebar zone에 reference 블록이 배치될 때
    • 해당 topic의 section_title이 있으면 → topic-center 또는 divider-text 블록을 자동 삽입
    • 이것은 코드 레벨 (Sonnet 판단 아님)
  3. renderer.py _group_blocks_by_area()에서 sidebar 처리:

    • sidebar area 그룹에 section label이 있으면 최상단에 배치
    • CSS: 작은 글씨 + 볼드 + 하단 구분선

사용 기술:

  • KEI_PROMPT JSON 스키마 확장 (section_title 필드 1개)
  • 기존 블록 (divider-text 또는 topic-center) 재활용
  • renderer.py 코드 로직으로 자동 삽입

N-3. max-height CSS가 콘텐츠를 잘라먹음

현상

  • 렌더된 HTML에서 텍스트가 중간에 뚝 잘려 보임
  • Selenium으로 측정하면 "overflow 없음"이라고 나옴 → 실제로는 잘리고 있는데 감지 못함
  • 결과: Phase L 피드백 루프가 "정상"으로 판단하고 넘어감 → 잘린 채로 최종 출력

원인 (코드 레벨)

renderer.py 229-235행:

# Phase L: 블록별 max-height 제약
max_height = block.get("_max_height_px")
if max_height:
    rendered_html = (
        f'<div style="max-height:{max_height}px; overflow:hidden;">'
        f'{rendered_html}</div>'
    )

이게 하는 일:

  1. 블록에 max-height: Npx; overflow: hidden CSS를 씌움
  2. → 콘텐츠가 N px을 넘으면 시각적으로 잘림
  3. overflow: hidden이므로 scrollHeight === clientHeight측정기가 "overflow 없음"으로 판단
  4. → 피드백 루프가 작동 안 함 → 잘린 채 확정

근본 원인: 텍스트가 공간에 맞는지를 CSS로 사후에 자르는 게 아니라, 편집 단계에서 글자 수를 맞춰야 한다.

해결 방안: max-height 제거 + 편집자에게 _max_chars 강제 전달

핵심 원칙:

  • 콘텐츠가 렌더링 전에 공간에 맞아야 한다 (fit before render)
  • CSS로 사후에 자르지 않는다
  • overflow는 측정으로 감지하고, 감지되면 편집자를 다시 호출한다

변경 대상: renderer.py, content_editor.py, slide_measurer.py

변경 1: renderer.py에서 max-height 래퍼 제거

# 229-235행 삭제. 아래 코드 완전 제거:
max_height = block.get("_max_height_px")
if max_height:
    rendered_html = (
        f'<div style="max-height:{max_height}px; overflow:hidden;">'
        f'{rendered_html}</div>'
    )

max-height 없이 렌더링 → overflow가 생기면 scrollHeight > clientHeight로 정확히 감지됨.

변경 2: content_editor.py 프롬프트에 _max_chars 강제 명시

현재 EDITOR_PROMPT의 purpose별 분량 원칙이 "가이드라인" 수준. _max_chars가 계산되어 있지만 편집자에게 전달이 안 되고 있음.

# fill_content()에서 각 블록의 _max_chars를 프롬프트에 명시
req_text += f"\n  **최대 글자 수 (절대 제한): {block.get('_max_chars', '없음')}자**"
req_text += f"\n  이 글자 수를 넘기면 슬라이드에서 잘린다. 반드시 지켜라."

변경 3: slide_measurer.py의 overflow 감지 정상화

max-height + overflow:hidden이 없어지면, 기존 측정 스크립트가 정상 작동:

// scrollHeight > clientHeight → 정확한 overflow 감지
overflowed: zone.scrollHeight > zone.clientHeight + 2

현재 _MEASURE_SCRIPT는 이미 이 로직을 갖고 있음. max-height만 제거하면 됨.

추가: overflow:visible 확인

  • CSS에서 zone/block 컨테이너에 overflow: hidden이 없는지 확인
  • base.css에 혹시 hidden이 있으면 제거
  • 기본값 overflow: visible이면 scrollHeight 측정이 정확

변경 4: Phase L 피드백 루프 강화

현재 pipeline.py 215-275행의 피드백 루프:

  1. 측정 → overflow 감지 → char_guide 축소 → 편집자 재호출 → 재렌더링
  2. 최대 3회 반복

수정사항:

  • char_guide 축소 대신 _max_chars 직접 축소 (더 정확)
  • 축소량: calculate_trim_chars(excess_px) 결과를 _max_chars에서 차감
  • 편집자 재호출 시 축소된 _max_chars를 프롬프트에 명시

사용 기술:

  • 기존 Selenium + scrollHeight > clientHeight — 이미 존재, max-height만 제거하면 작동
  • 기존 calculate_max_chars(), calculate_trim_chars() — 이미 존재
  • content_editor.py 프롬프트 확장 — _max_chars 전달만 추가
  • 새 도구 불필요

N-4. Stage 5가 HTML 텍스트를 읽어서 무용지물

현상

  • Kei 실장이 최종 검수 (Stage 5)에서 10분 걸리는데 아무것도 안 바뀜
  • 이유: Kei가 HTML 소스 텍스트를 읽고 검수함
  • HTML 태그 사이에서 실제 렌더링 결과를 상상해야 함 → 불가능
  • "텍스트가 잘리는지", "비중이 맞는지", "가독성이 괜찮은지" → HTML 텍스트로는 판단 불가

원인 (코드 레벨)

kei_client.py call_kei_final_review() 306-313행:

prompt = (
    KEI_REVIEW_PROMPT + "\n\n"
    f"## 핵심 메시지\n{core_message}\n\n"
    ...
    f"\n\n## 조립 HTML (요약)\n{html[:3000]}\n\n"  # ← HTML 소스 텍스트 3000자
    f"위 결과물을 검수하고 조정이 필요한지 판단해. JSON만."
)

Kei(Opus)는 멀티모달 모델이라 이미지를 볼 수 있는데, 현재는 텍스트만 전달.

해결 방안: Selenium 스크린샷 → Kei API에 이미지 전달

핵심 원칙:

  • Stage 5에서 Kei가 실제 렌더링된 슬라이드 스크린샷을 보고 검수
  • HTML 텍스트 읽기 → 이미지 보기로 전환
  • overflow 없으면 Stage 5 건너뜀 (시간 절약)
  • 최대 1회만 (현재 2회 → 1회)

기술 조사 결과

Selenium 스크린샷 → base64

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1280,720")
options.add_argument("--force-device-scale-factor=1")

driver = webdriver.Chrome(options=options)
driver.get(f"data:text/html;charset=utf-8,{encoded_html}")

# 슬라이드 요소만 정확히 캡처
slide = driver.find_element(By.CSS_SELECTOR, ".slide")
screenshot_b64 = slide.screenshot_as_base64  # str, 순수 base64
driver.quit()

API 출처: Selenium 4.x WebElement.screenshot_as_base64 프로퍼티

  • 반환: str (순수 base64, data URI prefix 없음)
  • 형식: PNG
  • 해당 요소의 bounding box만 캡처 (전체 페이지가 아님)

Anthropic Claude API 이미지 전달 형식

import anthropic

client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
response = await client.messages.create(
    model="claude-opus-4-0-20250514",  # Opus = 멀티모달 지원
    max_tokens=4096,
    messages=[{
        "role": "user",
        "content": [
            {
                "type": "image",
                "source": {
                    "type": "base64",
                    "media_type": "image/png",
                    "data": screenshot_b64,  # 순수 base64 문자열
                },
            },
            {
                "type": "text",
                "text": "이 슬라이드를 검수해줘. ...",
            },
        ],
    }],
)

API 출처: Anthropic 공식 Vision 문서

  • 지원 모델: Claude Opus 4, Sonnet 4, Haiku 3.5 전부 멀티모달 지원
  • 지원 포맷: PNG, JPEG, GIF, WebP
  • 이미지 크기 제한: 최대 8000x8000px, 5MB/장
  • 1280x720 슬라이드: ~1,229 토큰 (비용 미미)

문제: 현재 Kei API(/api/message)는 이미지 미지원

Kei persona_agent 조사 결과:

  • ChatRequest 모델: message: str (텍스트만)
  • 이미지 필드 없음
  • LLM 호출 시 messages를 {"role": "user", "content": str}로 전달

필요한 변경 (Kei persona_agent 측):

# ChatRequest 확장 (persona_agent/backend/main.py)
class ChatRequest(BaseModel):
    session_id: str | None = None
    message: str
    image_data: str | None = None        # base64 이미지 (선택)
    image_media_type: str | None = None  # "image/png" 등 (선택)
  • 4개 파일, ~50줄 변경
  • 기존 텍스트 요청은 깨지지 않음 (image 필드는 optional)
  • Anthropic SDK는 이미 이미지 content block 지원 → 그대로 전달만 하면 됨

전체 Stage 5 변경 흐름

현재:
  Phase L 측정 → Stage 5: Kei가 HTML 텍스트 3000자 읽기 → 조정

변경 후:
  Phase L 측정 → overflow 없으면 Stage 5 건너뜀 (시간 절약)
                → overflow 있으면:
                    1. Selenium으로 슬라이드 스크린샷 (base64 PNG)
                    2. 스크린샷 + 측정 데이터 → Kei API (이미지 포함)
                    3. Kei가 실제 렌더링 보고 판단 → 조정 지시
                    4. 최대 1회 (현재 2회에서 축소)

변경 대상:

  • kei_client.py: call_kei_final_review()에 이미지 전달 추가
  • pipeline.py: Stage 5에 스크린샷 촬영 + overflow 없으면 skip 로직
  • slide_measurer.py: 스크린샷 캡처 함수 추가 (capture_slide_screenshot())
  • Kei persona_agent: ChatRequest에 image 필드 추가 (4파일 ~50줄)

주의: Kei persona_agent 코드를 수정해야 함 → 사용자 승인 필요

대안: Kei API 변경 없이 Anthropic 직접 호출

Kei API 수정이 부담스러우면, Stage 5만 Anthropic API 직접 호출 가능:

  • anthropic.AsyncAnthropic으로 Opus 직접 호출
  • Kei 페르소나 시스템 프롬프트를 personas/kei.md에서 로드하여 system으로 전달
  • 단점: Kei의 RAG/세션 컨텍스트를 못 씀
  • 장점: persona_agent 수정 없음

실행 순서 (의존 관계)

N-3 (max-height 제거)     ← 가장 먼저. 다른 것과 독립.
  │
  ├→ N-1 (블록 선택 강제)  ← N-3과 독립. 병렬 가능.
  │
  ├→ N-2 (사이드바 제목)   ← N-1 완료 후 (블록 확정 후 제목 삽입)
  │
  └→ N-4 (스크린샷 검수)   ← N-3 완료 필수 (overflow 감지 정상화 후)

추천 순서:

  1. N-3 — max-height 제거 + _max_chars 편집자 전달 (즉시, 가장 급함)
  2. N-1 — 블록 선택 코드 강제 (N-3과 병렬 가능)
  3. N-2 — 사이드바 섹션 제목 (N-1 후)
  4. N-4 — 스크린샷 기반 검수 (N-3 후, Kei API 수정 필요)

충돌 / 회귀 / 오류 검토

검토 방법

  • 4개 변경의 모든 수정 대상 파일을 코드 레벨로 읽고 교차 검증
  • overflow: hidden 전수 조사 (.py, .css, .html 전체)
  • _max_height_px, _max_chars 참조 전수 조사
  • 각 변경 간 의존 관계 + 실행 순서에서의 충돌 가능성 점검

N-3 (max-height 제거) — 충돌 분석

overflow: hidden이 존재하는 3개 레이어:

위치 용도 건드리나
.slide (base.css:16) overflow: hidden 1280x720 프레임 바깥 차단 유지 (건드리지 않음)
.slide > div (base.css:76) overflow: visible area div (body, sidebar 등) 이미 visible. 변경 불필요
renderer.py:229-235 max-height:Npx; overflow:hidden 블록별 래퍼 이것만 제거

개별 블록 템플릿의 overflow: hidden (15개+):

  • card-image-3col.html, card-dark-overlay.html, venn-diagram.html
  • 이것은 이미지/카드의 border-radius 잘림용
  • 텍스트 clipping과 무관 → 건드리지 않음

Phase L 측정기 영향:

  • max-height 래퍼 제거 후, scrollHeight가 실제 콘텐츠 높이를 정확 반영
  • _MEASURE_SCRIPTblock.scrollHeight > block.clientHeight정상 작동
  • 이전에 false-negative(잘렸는데 감지 못함)이던 것이 정상 감지됨
  • Phase L 루프가 더 자주 트리거될 수 있음 → 의도한 동작 (잘리는 걸 고치는 것)
  • MAX_MEASURE_ROUNDS = 3이면 충분

회귀 위험: 없음. max-height 래퍼는 Phase L에서 추가된 것이고, 제거해도 기존 블록/CSS에 영향 없음.


N-1 (블록 선택 강제) — 충돌 분석

기존 Step B 후처리 체인 (design_director.py:819-850):

현재: Sonnet 응답 → 미등록 블록 거부 → area명 검증 → conclusion→footer 강제
추가: Sonnet 응답 → ★Kei 확정 블록 덮어쓰기 → 미등록 블록 거부 → area명 검증 → conclusion→footer 강제
시나리오 처리
Kei가 추천한 블록이 catalog에 없음 바로 다음 단계에서 미등록 검증 → PURPOSE_FALLBACK 교체
Kei가 추천한 블록이 sidebar 금지 _validate_height_budget()의 SIDEBAR_FORBIDDEN_BLOCKS 체크
Kei API 미응답 파이프라인 중단 + 에러 반환. fallback 없음. Kei API는 필수 인프라.

N-3과의 관계: 독립. N-1은 2단계, N-3은 4단계. 서로 다른 파이프라인 단계.

회귀 위험: 없음. 기존 검증 체인 위에 한 단계 추가할 뿐.


N-2 (사이드바 제목) — 충돌 분석

시나리오 위험 대응
label 블록 추가 → sidebar 높이 예산 초과 낮음 (label ~30px, 예산 490px) label 블록은 고정 30px, allocate 제외
N-1 미완료 상태에서 실행 Sonnet이 블록을 바꿔서 label 위치 엉뚱 실행 순서: N-1 먼저, N-2 나중
_group_blocks_by_area() 호환 flex-column 최상단에 자연 배치 호환 문제 없음

회귀 위험: 없음. 기존 로직에 label 삽입만 추가.


N-4 (스크린샷 검수) — 충돌 분석

시나리오 위험 대응
N-3 미완료 → overflow 감지 부정확 "overflow 없으면 skip" 판단이 틀림 실행 순서: N-3 먼저, N-4 나중
Selenium 인스턴스 충돌 Phase L에서 quit 후 Stage 5에서 새 생성 동시 사용 아님, 충돌 없음
Kei persona_agent 미수정 이미지 전달 불가 대안: Anthropic 직접 호출 (persona 프롬프트 파일에서 로드)
MAX_REVIEW_ROUNDS 2→1 축소 기존보다 조정 기회 줄어듦 스크린샷 기반이라 1회로 충분 (텍스트 기반이라 2회 필요했던 것)

N-4 선행 조건 결정 필요:

  • 옵션 A: Kei persona_agent 수정 (ChatRequest에 image 필드 추가, ~50줄)
    • 장점: Kei의 RAG + 세션 컨텍스트 활용 가능
    • 단점: persona_agent 코드 수정 필요
  • 옵션 B: Anthropic API 직접 호출 (persona_agent 수정 없이)
    • 장점: design_agent 내에서 완결
    • 단점: Kei의 RAG/세션 없음, 페르소나 프롬프트만 로드

회귀 위험: 없음. Stage 5가 기존에 거의 무의미했으므로 (아무것도 안 바뀜), 변경해도 기존 품질이 나빠질 수 없음.


상호 작용 매트릭스

N-1 N-2 N-3 N-4
N-1 N-2가 N-1에 의존 독립 독립
N-2 N-1 완료 후 실행 독립 독립
N-3 독립 독립 N-4가 N-3에 의존
N-4 독립 독립 N-3 완료 후 실행

충돌 가능 조합: 없음. 4개 변경이 모두 파이프라인의 서로 다른 단계를 수정하므로 교차 간섭 없음.


최종 실행 계획

실행 순서 (의존 관계 기반)

① N-3: max-height 래퍼 제거 + _max_chars 편집자 전달
   (독립, 즉시 실행 가능)

② N-1: 블록 선택 코드 강제
   (N-3과 독립, ①과 병렬 가능)

③ N-2: 사이드바 섹션 제목
   (②N-1 완료 후)

④ N-4: 스크린샷 기반 검수
   (①N-3 완료 후 + persona_agent 수정 또는 직접호출 결정 후)

각 항목별 변경 파일 + 예상 규모

항목 변경 파일 신규 코드 삭제 코드 프롬프트 변경
N-3 renderer.py, content_editor.py, pipeline.py ~10줄 ~7줄 EDITOR_PROMPT에 _max_chars 절대제한 추가
N-1 design_director.py ~15줄 (후처리 강제) ~0줄 STEP_B_PROMPT에서 블록선택 지시 제거
N-2 kei_client.py, design_director.py, renderer.py ~20줄 ~0줄 KEI_PROMPT에 section_title 필드 추가
N-4 slide_measurer.py, kei_client.py, pipeline.py + (persona_agent 4파일) ~80줄 ~10줄 KEI_REVIEW_PROMPT을 이미지 기반으로 변경

오류 처리 원칙

Kei API는 필수 인프라다. "실패하면 대체"가 아니라, 실패하면 파이프라인 중단이다.

시나리오 처리 이유
Kei API 미응답 (N-1, N-2, N-3, N-4 공통) 파이프라인 즉시 중단 + 에러 반환 Kei는 선택이 아닌 필수. 없으면 돌리면 안 됨
편집자(Kei)가 _max_chars 안 지킴 (N-3) Phase L 루프가 감지 → Kei 편집자 재호출 (최대 3회) 측정 기반 재시도
Selenium 스크린샷 실패 (N-4) Stage 5를 텍스트 기반으로 수행 (현재 방식) Selenium은 도구. 도구 실패 시 기존 방식 유지
sidebar label이 높이 초과 유발 (N-2) label을 고정 30px로 처리, allocate에서 제외 본문 블록 공간 유지

파일별 변경 범위 요약

파일 N-1 N-2 N-3 N-4
design_director.py Step B 프롬프트 축소 + 후처리 강제 sidebar label 삽입 - -
kei_client.py - KEI_PROMPT section_title 추가 - 이미지 전달 추가
content_editor.py - - _max_chars 프롬프트 전달 -
renderer.py - sidebar label 렌더 max-height 래퍼 삭제 -
pipeline.py - - Phase L 루프 _max_chars 축소 Stage 5 스크린샷 + skip 로직
slide_measurer.py - - - capture_slide_screenshot() 추가
space_allocator.py - - - -
Kei persona_agent - - - ChatRequest 이미지 확장 (~50줄)