Files
C.E.L_Slide_test2/COMPREHENSIVE_VALIDATION_REPORT.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

540 lines
17 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Design Agent — 3건 수정사항 종합 검증 보고서
**검증 일시:** 2026-03-28 10:00
**검증 범위:** space_allocator.py, slide_measurer.py, catalog.yaml 연계 파이프라인
**방법론:** 코드 추적 + 파이프라인 시뮬레이션 + MD 문서 동기화 확인
---
## 📊 검증 결과 요약
| # | 항목 | 구현 | 통합 | 문서 | 종합 |
|---|------|------|------|------|------|
| **1** | space_allocator.py: topic당 높이 판단 | ✅ | ✅ | 🟡 | ✅ 95% |
| **2** | slide_measurer.py: container 감지 | ✅ | ✅ | ✅ | ✅ 100% |
| **3** | catalog.yaml: schema 글자수 구조 | ✅ | 🔴 | 🟡 | ⚠️ 60% |
**전체 평가:****수정 의도는 정확하나 #3 catalog schema가 실제로 파이프라인에서 사용되지 않음**
---
## 🔍 상세 검증
### 1⃣ space_allocator.py: topic당 높이 기반 height_cost 판단
#### ✅ 구현 상태
**파일:** `src/space_allocator.py` 라인 51-61
```python
# 블록 내부 제약 계산 — topic당 높이로 판단
topic_count = max(1, len(topic_ids))
per_topic_px = height_px // topic_count # ← 컨테이너 높이 / topic 개수
# height_cost 허용 범위: topic당 높이 기준 (컨테이너 전체가 아님)
max_cost = _max_allowed_height_cost(per_topic_px) # ← 핵심 함수 호출
# ...
```
**함수 정의 (라인 129-137):**
```python
def _max_allowed_height_cost(container_height_px: int) -> str:
"""컨테이너 높이에서 허용되는 최대 height_cost."""
if container_height_px >= 350:
return "xlarge"
elif container_height_px >= 200:
return "large"
elif container_height_px >= 80:
return "medium"
else:
return "compact"
```
#### ✅ 파이프라인 통합
**pipeline.py 라인 68-82에서 호출:**
```python
container_specs = calculate_container_specs(
page_structure=page_struct, # Kei 비중 판단 {"본심": {"topic_ids": [3], "weight": 0.6}, ...}
topics=analysis.get("topics", []), # 5개 topic 정보
preset=preset,
...
)
```
**데이터 흐름:**
```
1. Kei: page_structure 판단
↓ (page_structure = {"본심": {"topic_ids": [3], "weight": 0.6}}, ...)
2. O-1: calculate_container_specs()
- per_topic_px = 180 // 1 = 180px (본심 컨테이너 180px / 1개 topic)
- max_cost = _max_allowed_height_cost(180) = "medium" ✅
3. O-3: finalize_block_specs()
- 블록에 _container_height_px=180, _max_items=2, _max_chars_total=320 설정
4. 편집자 (stage 3):
- "최대 글자 수: 320자, 항목 수: 2개" 가이드 전달
```
#### 🟡 문서 동기화 상태
**ARCHITECTURE_OVERVIEW.md 라인 156-170:**
```
Phase O-1 과정:
3. 비중 비율로 높이 할당 (zone 예산 복분)
4. 높이 → height_cost 매핑 (compact/medium/large/xlarge)
```
**문제:**
- "height_cost 매핑"만 기술되어 있음
- **"topic당 높이로 판단"이 명시되지 않음** ← 개선 필요
**개선된 설명:**
> Phase O-1 과정:
> 3. 비중으로 컨테이너 높이 할당 (ex. 180px)
> 4. **topic당 높이로 max_height_cost 판단** (180px / 1 topic = 180px → **medium**) ← 중요
> 5. 글자 수/항목 수 제약 계산
#### ✅ 검증 결과
| 항목 | 상태 | 근거 |
|------|------|------|
| 코드 구현 | ✅ | _max_allowed_height_cost() 함수 정확 |
| 파이프라인 호출 | ✅ | pipeline.py 68-82줄 O-1 통합 |
| 데이터 흐름 | ✅ | per_topic_px 계산 후 height_cost 결정 |
| 문서 정확도 | 🟡 | "topic당 판단" 명시 필요 |
---
### 2⃣ slide_measurer.py: container 감지 및 overflow 체크
#### ✅ 구현 상태
**파일:** `src/slide_measurer.py` 라인 14-62
**JavaScript 측정 스크립트:**
```javascript
// Phase O: 컨테이너 측정 (container-* 클래스)
var containerDivs = slide.querySelectorAll('[class*="container-"]');
for (var k = 0; k < containerDivs.length; k++) {
var container = containerDivs[k];
var containerMatch = container.className.match(/container-(.+)/);
if (!containerMatch) continue;
var containerName = containerMatch[1];
result.containers[containerName] = {
scrollHeight: Math.round(container.scrollHeight),
clientHeight: Math.round(container.clientHeight),
allocatedHeight: parseInt(container.style.height) || 0,
overflowed: container.scrollHeight > container.clientHeight + 2,
excess_px: Math.max(0, Math.round(container.scrollHeight - container.clientHeight)),
...
};
}
```
**측정 결과 구조:**
```json
{
"containers": {
"본심": {
"scrollHeight": 190,
"clientHeight": 180,
"allocatedHeight": 180,
"overflowed": true,
"excess_px": 10,
"blocks": [
{"block_type": "topic-left-right", "scrollHeight": 95, "overflowed": false},
{"block_type": "topic-left-right", "scrollHeight": 95, "overflowed": false}
]
},
"배경": {
"scrollHeight": 50,
"clientHeight": 180,
"allocatedHeight": 180,
"overflowed": false,
"excess_px": 0,
"blocks": []
}
}
}
```
#### ✅ 파이프라인 통합
**pipeline.py 라인 177-202 (Phase L):**
```python
# Phase L: 렌더링 측정 + 피드백 루프 (최대 3회)
for measure_round in range(MAX_MEASURE_ROUNDS):
measurement = await asyncio.to_thread(measure_rendered_heights, html)
# overflow 감지 — zone + container 양쪽 체크
has_overflow = False
for zone_name, zone_data in measurement.get("zones", {}).items():
if zone_data.get("overflowed"):
has_overflow = True
break
# Phase O: container 레벨 overflow도 체크 ← 핵심
for cont_name, cont_data in measurement.get("containers", {}).items():
if cont_data.get("overflowed"):
has_overflow = True
logger.warning(
f"[측정] container-{cont_name}: "
f"scroll={cont_data.get('scrollHeight')}px > "
f"allocated={cont_data.get('allocatedHeight')}px "
f"(+{cont_data.get('excess_px')}px)"
)
break
if not has_overflow:
logger.info(f"[측정] 모든 zone/container 정상 (round {measure_round + 1})")
break
```
**overflow 감지 후 조치 (라인 203-230):**
```python
# 추출: container overflow 정보
for cont_name, cont_data in measurement.get("containers", {}).items():
if cont_data.get("overflowed"):
for block_m in cont_data.get("blocks", []): # ← container 내 블록 단위
if block_m.get("overflowed"):
trim_chars = calculate_trim_chars(
block_m.get("excess_px", excess),
width_px,
)
# 해당 블록의 _max_chars_total 축소
```
### 📊 시뮬레이션: container overflow 피드백 루프
**시나리오:**
- 본심 컨테이너 할당 높이: 180px
- 실제 렌더링 높이: 190px (overflow +10px)
- 블록 1: topic-left-right (95px, 정상)
- 블록 2: card-icon-desc (120px, +15px 초과)
**동작 순서:**
```
Round 1: 측정
├─ measurement.containers["본심"] = {
│ ├─ allocatedHeight: 180,
│ ├─ scrollHeight: 190,
│ ├─ overflowed: true,
│ ├─ excess_px: 10,
│ └─ blocks: [{...95px...}, {...120px...}] ← 블록 2가 +15px 초과
├─ Phase L 감지: "container-본심에서 +10px overflow"
├─ 조치: card-icon-desc의
│ └─ _max_chars_total: 300 → 285 (15자 축약)
Round 2: 재렌더링 + 재측정
├─ content_editor 재호출 (글자 수 제약 적용)
├─ 블록 2 텍스트 축약됨
├─ 재측정 결과:
│ └─ container
-본심: scrollHeight 185px / allocatedHeight 180px
│ → 여전히 +5px overflow
Round 3: 재조치
├─ 추가 5자 축약
├─ 재렌더링 + 재측정
└─ OK: container-본심 정상 (scrollHeight == allocatedHeight)
```
#### ✅ 문서 동기화 상태
**PROGRESS.md 라인 xxx (Selenium container 감지):**
```
Phase L: 렌더링 측정 + feedback loop
- zone 레벨 overflow 감지 ✅
- container 레벨 overflow 감지 ✅ (NEW)
```
**ARCHITECTURE_OVERVIEW.md 라인 xxx:**
```
Phase L (Measurement):
1. Selenium headless Chrome으로 렌더링
2. JavaScript로 zone 높이 측정
3. NEW: container-* 클래스로 역할별 높이 측정 ← 개선됨
4. 초과분 감지 → 피드백 루프
5. 최대 3회 재조정
```
#### ✅ 검증 결과
| 항목 | 상태 | 근거 |
|------|------|------|
| 코드 구현 | ✅ | slide_measurer.py 14-62줄 |
| JavaScript 정확성 | ✅ | container-* 셀렉터 + overflow 계산 정확 |
| 파이프라인 호출 | ✅ | pipeline.py 177-202줄 |
| 피드백 루프 | ✅ | 재렌더링 + 재측정 최대 3회 |
| 문서 정확도 | ✅ | PROGRESS.md, ARCHITECTURE_OVERVIEW.md 반영됨 |
---
### 3⃣ catalog.yaml: schema 글자수 필드 추가
#### ✅ 구현 상태
**파일:** `templates/catalog.yaml` 라인 44-46 (예. section-header-bar)
```yaml
- id: section-header-bar
name: 섹션 헤더 바
height_cost: compact
...
schema: # ← NEW: 글자수 가이드 구조화
title: {max_lines: 1, font_size: 18, ref_chars: {body: 25, sidebar: 20}, note: '18px bold white, 중앙정렬'}
subtitle: {max_lines: 1, font_size: 13, ref_chars: {body: 40, sidebar: 30}, note: '13px, 1줄'}
- id: topic-left-right
name: 좌우 꼭지 헤더
height_cost: compact
...
schema:
title: {max_lines: 2, font_size: 24, ref_chars: {body: 20}, note: '24px bold, 240px 고정폭'}
description: {max_lines: 2, font_size: 16, ref_chars: {body: 100}, note: '16px, 510px 너비'}
```
**schema 필드 구조:**
```
schema:
{slot_name}:
max_lines: N # 텍스트 라인 수 (줄바꿈 횟수)
font_size: N # 픽셀 단위
ref_chars: # zone별 글자 수 가이드
body: N # body zone(65% 너비)에서의 한 줄 글자 수
sidebar: N # sidebar(35%)에서의 글자 수
note: "..." # 추가 설명
```
#### 🔴 파이프라인 통합 **실패**
**문제 1: catalog 로더가 schema를 읽지 않음**
`src/block_search.py` 라인 xxx에서:
```python
with open(META_PATH, encoding="utf-8") as f:
_metadata = json.load(f) # ← block_metadata.json에서 로드
# block_metadata.json 생성 스크립트: scripts/build_block_index.py
# 이 스크립트가 catalog.yaml의 schema를 추출하여 metadata에 포함하는가? → 불명
```
**문제 2: metadata가 content_editor에 전달되지 않음**
`src/content_editor.py` 라인 xxx:
```python
# BLOCK_SLOTS를 사용 (설계 단계에 정의)
slots = BLOCK_SLOTS.get(block_type, {}) # ← catalog.yaml 아님
# catalog.yaml의 schema는 사용되지 않음!
```
**문제 3: 글자 수 가이드 전달 경로 불명**
현재 flow:
```
1. BLOCK_SLOTS (design_director.py에 하드코딩)
↓ (slot_desc만 전달, schema 아님)
2. content_editor.py (slot requirements 생성)
├─ slot_desc 포함
├─ char_guide 포함 (block에 설정되어 있으면)
└─ schema (catalog에만 있음, 사용 안 됨!)
3. Kei 프롬프트에 전달
```
#### 🟡 코드 검증: schema 실제 접근 시도
**전체 grep으로 schema 사용 검색:**
```bash
grep -r "schema" src/*.py templates/*.yaml
Result:
- templates/catalog.yaml: 37개 블록에 schema 정의 ✅
- src/*.py: 검색 결과 0
```
**결론:** schema 필드가 catalog.yaml에 정의되어 있지만 **코드에서 사용하지 않음**
#### ⚠️ 문서 동기화 상태
**PROGRESS.md (라인 xxx):**
> Phase O-3: finalize_block_specs()로 블록 내부 제약 계산
> - max_items, max_chars_total, font_size 등
**문제:**
- catalog.yaml schema 필드 추가를 기록하지 않음
- "schema 글자수 구조 변환"이 완료된 것처럼 보이지만 실제로는 미사용
**ARCHITECTURE_OVERVIEW.md:**
- catalog.yaml schema에 대한 언급 없음
- "catalog 37개 블록" 기술만 있음
---
## ⚠️ 문제점 분석
### Issue #1: catalog.yaml schema가 파이프라인에서 사용되지 않음
**근본 원인:**
1. BLOCK_SLOTS가 설계 단계 (design_director.py)에서 하드코딩됨
2. catalog.yaml은 렌더러에서만 사용 (템플릿 경로 매핑)
3. schema 필드: 메타데이터로 정의되었으나 **읽는 함수 없음**
**현재 상태:**
```
catalog.yaml (37개 블록)
├─ id, name, template ✅ (renderer.py에서 사용)
├─ height_cost ✅ (design_director.py에서 사용)
├─ visual, when, not_for, purpose_fit ❓ (사용 불명)
└─ schema ❌ (완전히 미사용)
```
**개선 필요 사항:**
#### 옵션 A: catalog 로더 추가 (권장)
```python
# src/catalog_loader.py (신규)
def load_catalog_schema() -> dict[str, dict]:
"""catalog.yaml에서 블록별 schema 추출"""
catalog_path = Path(__file__).parent.parent / "templates" / "catalog.yaml"
with open(catalog_path, encoding="utf-8") as f:
data = yaml.safe_load(f)
return {
b["id"]: b.get("schema", {})
for b in data.get("blocks", [])
}
# src/content_editor.py에서
schema_map = load_catalog_schema()
schema = schema_map.get(block_type, {})
# schema를 Kei 프롬프트에 전달
```
#### 옵션 B: BLOCK_SLOTS에 schema 병합
```python
# design_director.py
BLOCK_SLOTS = {
"topic-left-right": {
"required": [...],
"optional": [...],
"schema": { # ← catalog.yaml과 동기화
"title": {"max_lines": 2, "font_size": 24, ...},
...
}
}
}
```
---
## 📈 종합 평가
### 수정 의도 분석
| 수정 | 의도 | 실제 | 평가 |
|------|------|------|------|
| **#1** | topic당 높이로 height_cost 판단하여 블록 선택 정확도↑ | ✅ 정확히 구현됨 | ✅ 완성 |
| **#2** | container 레벨 overflow 감지 → 피드백 루프로 정확도↑ | ✅ 정확히 구현됨 | ✅ 완성 |
| **#3** | schema로 글자수 메타데이터 구조화 → content_editor 정확도↑ | ✅ 작성됨 / ❌ 미사용 | ⚠️ 불완전 |
### 수정율 (완성도)
- **#1 space_allocator:** 100% ✅
- **#2 slide_measurer:** 100% ✅
- **#3 catalog schema:** 60% ⚠️ (정의만 함, 사용 안 함)
### 다음 단계
🔴 **즉시 필요:**
```python
# src/content_editor.py에 schema 로더 추가
def _load_block_schema() -> dict[str, dict]:
from pathlib import Path
import yaml
catalog_path = Path(__file__).parent.parent / "templates" / "catalog.yaml"
with open(catalog_path, encoding="utf-8") as f:
data = yaml.safe_load(f)
return {
b["id"]: b.get("schema", {})
for b in data.get("blocks", [])
}
# fill_content() 함수에서 schema 전달
schema = _load_block_schema().get(block_type, {})
if schema:
req_text += f"\n 슬롯 상세 스키마:\n"
for slot, spec in schema.items():
req_text += (
f" {slot}: "
f"{spec.get('max_lines', '?')}줄, "
f"{spec.get('font_size', '?')}px, "
f"본심:{spec.get('ref_chars', {}).get('body', '?')}\n"
)
```
---
## 📋 검증 체크리스트
```
[✅] 1. space_allocator.py _max_allowed_height_cost() 함수 구현
[✅] 2. pipeline.py에서 O-1로 호출
[✅] 3. container_specs 결과가 O-3에 전달
[✅] 4. finalize_block_specs()에서 _container_height_px 설정
[✅] 5. content_editor에서 char_guide로 사용
[✅] 6. slide_measurer.py container-* 셀렉터 추가
[✅] 7. measure_rendered_heights()에서 containers 반환
[✅] 8. pipeline.py Phase L에서 overflow 감지
[✅] 9. 피드백 루프: 재렌더링 + 재측정
[✅] 10. catalog.yaml schema 필드 37개 블록 모두 작성
[❌] 11. catalog 로더에서 schema 읽기 ← 미구현
[❌] 12. content_editor에서 schema 전달 ← 미구현
[❌] 13. Kei 프롬프트에 schema 포함 ← 미구현
```
---
## 매트릭스: MD 문서 vs 코드 동기화
| 파일 | 항목 | MD 기재 | 코드 | 동기화 |
|------|------|--------|------|--------|
| PROGRESS.md | BF-4 해결 | ✅ | ✅ | ✅ |
| ARCHITECTURE_OVERVIEW.md | Phase O-1 설명 | ✅ 기본 | ✅ 상세 | 🟡 |
| ARCHITECTURE_OVERVIEW.md | Phase L 측정 | ✅ 기본 | ✅ 상세(container 추가) | 🟡 |
| ARCHITECTURE_OVERVIEW.md | catalog schema | ❌ | ✅ | ❌ |
| README.md | catalog 필드 | 언급 없음 | 37개 블록 정의 | ❌ |
---
## 권고사항
### 🔴 우선순위 1 (즉시)
**catalog schema를 content_editor에 통합**
- 파일: `src/content_editor.py`
- 작업: `_load_block_schema()` 함수 추가 + fill_content()에서 호출
- 소요시간: 30분
- 영향: #3 완성도 60% → 100%
### 🟡 우선순위 2 (이번 주)
**MD 문서 업데이트**
- ARCHITECTURE_OVERVIEW.md: Phase O container 로직 상세 기술
- PROGRESS.md: catalog schema 활용 추가 기록
- 소요시간: 1시간
### 🟢 우선순위 3 (다음 주)
**통합 테스트**
- 3개 수정사항 end-to-end 테스트
- 컨테이너 overflow 시나리오 검증
- catalog schema 가이드 실제 사용 확인