- 루트의 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>
17 KiB
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
# 블록 내부 제약 계산 — 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):
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에서 호출:
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 측정 스크립트:
// 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)),
...
};
}
측정 결과 구조:
{
"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):
# 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):
# 추출: 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)
- 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에서:
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:
# 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 사용 검색:
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가 파이프라인에서 사용되지 않음
근본 원인:
- BLOCK_SLOTS가 설계 단계 (design_director.py)에서 하드코딩됨
- catalog.yaml은 렌더러에서만 사용 (템플릿 경로 매핑)
- schema 필드: 메타데이터로 정의되었으나 읽는 함수 없음
현재 상태:
catalog.yaml (37개 블록)
├─ id, name, template ✅ (renderer.py에서 사용)
├─ height_cost ✅ (design_director.py에서 사용)
├─ visual, when, not_for, purpose_fit ❓ (사용 불명)
└─ schema ❌ (완전히 미사용)
개선 필요 사항:
옵션 A: catalog 로더 추가 (권장)
# 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 병합
# 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% ⚠️ (정의만 함, 사용 안 함)
다음 단계
🔴 즉시 필요:
# 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 가이드 실제 사용 확인