# 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 후처리 (코드 강제)**: ```python # 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 예시: ```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행:** ```python # Phase L: 블록별 max-height 제약 max_height = block.get("_max_height_px") if max_height: rendered_html = ( f'
' f'{rendered_html}
' ) ``` 이게 하는 일: 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 래퍼 제거 ```python # 229-235행 삭제. 아래 코드 완전 제거: max_height = block.get("_max_height_px") if max_height: rendered_html = ( f'
' f'{rendered_html}
' ) ``` max-height 없이 렌더링 → overflow가 생기면 `scrollHeight > clientHeight`로 정확히 감지됨. ### 변경 2: content_editor.py 프롬프트에 _max_chars 강제 명시 현재 `EDITOR_PROMPT`의 purpose별 분량 원칙이 "가이드라인" 수준. `_max_chars`가 계산되어 있지만 편집자에게 전달이 안 되고 있음. ```python # 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이 없어지면, 기존 측정 스크립트가 정상 작동: ```javascript // 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행:** ```python 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 ```python 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 이미지 전달 형식 ```python 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 측):** ```python # 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_SCRIPT`의 `block.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줄) |