변경:
```
**design_director.py — sidebar 블록에 column_override 주입:**
`_validate_height_budget()` 함수 내, 금지 블록 처리 이후에 삽입:
```python
# sidebar 카드 블록 1열 강제 (J-6)
CARD_BLOCKS = {
"card-tag-image", "card-icon-desc", "card-image-3col",
"card-dark-overlay", "card-compare-3col", "card-image-round",
"card-stat-number",
}
for block in blocks:
if block.get("area") == "sidebar" and block.get("type") in CARD_BLOCKS:
if "data" not in block:
block["data"] = {}
block["data"]["column_override"] = 1
```
**충돌:** 없음. `column_override`는 새 키. `default(cards|length)`로 body에서는 기존대로.
**회귀:** 없음. 기존 렌더링 동작 변경 없음.
---
### J-7: Stage 5 최종 검토 Kei 전환
**방법:** `kei_client.py`에 `call_kei_final_review()` 신규 함수 추가 + `pipeline.py`에서 호출
**kei_client.py 신규 함수:**
```python
KEI_REVIEW_PROMPT = """당신은 11년 경력의 기획 실장이다. 디자인 팀장이 조립한 슬라이드를 최종 검수한다.
## 검수 관점
1. 핵심 메시지(core_message)가 시각적으로 명확히 전달되는가?
2. 콘텐츠 흐름(문제제기→사례→정의→관계→결론)이 블록 배치와 일치하는가?
3. 각 블록이 해당 꼭지의 purpose에 적합한가?
4. 중요한 내용이 빠지거나 과도하게 축소되지 않았는가?
5. 높이 초과: 각 zone의 블록+텍스트가 예산을 초과하는가?
- 텍스트 축약으로 해결 가능 → shrink
- 콘텐츠가 본질적으로 큼 → overflow_detected
## 조정 action
- expand: 텍스트 늘림 (target_ratio, 예: 1.3)
- shrink: 텍스트 줄임 (target_ratio, 예: 0.7)
- rewrite: 텍스트 재작성 (detail에 방향)
- overflow_detected: 높이 초과, 콘텐츠 판단 필요 (zone과 블록 명시)
## 출력 (JSON만)
{"needs_adjustment": true/false, "issues": ["이슈1"], "adjustments": [{"block_area": "...", "action": "...", "target_ratio": 1.3, "detail": "..."}]}
"""
async def call_kei_final_review(
html: str,
block_summary: list[str],
zone_budget_text: str,
overflow_hint_text: str,
analysis: dict[str, Any],
) -> dict[str, Any] | None:
"""Kei(Opus)가 최종 검수한다.
반드시 Kei API 경유. Sonnet 사용 절대 금지.
session_id: design-agent-final-review
"""
kei_url = getattr(settings, "kei_api_url", "http://localhost:8000")
core_message = analysis.get("core_message", "") if analysis else ""
topics_summary = ""
if analysis:
topics_summary = "\n".join(
f"- 꼭지 {t.get('id')}: {t.get('title', '')} [{t.get('purpose', '')}]"
for t in analysis.get("topics", [])
)
prompt = (
KEI_REVIEW_PROMPT + "\n\n"
f"## 핵심 메시지\n{core_message}\n\n"
f"## 꼭지 목록\n{topics_summary}\n\n"
f"## 블록별 데이터 양\n" + "\n".join(block_summary) +
zone_budget_text +
overflow_hint_text +
f"\n\n## 조립 HTML (요약)\n{html[:3000]}\n\n"
f"위 결과물을 검수하고 조정이 필요한지 판단해. JSON만."
)
try:
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream(
"POST",
f"{kei_url}/api/message",
json={
"message": prompt,
"session_id": "design-agent-final-review",
"mode_hint": "chat",
},
timeout=None,
) as response:
if response.status_code != 200:
logger.warning(f"Kei 최종 검수 HTTP {response.status_code}")
return None
full_text = await stream_sse_tokens(response)
if full_text:
result = _parse_json(full_text)
if result and "needs_adjustment" in result:
logger.info(f"[Kei 최종 검수] needs_adjustment={result['needs_adjustment']}")
return result
return None
except Exception as e:
logger.warning(f"Kei 최종 검수 실패: {e}")
return None
```
**pipeline.py 변경:**
- import: `from src.kei_client import ... call_kei_final_review`
- `_review_balance()` 내부: Sonnet API 호출 → `call_kei_final_review()` 호출로 교체
- 기존 `block_summary`, `zone_budget_text`, `overflow_hint_text` 구성 로직은 유지 (pipeline에 남음)
- `anthropic.AsyncAnthropic` + `client.messages.create` 코드 제거
- `import anthropic`은 Stage 4(`_adjust_design`)에서 아직 사용하므로 유지
**출력 스키마:** 기존과 100% 동일 → `_apply_adjustments()`, `_convert_kei_judgment()` 변경 불필요.
**overflow 처리:** 기존 Stage 5 루프의 overflow_detected → Kei overflow 호출 흐름 그대로 유지.
---
## 실행 프로세스 (의존 관계 + 순서)
```
Phase J-A: 팀장 권한 제한 + 가이드 수정
├── J-1: STEP_B_PROMPT Opus 존중 규칙 (design_director.py 744행)
├── J-2: section-header-bar body 금지 (BODY_FORBIDDEN_MAP + 교체 로직)
├── J-3a: purpose 가이드 수정 (504, 506행 + PURPOSE_FALLBACK)
├── J-3b: catalog.yaml 수정 (376행)
└── J-6: sidebar 1열 강제 (템플릿 2개 + design_director 주입)
↓ (J-A 완료 후)
Phase J-B: 편집자 강화
└── J-4: source 슬롯 금지 규칙 (EDITOR_PROMPT)
↓ (J-B 완료 후)
Phase J-C: 최종 검토 Kei 전환
└── J-7: call_kei_final_review() 신규 + pipeline Stage 5 교체
↓
검증: import + 서버 기동 + 결과물 비교
```
### Phase J-A 내부 의존 관계
- J-2는 `_validate_height_budget()` 수정 → J-6도 같은 함수 안에 삽입 → **J-2 먼저, J-6 이후**
- J-1, J-3a, J-3b는 서로 독립 → 순서 무관
### Phase J-C 의존
- J-7은 J-A/J-B와 독립이지만, **J-A 수정된 결과물로 검증해야 의미** → J-A/J-B 완료 후 실행
---
## 변경 파일 총괄
| 파일 | 항목 | 변경 성격 |
|------|------|----------|
| `src/design_director.py` | J-1, J-2, J-3a, J-6 | 프롬프트 + BODY_FORBIDDEN_MAP + PURPOSE_FALLBACK + sidebar column_override |
| `src/content_editor.py` | J-4 | EDITOR_PROMPT에 source 규칙 추가 |
| `src/kei_client.py` | J-7 | KEI_REVIEW_PROMPT + call_kei_final_review() 신규 |
| `src/pipeline.py` | J-7 | _review_balance() 내부 Sonnet → Kei 교체 + import 추가 |
| `templates/catalog.yaml` | J-3b | not_for 1건 수정 |
| `templates/blocks/cards/card-tag-image.html` | J-6 | column_override 지원 |
| `templates/blocks/cards/card-icon-desc.html` | J-6 | column_override 지원 |
---
## 충돌/회귀/오류 최종 검증
| 항목 | 충돌 | 회귀 | Kei/Sonnet | 하드코딩 | 단발성 | 주의 사항 |
|------|:---:|:---:|:----------:|:------:|:-----:|----------|
| J-1 | 없음 | 없음 | Sonnet(기존) | 없음 | 아님 | — |
| J-2 | **주의** | 없음 | — | 상수 | 아님 | 루프 중 삭제 → 별도 필터링 + zone_blocks 재구성 |
| J-3a | 없음 | I-1과 다른 목적 | Sonnet(기존) | 없음 | 아님 | PURPOSE_FALLBACK도 같이 수정 |
| J-3b | 없음 | I-2와 다른 목적 | — | 없음 | 아님 | — |
| J-4 | 없음 | I-5와 보완 | **Kei**(편집자) | 없음 | 아님 | — |
| J-6 | **주의** | 없음 | — | 범용 키 | 아님 | 템플릿 2개 수정 + data 주입 |
| J-7 | **주의** | 프로세스 재설계 유지 | **Kei**(신규) | 없음 | 아님 | pipeline import + Sonnet 코드 제거 |
**Sonnet 신규 투입: 0건**
**Kei API 사용: J-4(기존 편집자), J-7(신규 최종 검수)**
**하드코딩: 0건**
**회귀: 0건**
**단발성: 0건**
---
## 실행 결과 상세
### Phase J-A: 팀장 권한 제한 + 가이드 수정 (5개) ✅
| 항목 | 파일 | 반영 내용 |
|------|------|----------|
| J-1 | `src/design_director.py` 744행 | `"Opus 추천이 있으면 참고하되, 최종 선택은 팀장 판단"` → `"Opus 추천 블록을 기본 사용한다. 높이 초과 등 명확한 사유 없이 변경하지 마라. 변경 시 reason에 사유를 반드시 명시하라."` |
| J-2 | `src/design_director.py` 899행 | `BODY_FORBIDDEN_MAP`에 `"section-header-bar": None` 추가. 금지 블록 처리 로직 변경: `None`이면 교체가 아닌 삭제. `blocks_to_remove` 별도 리스트로 루프 중 삭제 안전 처리. 삭제 후 `zone_blocks` 재구성 추가. |
| J-3a | `src/design_director.py` 504행, 506행 | purpose 가이드: `근거사례 → card-icon-desc` → `card-numbered`, `용어정의 → card-icon-desc` → `card-numbered, dark-bullet-list`. `PURPOSE_FALLBACK` 892행: `"용어정의": "card-icon-desc"` → `"card-numbered"` |
| J-3b | `templates/catalog.yaml` 376행 | `not_for: '용어 정의 → card-icon-desc 사용'` → `'용어 정의 → card-numbered 사용'` |
| J-6 | `templates/blocks/cards/card-tag-image.html` 9행 | `--ct-count: {{ cards\|length }}` → `--ct-count: {{ column_override \| default(cards\|length) }}` |
| J-6 | `templates/blocks/cards/card-icon-desc.html` 9행 | `--ci-count: {{ cards\|length }}` → `--ci-count: {{ column_override \| default(cards\|length) }}` |
| J-6 | `src/design_director.py` `_validate_height_budget()` 내 | sidebar 카드 블록에 `block["data"]["column_override"] = 1` 주입. CARD_BLOCKS 상수로 대상 블록 정의. |
### Phase J-B: 편집자 강화 (1개) ✅
| 항목 | 파일 | 반영 내용 |
|------|------|----------|
| J-4 | `src/content_editor.py` EDITOR_PROMPT | `## source 슬롯 규칙 (절대 규칙)` 섹션 추가. 출처만 허용, 꼭지 제목/주제어 금지, 없으면 빈 문자열. 올바른/잘못된 예시 포함. Kei API(편집자)에 전달됨. |
### Phase J-C: 최종 검토 Kei 전환 (1개) ✅
| 항목 | 파일 | 반영 내용 |
|------|------|----------|
| J-7 | `src/kei_client.py` | `KEI_REVIEW_PROMPT` 상수 신규: 11년 경력 기획 실장 관점, 핵심 메시지 전달/콘텐츠 흐름/purpose 적합성/높이 초과 검수. `call_kei_final_review()` 함수 신규: session_id `"design-agent-final-review"`, Kei API SSE 스트리밍, 출력 스키마 기존과 100% 동일. |
| J-7 | `src/pipeline.py` import | `call_kei_final_review` import 추가 |
| J-7 | `src/pipeline.py` `_review_balance()` | Sonnet API(`anthropic.AsyncAnthropic` + `client.messages.create`) 코드 제거. `call_kei_final_review(html, block_summary, zone_budget_text, overflow_hint_text, analysis)` 호출로 교체. block_summary/zone_budget_text/overflow_hint_text 구성 로직은 pipeline에 유지. |
---
## 검증 체크리스트 (실행 완료)
### 팀장 권한 제한
- [x] J-1: STEP_B_PROMPT에 "Opus 추천 기본 사용, 변경 금지" 명시
- [x] J-2: BODY_FORBIDDEN_MAP에 section-header-bar: None. 삭제 로직 + zone_blocks 재구성
- [x] J-3a: purpose 가이드 용어정의/근거사례에서 card-icon-desc 제거 → card-numbered
- [x] J-3a: PURPOSE_FALLBACK 용어정의 → card-numbered
- [x] J-3b: catalog.yaml "용어 정의 → card-numbered"
- [x] J-6: 템플릿 2개 column_override 지원 + sidebar 블록에 column_override=1 주입
### 편집자 강화
- [x] J-4: EDITOR_PROMPT에 source 슬롯 금지 규칙 추가 (Kei API 편집자 경유)
### 최종 검토 Kei 전환
- [x] J-7: call_kei_final_review() 함수 신규 (kei_client.py)
- [x] J-7: _review_balance() → Sonnet 코드 제거, Kei API 호출로 교체
- [x] J-7: Stage 5에 Sonnet 모델 참조 0건 확인
### 기술 검증
- [x] 모든 모듈 import 성공
- [x] FastAPI 앱 로드 성공 (8 routes)
- [x] BLOCK_SLOTS 38/38, slot_desc 38/38 (Phase I 회귀 없음)
- [x] BODY_FORBIDDEN_MAP: section-header-bar=None 확인
- [x] PURPOSE_FALLBACK 용어정의=card-numbered 확인
### 절대 규칙 준수
- [x] Sonnet 신규 투입 0건 — Stage 5가 Kei API만 사용
- [x] 하드코딩 0건
- [x] 단발성 수정 0건
- [x] Phase I 회귀 0건
- [x] persona_agent 수정 0건
---
## 이력
| 날짜 | 내용 |
|------|------|
| 2026-03-26 | Phase I 완료 후 결과물 3회 비교. 7개 문제 진단. Phase J 계획 수립. |
| 2026-03-26 | 기술 조사 + 충돌/회귀/오류 검토 완료. 구현 상세 + 실행 프로세스 확정. |
| 2026-03-26 | **Phase J 실행 완료.** 7개 항목 전수 구현. 검증 전항목 통과. Stage 5 Kei 전환 확인. |