Files
C.E.L_Slide_test2/docs/history/IMPROVEMENT-PHASE-L.md
kyeongmin c42e01f060 문서 정리: Phase 히스토리 md를 docs/history/로 이동 + 오래된 테스트/에셋 정리
- 루트의 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>
2026-04-13 10:56:23 +09:00

23 KiB
Raw Permalink Blame History

Phase L: 렌더링 측정 에이전트 + Purpose 기반 공간 할당 + 수학적 조정

상태: 완료 — Selenium 측정 + 피드백 루프 구축. Phase O에서 container div 감지 추가.

Phase I~K에서 프롬프트/규칙/검수를 개선했지만, 실제 렌더링 결과를 측정하지 않아 미충족 7건 + 부분충족 4건이 해결되지 않음. 핵심: LLM이 추정하는 것이 아니라, 코드가 정확하게 계산하고 측정하는 구조로 전환.

후속 변경 (Phase O):

  • allocate_height_budget()calculate_container_specs()로 교체
  • _max_height_px_container_height_px로 교체
  • max-height CSS 래퍼 → Phase N에서 제거
  • _MEASURE_SCRIPT.container-* 셀렉터 추가

근본 문제

현재 파이프라인은 "만들고 나서 맞는지 모른다" 구조.

시점 지금 있어야 하는 것
만들기 전 블록 타입별 고정값 합산 (compact=70px) purpose별 비율로 실제 px 예산 할당
만든 후 LLM이 HTML 텍스트 읽고 추정 렌더링 엔진이 실제 px 측정
안 맞을 때 LLM이 "shrink 0.7" 추정 수학 공식으로 정확한 축약량 계산

미충족 + 부분충족 전체 목록 (11건)

미충족 7건

# 항목 현재 상태 원인
1 2단계 높이 검증 블록 타입별 고정값 합산 실제 텍스트 양 반영 안 됨
2 5단계 높이 초과 감지 글자 수로 추정 실제 px 모름
3 5단계 핵심전달 주인공 확인 추정 실제 크기 비율 모름
4 5단계 문제제기 간결 확인 추정 실제 렌더링 높이 모름
5 5단계 비교표 잘림 감지 추정 scrollHeight vs clientHeight 안 봄
6 4단계 CSS 조정 효과 검증 없음 조정 전후 비교 안 함
7 5단계 Kei 검수 근거 추정 기반 실제 수치 없이 검수

부분충족 4건

# 항목 현재 상태 원인
8 Step B Sonnet 높이 예산 준수 프롬프트 지시만 물리적 강제 없음
9 Step 3 편집자 분량 준수 가이드라인만 정확한 max 글자 수 계산 안 됨
10 Step 5 shrink/expand 효과 비율로 조정 조정 후 재측정 안 함
11 5단계 용어정의 sidebar 확인 프롬프트 지시만 코드 레벨 강제 없음

해결 방법 4가지

방법 1: Purpose 기반 공간 할당 (만들기 전)

원리: purpose의 중요도에 따라 zone 내 각 블록의 max-height를 코드로 결정론적으로 할당.

body zone = 490px (전체 예산)

purpose별 비율 할당:
  핵심전달 = 55% → max 270px
  문제제기 = 20% → max 98px
  근거사례 = 25% → max 122px

→ 블록 수와 purpose에 따라 자동 계산
→ AI 추정이 아닌 코드 계산

구현:

PURPOSE_WEIGHT = {
    "핵심전달": 0.55,    # 주인공 — 가장 큰 비중
    "문제제기": 0.20,    # 도입부 — 간결
    "근거사례": 0.25,    # 보조 — 짧게
    "결론강조": 1.0,     # footer 전용 (별도 zone)
    "용어정의": 1.0,     # sidebar 전용 (별도 zone)
}

def allocate_height_budget(blocks: list[dict], zone_budget_px: int) -> dict:
    """purpose별 비중으로 각 블록의 max-height를 할당한다."""
    flow_blocks = [b for b in blocks if b.get("role") != "reference"]
    total_weight = sum(PURPOSE_WEIGHT.get(b.get("purpose", ""), 0.2) for b in flow_blocks)
    gap_total = 20 * max(0, len(flow_blocks) - 1)
    available = zone_budget_px - gap_total

    allocation = {}
    for block in flow_blocks:
        weight = PURPOSE_WEIGHT.get(block.get("purpose", ""), 0.2)
        ratio = weight / total_weight
        allocation[block.get("topic_id")] = int(available * ratio)

    return allocation
    # 예: {1: 98, 3: 270, 5: 122}  (topic_id → max_height_px)

해결하는 미충족: #1 (높이 검증), #3 (주인공 확인), #8 (예산 강제)


방법 2: 렌더링 측정 에이전트 (만든 후)

원리: HTML을 실제 브라우저에서 렌더링하고 각 zone/block의 px을 정확히 측정.

Selenium (이미 설치됨) 사용:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def measure_rendered_heights(html: str, slide_width: int, slide_height: int) -> dict:
    """렌더링된 HTML의 각 zone/block 실제 px 높이를 측정한다."""
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument(f"--window-size={slide_width},{slide_height}")
    driver = webdriver.Chrome(options=options)

    try:
        driver.get("data:text/html;charset=utf-8," + html)

        results = driver.execute_script("""
            const slide = document.querySelector('.slide');
            const zones = {};

            // 각 zone (area) 측정
            slide.querySelectorAll('[class^="area-"]').forEach(zone => {
                const className = zone.className;
                const blocks = [];

                zone.querySelectorAll('[class^="block-"]').forEach(block => {
                    blocks.push({
                        className: block.className,
                        scrollHeight: block.scrollHeight,
                        clientHeight: block.clientHeight,
                        overflowed: block.scrollHeight > block.clientHeight,
                        excess_px: Math.max(0, block.scrollHeight - block.clientHeight)
                    });
                });

                zones[className] = {
                    scrollHeight: zone.scrollHeight,
                    clientHeight: zone.clientHeight,
                    overflowed: zone.scrollHeight > zone.clientHeight,
                    excess_px: Math.max(0, zone.scrollHeight - zone.clientHeight),
                    blocks: blocks
                };
            });

            // 슬라이드 전체
            return {
                slide: {
                    scrollHeight: slide.scrollHeight,
                    clientHeight: slide.clientHeight,
                    overflowed: slide.scrollHeight > slide.clientHeight
                },
                zones: zones
            };
        """)
        return results
    finally:
        driver.quit()

측정 결과 예시:

{
  "slide": {"scrollHeight": 750, "clientHeight": 720, "overflowed": true},
  "zones": {
    "area-body": {
      "scrollHeight": 520, "clientHeight": 490, "overflowed": true, "excess_px": 30,
      "blocks": [
        {"className": "block-quote-big", "scrollHeight": 160, "clientHeight": 160, "overflowed": false},
        {"className": "block-topic-header", "scrollHeight": 80, "clientHeight": 80, "overflowed": false},
        {"className": "block-split-compare", "scrollHeight": 280, "clientHeight": 250, "overflowed": true, "excess_px": 30}
      ]
    },
    "area-sidebar": {
      "scrollHeight": 400, "clientHeight": 490, "overflowed": false
    }
  }
}

viewport 크기는 config에서 읽음 (하드코딩 아님):

from src.config import settings
results = measure_rendered_heights(html, settings.slide_width, settings.slide_height)

해결하는 미충족: #2 (높이 초과 감지), #5 (비교표 잘림), #6 (CSS 효과 검증), #7 (검수 근거), #10 (조정 효과)


방법 3: CSS max-height 제약 (구조적 보장)

원리: 방법 1에서 할당한 max-height를 실제 CSS에 적용하여 물리적으로 넘치지 않게 함.

렌더링 시 적용:

# renderer.py에서 블록 렌더링 시 max-height 주입
for block in blocks:
    allocated = height_allocation.get(block.get("topic_id"))
    if allocated:
        block["_max_height_px"] = allocated
<!-- 템플릿에서 max-height 적용 -->
<div style="max-height: {{ _max_height_px }}px; overflow: hidden;">
  <!-- 블록 내용 -->
</div>

측정 에이전트(방법 2)가 overflow 감지:

  • scrollHeight > clientHeight → 콘텐츠가 잘림 → 축약 필요
  • 정확한 초과량(excess_px) 제공

해결하는 미충족: #8 (예산 강제), #11 (sidebar 물리적 강제)


방법 4: 조정량 수학적 계산 (AI 추정 → 공식)

원리: 측정 에이전트가 보고한 excess_px에서 삭제할 글자 수를 수학 공식으로 계산.

def calculate_trim_chars(
    excess_px: int,
    font_size_px: float,
    line_height: float,
    container_width_px: int,
    avg_char_width_px: float = 16.0,  # 한글 Pretendard 기준
) -> int:
    """초과 px에서 삭제할 글자 수를 수학적으로 계산한다.

    AI 추정이 아닌 결정론적 공식.
    """
    line_height_px = font_size_px * line_height
    lines_to_remove = math.ceil(excess_px / line_height_px)
    chars_per_line = int(container_width_px / avg_char_width_px)
    chars_to_remove = lines_to_remove * chars_per_line
    return chars_to_remove

# 예: excess_px=62, font=16px, line-height=1.7, width=700px
# → line_height_px = 27.2
# → lines_to_remove = ceil(62/27.2) = 3
# → chars_per_line = 700/16 = 43
# → chars_to_remove = 3 × 43 = 129자

편집자 재호출 시:

# 기존: "shrink target_ratio: 0.7" (AI 추정)
# 변경: "quote-big-mark의 quote_text를 129자 줄여라" (수학적 계산)

해결하는 미충족: #4 (간결 확인), #9 (편집자 분량 정확), #10 (shrink 효과)


전체 통합 파이프라인 (Phase L 적용 후)

[1단계] Kei 분석
  → purpose별 꼭지 + 비중 결정
      ↓
[방법 1] Purpose 기반 공간 할당 (코드, 결정론적)
  → body 내 각 블록별 max-height 할당 (px)
  → max 글자 수 수학적 계산 (방법 4)
      ↓
[2단계] 팀장 블록 선택
  → 할당된 max-height 안에서 가능한 블록만 선택
      ↓
[3단계] 편집자 텍스트 채움
  → max 글자 수 제약 (수학적 계산 기반, AI 추정 아님)
      ↓
[4단계] CSS 조정 + 렌더링
  → max-height CSS 제약 포함 (방법 3)
      ↓
[방법 2] 렌더링 측정 에이전트 (Selenium)
  → 각 zone/block의 실제 px 측정
  → overflow 감지 (scrollHeight > clientHeight)
      ↓
  ├── 맞으면 → [5단계] Kei 검수 (실제 px 수치 전달)
  │             Kei가 받는 정보:
  │             "body zone: 실제 480px / 예산 490px — OK"
  │             "핵심전달 블록: 260px (body의 54%) — 주인공 비중 충족"
  │             "비교표: 250px, 잘림 없음"
  │             → 근거 있는 콘텐츠 검수 가능
  │
  └── 안 맞으면 → [방법 4] 수학적 축약량 계산
                  "quote-big-mark: 62px 초과 → 129자 삭제 필요"
                  → 편집자 재호출 (정확한 글자 수)
                  → 재렌더링 → 재측정 → 반복

미충족/부분충족 해결 매핑

# 항목 해결 방법 근거
1 2단계 높이 검증이 추정 방법 1 (할당) + 방법 2 (측정) purpose별 px 할당 + 실제 렌더링 검증
2 5단계 높이 초과 감지가 추정 방법 2 (측정) scrollHeight > clientHeight 정확 감지
3 5단계 핵심전달 주인공 확인 불가 방법 1 (할당) + 방법 2 (측정) 할당 비율 55% 대비 실제 비율 비교
4 5단계 문제제기 간결 확인 불가 방법 2 (측정) + 방법 4 (계산) 실제 px + 수학적 글자 수 계산
5 5단계 비교표 잘림 감지 불가 방법 2 (측정) scrollHeight > clientHeight로 잘림 정확 감지
6 4단계 CSS 조정 효과 검증 불가 방법 2 (측정) 조정 전후 실제 px 비교
7 5단계 Kei 검수 근거 없음 방법 2 (측정) 실제 px 수치를 Kei에게 전달
8 Step B 높이 예산 안 지킴 방법 1 (할당) + 방법 3 (CSS) max-height로 물리적 강제
9 편집자 분량 안 지킴 방법 4 (계산) 할당 높이에서 max 글자 수 수학적 계산
10 shrink 효과 검증 불가 방법 2 (측정) 조정 후 재렌더링 → 재측정
11 용어정의 sidebar 강제 방법 3 (CSS) sidebar 외 zone에서 용어정의 블록 물리적 차단

실행 순서

L-Step 1: 공간 할당 엔진

  1. PURPOSE_WEIGHT 상수 + allocate_height_budget() 함수
  2. calculate_trim_chars() 수학적 글자 수 계산 함수
  3. pipeline.py에서 2단계 완료 후 할당 실행

L-Step 2: 렌더링 측정 에이전트

  1. measure_rendered_heights() 함수 (Selenium headless)
  2. pipeline.py에서 4단계 완료 후 측정 실행
  3. 측정 결과를 step4_measurement.json으로 저장 (K-1 연동)

L-Step 3: CSS max-height 제약

  1. renderer.py에서 블록별 max-height 적용
  2. 할당 → CSS 제약 → 렌더링 → 측정 파이프 연결

L-Step 4: 피드백 루프

  1. 측정 결과 overflow → 수학적 축약량 계산 → 편집자 재호출
  2. 재렌더링 → 재측정 → 맞으면 5단계로
  3. Kei 검수에 실제 px 수치 전달

필요 기술/도구

도구 용도 설치 상태
Selenium + Chrome headless 렌더링 측정 설치됨 (4.34.0)
ChromeDriver Selenium 구동 webdriver-manager로 자동 관리
math (Python 표준) 축약량 계산 기본 포함
config.py settings viewport 크기 (하드코딩 방지) 이미 존재 (slide_width, slide_height)

하드코딩 방지

  • viewport 크기: settings.slide_width, settings.slide_height에서 읽음
  • purpose 비율: PURPOSE_WEIGHT 상수 (범용, 콘텐츠 무관)
  • 글자 수 계산: 폰트 크기/line-height를 CSS 변수에서 읽거나 config에서 관리
  • 반응형 전환 시: config만 바꾸면 측정도 따라감

코드 조사 결과 (정밀 검토)

현재 있는 것

항목 위치 상태
zone별 budget_px design_director.py 322~370행 4개 프리셋 × 4개 zone
HEIGHT_COST_PX design_director.py 906~911행 compact=70, medium=150, large=250, xlarge=400
overflow 수집 함수 design_director.py 962~1069행 블록 타입 기반 추정 (실제 렌더링 아님)
style_override 주입 경로 slide-base.html 45행 max-height 주입 가능
Selenium v4.34.0 사용 가능
Pillow 설치됨 사용 가능
config slide_width/height config.py 1280/720

없는 것 (Phase L에서 구현)

항목 필요 이유
PURPOSE_WEIGHT 상수 purpose → 공간 비율 매핑. 현재 존재하지 않음
allocate_height_budget() zone 내 블록별 max-height 계산. 현재 없음
measure_rendered_heights() 실제 렌더링 px 측정. 현재 없음
calculate_trim_chars() 초과 px → 삭제 글자 수 계산. 현재 없음
Pretendard 로컬 폰트 CDN만 있음. Pillow 계산용으로 다운로드 필요
max-height CSS 적용 현재 area에 max-height 없음

충돌/회귀 검토

방법 1 (Purpose 할당)

  • PURPOSE_WEIGHT 상수 신규 추가 → 기존 코드와 충돌 없음
  • allocate_height_budget() 신규 함수 → _validate_height_budget()별개, 충돌 없음
  • pipeline.py Stage 2 이후 삽입 → 기존 흐름 변경 없이 추가
  • Phase I~K 회귀 없음

방법 2 (Selenium 측정)

  • measure_rendered_heights() 신규 모듈 (src/slide_measurer.py) → 기존 코드와 충돌 없음
  • pipeline.py Stage 4 이후 삽입 → 기존 render_slide() 결과를 입력으로 사용
  • 주의: Selenium 동기식 → asyncio.to_thread() 래핑 필요
  • Kei 검수에 측정 결과 전달 → call_kei_final_review() 파라미터 확장
  • 회귀 없음: 기존 HTML 렌더링 그대로, 측정은 추가 단계

방법 3 (CSS max-height)

  • style_override에 max-height 주입 → 기존 area_styles 구조 활용
  • 충돌 주의: Phase A-5에서 .slide > div { overflow: visible }로 변경한 이유가 "텍스트 잘림 방지"
    • max-height 적용 시 overflow: visible과 충돌
    • 해결: 측정 시에만 overflow: hidden 임시 적용하거나, 블록 레벨에서만 max-height 적용 (area 레벨이 아닌)
  • Phase I~K 회귀 없음

방법 4 (수학적 계산)

  • Pretendard 로컬 폰트 필요 → CDN에서 다운로드하여 data/fonts/에 캐싱
  • Pillow multiline_textbbox() 사용 → 기존 코드와 충돌 없음
  • calculate_trim_chars() 신규 유틸 → 별도 모듈
  • Phase I~K 회귀 없음

Kei vs Sonnet vs 코드 역할 분담

역할 담당 AI/코드
Purpose 비율 결정 코드 (PURPOSE_WEIGHT) 결정론적
max-height 할당 코드 (allocate_height_budget) 결정론적
max 글자 수 계산 코드 (calculate_trim_chars) 결정론적
렌더링 측정 Selenium (브라우저 엔진) 결정론적
overflow 감지 코드 (scrollHeight > clientHeight) 결정론적
텍스트 축약 실행 Kei (편집자, Kei API) AI (도메인 지식)
최종 검수 Kei (실장, Kei API) AI (실제 px 수치 기반)
CSS 조정 Sonnet (실무자) AI (Stage 4 기존)

핵심: 측정/계산/감지는 전부 코드(결정론적). AI는 콘텐츠 판단(축약/검수)만.


주의가 필요한 3곳

1. overflow: visible vs max-height 충돌

현재: .slide > div { overflow: visible } (Phase A-5) Phase L: 블록에 max-height 적용 시 넘치는 콘텐츠가 visible 상태로 보임 해결 방안:

  • (A) 블록 wrapper에 overflow: hidden + max-height → 블록 레벨에서 잘림
  • (B) area 레벨은 visible 유지, 블록 레벨에서만 제약 → Phase A-5 원칙 유지
  • 권장: (B) — area는 건드리지 않고, 개별 블록 wrapper에만 max-height 적용

2. Selenium 동기식 → async 파이프라인

현재: pipeline.py 전체가 async Selenium: 동기식 API 해결:

import asyncio

async def measure_async(html: str) -> dict:
    return await asyncio.to_thread(measure_rendered_heights, html)

3. Pretendard 로컬 폰트

현재: CDN만 (@import url) Pillow 계산에 필요: 로컬 .ttf 파일 해결:

  • 첫 실행 시 CDN에서 다운로드 → data/fonts/Pretendard-Regular.ttf 캐싱
  • 또는 프로젝트에 폰트 파일 포함 (라이선스: OFL — 재배포 가능)

실행 방안 상세

L-Step 1: 공간 할당 엔진

신규 파일: src/space_allocator.py

PURPOSE_WEIGHT = {
    "핵심전달": 0.55,
    "문제제기": 0.20,
    "근거사례": 0.25,
    "결론강조": 1.0,   # footer 전용
    "용어정의": 1.0,   # sidebar 전용
}

def allocate_height_budget(blocks, zone_budget_px, gap_px=20):
    """purpose 비중으로 각 블록의 max-height를 할당한다. 결정론적."""
    ...

def calculate_max_chars(max_height_px, font_size_px, line_height, container_width_px, font_path):
    """할당된 높이에서 최대 글자 수를 수학적으로 계산한다."""
    ...

def calculate_trim_chars(excess_px, font_size_px, line_height, container_width_px, font_path):
    """초과 px에서 삭제할 글자 수를 수학적으로 계산한다."""
    ...

반영 위치: pipeline.py Stage 2 완료 후 충돌: 없음. 신규 모듈. 회귀: 없음.

L-Step 2: 렌더링 측정 에이전트

신규 파일: src/slide_measurer.py

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from src.config import settings

def measure_rendered_heights(html: str) -> dict:
    """렌더링된 HTML의 각 zone/block 실제 px을 측정한다. 결정론적."""
    options = Options()
    options.add_argument("--headless=new")
    options.add_argument(f"--window-size={settings.slide_width},{settings.slide_height}")
    driver = webdriver.Chrome(options=options)
    try:
        driver.get("data:text/html;charset=utf-8," + html)
        # 폰트 로딩 대기
        driver.execute_script("return document.fonts.ready")
        # 각 zone/block 측정
        results = driver.execute_script("""...""")
        return results
    finally:
        driver.quit()

반영 위치: pipeline.py Stage 4 완료 후 (렌더링 직후) 저장: step4_measurement.json (K-1 연동) 충돌: 없음. 신규 모듈. 회귀: 없음.

L-Step 3: CSS max-height 제약

반영 위치: renderer.py 블록 렌더링 시 방식: 블록 wrapper에 max-height 적용 (area 레벨 아님 — Phase A-5 원칙 유지)

<!-- 블록별 max-height (area 레벨이 아닌 블록 레벨) -->
<div style="max-height: {{ _max_height_px }}px; overflow: hidden;">
  {{ block_html }}
</div>

충돌: Phase A-5 overflow: visible은 area 레벨 → 블록 레벨 max-height와 충돌 없음 회귀: 없음.

L-Step 4: 피드백 루프

반영 위치: pipeline.py Stage 4~5 사이

렌더링 완료 (Stage 4)
    ↓
측정 (slide_measurer)
    ↓
overflow 있으면:
    수학적 축약량 계산 (space_allocator)
    편집자 재호출 (fill_content) — "quote_text를 129자 줄여라"
    재렌더링 (render_slide)
    재측정
    MAX 3회 반복
    ↓
overflow 없으면:
    Kei 검수 (call_kei_final_review) — 실제 px 수치 포함

Kei 검수에 전달할 측정 결과:

"body zone: 실제 480px / 예산 490px — OK"
"핵심전달(compare-2col-split): 260px (body의 54%) — 주인공 비중 충족"
"문제제기(quote-big-mark): 90px (body의 19%) — 간결"
"비교표: scrollHeight=250, clientHeight=260 — 잘림 없음"

충돌: 기존 Stage 5 Kei 검수 구조 유지. 파라미터에 measurement 추가만. 회귀: 없음.


하드코딩 방지 확인

항목 하드코딩? 근거
PURPOSE_WEIGHT 비율 아님 범용 상수. 콘텐츠 유형 무관.
max-height px 아님 budget_px × purpose 비율로 계산. 고정값 아님.
viewport 크기 아님 settings.slide_width/height에서 읽음.
폰트 메트릭 아님 Pillow가 실제 폰트 파일에서 측정.
축약 글자 수 아님 excess_px / line_height × chars_per_line 공식 계산.
CSS max-height 아님 allocate_height_budget() 결과를 동적 주입.
overflow 감지 아님 scrollHeight > clientHeight 브라우저 네이티브.

예상 효과 (Phase L 적용 전후)

항목 Phase L 전 Phase L 후
비교표 잘림 모름 scrollHeight 250 > clientHeight 240 → 10px 잘림 감지
핵심전달 주인공 추정 260px / 490px = 53% — 주인공 비중 수치로 확인
문제제기 간결 추정 90px / 98px 할당 — 할당 내 OK
shrink 효과 모름 조정 전 520px → 조정 후 480px — 40px 감소 확인
Kei 검수 근거 없음 실제 px 수치 기반 판단
편집자 분량 가이드만 max 129자 — 수학적 계산

이력

날짜 내용
2026-03-26 Phase K 완료 후 결과물 분석. 미충족 7건 + 부분충족 4건 전수 진단. 4가지 해결 방법 도출. Phase L 계획 수립.
2026-03-26 코드 전수 조사 + 충돌/회귀 정밀 검토 완료. 주의 사항 3곳 식별. 실행 방안 상세 확정.