Files
C.E.L_Slide_test2/IMPROVEMENT-PHASE-L.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

619 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 추정이 아닌 코드 계산
```
**구현:**
```python
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 (이미 설치됨) 사용:**
```python
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()
```
**측정 결과 예시:**
```json
{
"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에서 읽음 (하드코딩 아님):**
```python
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에 적용하여 물리적으로 넘치지 않게 함.
**렌더링 시 적용:**
```python
# renderer.py에서 블록 렌더링 시 max-height 주입
for block in blocks:
allocated = height_allocation.get(block.get("topic_id"))
if allocated:
block["_max_height_px"] = allocated
```
```html
<!-- 템플릿에서 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에서 삭제할 글자 수를 수학 공식으로 계산.
```python
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자
```
**편집자 재호출 시:**
```python
# 기존: "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: 렌더링 측정 에이전트
4. `measure_rendered_heights()` 함수 (Selenium headless)
5. pipeline.py에서 4단계 완료 후 측정 실행
6. 측정 결과를 step4_measurement.json으로 저장 (K-1 연동)
### L-Step 3: CSS max-height 제약
7. renderer.py에서 블록별 max-height 적용
8. 할당 → CSS 제약 → 렌더링 → 측정 파이프 연결
### L-Step 4: 피드백 루프
9. 측정 결과 overflow → 수학적 축약량 계산 → 편집자 재호출
10. 재렌더링 → 재측정 → 맞으면 5단계로
11. 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
**해결:**
```python
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`
```python
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`
```python
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 원칙 유지)
```html
<!-- 블록별 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곳 식별. 실행 방안 상세 확정. |