- 루트의 IMPROVEMENT-PHASE-*.md, PHASE-*.md 등 45개 → docs/history/로 이동 - docs/block-tests/ 오래된 블록 테스트 HTML 삭제 (figma_to_html_agent로 대체) - docs/figma-analysis/, docs/figma-assets/, docs/figma-screenshots/ 정리 - docs/test-*.html 등 초기 테스트 파일 정리 - 참고 페이지/ 스크린샷 정리 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
566 lines
24 KiB
Markdown
566 lines
24 KiB
Markdown
# 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'<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 래퍼 제거
|
|
|
|
```python
|
|
# 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`가 계산되어 있지만 편집자에게 전달이 안 되고 있음.
|
|
|
|
```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줄) |
|