# Phase B: 누락 기능 구현 — 실행 상세

> 누락된 기능 구현. 실작업 5개 (B-6, B-7은 해결됨).
> 원칙: 하드코딩 금지. 모든 판단은 AI 사고. 회귀 금지.

---

## 실행 순서

```
[독립] B-1 (details 템플릿), B-4+B-5 (이미지/표 판단), B-8 (fallback 교체)
  → B-2 (인쇄 JS, B-1 후)
  → B-3 (catalog 등록, B-1 후)
```

---

## B-1: details-block 템플릿 제작 ✅ 완료

### 현재 상태
- BLOCK_SLOTS에 정의 있음 (design_director.py:47~49): `required: ["summary_text", "detail_content"], optional: ["label"]`
- _apply_defaults에 기본값 있음 (content_editor.py:248): `{"summary_text": "(상세 내용)", "detail_content": ""}`
- **HTML 템플릿 파일이 templates/blocks/ 어디에도 없음** → 렌더링 불가

### API 선택
- AI 호출 없음. HTML/CSS 템플릿만.

### 작업
`templates/blocks/emphasis/details-block.html` 신규 제작

**구조:**
```html
<details class="block-details">
    <summary class="dt-summary">
        {% if label %}<span class="dt-label">{{ label }}</span>{% endif %}
        {{ summary_text }}
    </summary>
    <div class="dt-content">{{ detail_content }}</div>
</details>
```

### 하드코딩 점검 — CSS 규칙
- 색상: `var(--color-*)` 만 사용. `#직접값` 금지
- 폰트: `var(--font-*)` 사용
- 여백: `var(--spacing-*)` 사용
- 테두리: `var(--border-width)`, `var(--accent-border)`, `var(--radius)` 사용
- **기존 emphasis 블록의 직접값(#1e3a5f 등)을 따라하지 않는다**

### 충돌/회귀
- 충돌: 없음. 신규 파일 추가만
- 회귀: 없음. BLOCK_SLOTS/_apply_defaults와 정합
- 단발성: 아님. `<details>/<summary>`는 HTML 표준 — 브라우저 내장, 의존성 없음

### 수정 파일
- 신규: `templates/blocks/emphasis/details-block.html`

### 구현 결과
- `templates/blocks/emphasis/details-block.html` 신규 제작 완료
- HTML 구조: `<details class="block-details">` → `<summary class="dt-summary">` (label + summary_text) → `<div class="dt-content">` (detail_content)
- CSS: **`#직접값` 0개** — 전부 디자인 토큰으로 구현
  - 배경: `var(--color-bg-subtle)`, 테두리: `var(--color-border)`, 액센트: `var(--color-accent)`
  - 폰트: `var(--font-body)`, `var(--font-caption)`, 여백: `var(--spacing-inner)`, `var(--spacing-block)`
- summary 마커: 기본 브라우저 마커 숨기고(`list-style: none`, `::-webkit-details-marker { display: none }`) 커스텀 ▶/▼ 표시
- 좌측 파란 액센트 라인: `border-left: var(--accent-border) solid var(--color-accent)` — quote-left-border와 톤 통일

---

## B-2: 인쇄 시 details 자동 펼침 JS ✅ 완료

### 현재 상태
- slide-base.html의 `@media print`에 page-break만 있음
- details 자동 펼침 JS 없음
- CLAUDE.md: "인쇄 시 JavaScript 6줄로 자동 펼침"

### 작업
slide-base.html `</body>` 직전에 삽입:

```html
<script>
window.onbeforeprint = function() {
    document.querySelectorAll('details').forEach(function(d) { d.open = true; });
};
window.onafterprint = function() {
    document.querySelectorAll('details').forEach(function(d) { d.open = false; });
};
</script>
```

### 하드코딩 점검
- 없음. DOM API만 사용. 고정값 없음.

### 충돌/회귀
- 충돌: 없음. `{% endfor %}` 이후, Jinja2 루프 밖에 삽입
- 회귀: 없음. 기존 HTML에 `<details>` 없으면 querySelectorAll이 빈 NodeList → 무동작
- 다운로드 HTML: renderer.py의 CSS 인라인 삽입은 `<link>` → `<style>` 교체만. `<script>`는 그대로 포함됨 ✅

### 수정 파일
- `templates/slide-base.html`

### 의존성
- B-1 완료 후 의미 있음 (details 태그가 있어야 JS가 동작)

### 구현 결과
- slide-base.html `</body>` 직전에 `<script>` 블록 삽입
- `window.onbeforeprint`: 모든 `<details>` 요소에 `open = true` 설정 (인쇄 시 펼침)
- `window.onafterprint`: 모든 `<details>` 요소에 `open = false` 복원 (인쇄 후 접힘)
- `<details>` 태그가 없으면 `querySelectorAll` 빈 NodeList → 무동작 (기존 슬라이드에 영향 없음)
- 다운로드 HTML에도 JS 그대로 포함됨 (renderer의 CSS 인라인 처리는 `<link>` → `<style>` 교체만)

---

## B-3: catalog에 details-block 등록 ✅ 완료

### 현재 상태
- catalog.yaml에 미등록 → Sonnet(팀장)이 이 블록을 선택할 수 없음

### 작업
catalog.yaml blocks 배열에 추가:

```yaml
- id: details-block
  name: 자세히보기 (접기/펼치기)
  template: blocks/emphasis/details-block.html
  height_cost: "~60px (compact, 접힌 상태 기준. 펼치면 내용에 따라 가변)"
  visual: "접힌 요약 1줄 + 클릭하면 상세 내용 펼침. HTML 네이티브 <details> 사용."
  when: >
    너무 구체적/세부적인 내용을 접어서 보여줄 때.
    본문 흐름을 끊지 않으면서 상세 데이터를 제공할 때.
    비교표, 상세 스펙 등 detail_target 꼭지.
  not_for: >
    본문 핵심 내용 (접으면 안 됨).
    결론이나 강조 메시지 (항상 보여야 함).
    일반 텍스트 (topic-header 또는 card 사용).
  slots:
    required: [summary_text, detail_content]
    optional: [label]
  character_limits:
    summary_text: 60
    detail_content: 500
    label: 10
```

### 하드코딩 점검
- height_cost: 접힌 상태의 사실적 높이. AI가 zone 예산 계산에 사용하는 참고값
- character_limits: AI 참고용 가이드. 강제 아님 (CLAUDE.md: "의미 우선")

### 충돌/회귀
- 충돌: 없음. catalog에 항목 추가만
- _load_catalog_map(): id+template만 추출하므로 정상 로드
- 높이 참고표 주석(14행): compact 목록에 details-block 추가 필요

### 수정 파일
- `templates/catalog.yaml`

### 의존성
- B-1 (템플릿이 존재해야 catalog에 등록 의미 있음)

### 구현 결과
- catalog.yaml emphasis 섹션 마지막(divider-text 뒤, media 섹션 전)에 삽입
- id: `details-block`, template: `blocks/emphasis/details-block.html`
- height_cost: `compact` (접힌 상태 기준)
- when: "너무 구체적/세부적인 내용을 접어서 보여줄 때. detail_target 꼭지."
- not_for: "본문 핵심 내용 (접으면 안 됨). 결론 → conclusion-accent-bar."
- slots: `required: [summary_text, detail_content], optional: [label]`
- `_load_catalog_map()` 정상 로드 확인 (총 46개 블록)

---

## B-4 + B-5: 1단계 이미지/표 상세 판단 필드 ✅ 완료

### 현재 상태
- KEI_PROMPT 출력 형식(kei_client.py:44~53행)에 `content_type: "text|image|table|mixed"` 한 줄만
- 이미지 개수/소속/핵심여부/텍스트포함, 표 행/열 규모 등 상세 필드 없음
- CLAUDE.md: "몇 개인지, 어떤 꼭지 소속인지, 핵심/보조인지", "행/열 규모, 전체 표시 가능 여부"

### API 선택
- **Kei API** (1차). KEI_PROMPT가 Kei API로 전달됨 (kei_client.py:96행)
- Sonnet 직접이 아님 ✅

### 작업 — 3곳 동기화 필수

**1) KEI_PROMPT (kei_client.py:20~56행)**

프롬프트 본문에 이미지/표 판단 규칙 보강 + 출력 형식에 필드 추가:

현재 출력 형식:
```json
{"title": "...", "total_pages": 1, "info_structure": "...",
 "topics": [{"id": 1, ..., "content_type": "text|image|table|mixed", "detail_target": false, "page": 1}]}
```

변경:
```json
{"title": "...", "total_pages": 1, "info_structure": "...",
 "topics": [{"id": 1, ..., "content_type": "text|image|table|mixed", "detail_target": false, "page": 1}],
 "images": [{"topic_id": 1, "role": "key|supporting", "has_text": false, "description": "이미지 설명"}],
 "tables": [{"topic_id": 2, "rows": 5, "cols": 3, "fits_single_page": true, "description": "표 설명"}]}
```

**2) fallback system_prompt (kei_client.py:168~184행) — 동기화**

현재 문제:
- `role` 필드 누락 → sidebar-right 프리셋 절대 선택 안 됨
- `info_structure` 필드 누락
- `images[]`, `tables[]` 없음

→ KEI_PROMPT와 **동일한 출력 스키마**로 전면 동기화.
이것은 단발성 패치가 아니라, "같은 역할(1단계 실장)의 두 경로가 동일한 출력 구조를 사용해야 한다"는 구조적 원칙.

**3) manual_classify (kei_client.py:225~243행) — 기본값 추가**

```python
return {
    ...
    "images": [],
    "tables": [],
}
```

### 하드코딩 점검
- images[]/tables[] 필드: AI가 판단하여 채움. 스키마 정의일 뿐 고정값 아님 ✅
- "key|supporting", "true|false": AI가 선택하는 enum. 하드코딩 아님 ✅

### 하류 영향 분석 (에이전트 검증 완료)
| 모듈 | images[]/tables[] 추가 영향 |
|------|---------------------------|
| pipeline.py | `.get("topics")`, `.get("total_pages")`만 접근. 무시됨 ✅ |
| design_director.py select_preset() | topics의 role/emphasis만 사용. 무시됨 ✅ |
| design_director.py create_layout_concept() | user_prompt에 analysis 텍스트로 포함 → 이점 (Sonnet이 참고) ✅ |
| content_editor.py fill_content() | analysis 미참조 (인자로만 받음). 완전 무관 ✅ |
| pipeline.py _adjust_design() | select_preset()만 호출. 무시됨 ✅ |

### 충돌/회귀
- 충돌: 없음. 기존 필드 변경 없이 필드 추가만
- 회귀: 없음. images[]/tables[]가 없어도 하류 `.get()` 패턴으로 안전
- **기존 결함 수리 포함**: fallback에 role/info_structure 누락 문제도 함께 해결

### 수정 파일
- `src/kei_client.py` — KEI_PROMPT, fallback system_prompt, manual_classify (3곳)

### 구현 결과 — 3곳 동기화

**1) KEI_PROMPT (kei_client.py:38~40행 프롬프트 본문, 46~56행 출력 형식)**
- 프롬프트 본문: "이미지/표가 있으면 그것도 판단해줘" → 구체화
  - "이미지가 있으면: 몇 개인지, 어떤 꼭지 소속인지, 핵심인지 보조인지, 텍스트 포함 여부 판단"
  - "표가 있으면: 행/열 규모, 1페이지 전체 표시 가능 여부 판단"
  - "이미지/표 판단 결과를 images[], tables[] 배열에 기록"
- 출력 형식: topics[] 뒤에 images[], tables[] 배열 추가
  - images: `[{"topic_id": 1, "role": "key|supporting", "has_text": false, "description": "..."}]`
  - tables: `[{"topic_id": 2, "rows": 5, "cols": 3, "fits_single_page": true, "description": "..."}]`

**2) fallback system_prompt (kei_client.py:172~195행)**
- 기존 누락 필드 전부 추가: `role: "flow|reference"`, `info_structure`, `images: []`, `tables: []`
- 꼭지 추출 규칙에 "참조 정보는 role: 'reference'" 추가
- 출력 스키마가 KEI_PROMPT와 동일한 구조로 동기화
- **기존 결함 수리**: fallback에서도 sidebar-right 프리셋이 선택 가능해짐 (role 필드 존재)

**3) manual_classify (kei_client.py:238~258행)**
- `info_structure: ""` 추가
- topics[0]에 `role: "flow"` 추가
- 최상위에 `images: []`, `tables: []` 추가

---

## B-6, B-7: 해결됨 (작업 불필요)

- B-6: quote-left-border → 등록 안 함 확정 (구 블록 제거 방향)
- B-7: comparison-2col → 등록 안 함 확정 (구 블록 제거 방향)

---

## B-8: fallback_layout에서 card-grid → topic-header 교체 ✅ 완료

### 현재 상태
- `_fallback_layout()` (design_director.py:438행): `"type": "card-grid"`
- card-grid는 BLOCK_SLOTS에서 제거됨 (주석 24행), _apply_defaults에서도 제거됨, catalog에도 없음
- **현재 fallback 경로가 이미 깨져있음** (정합성 분석으로 확인)

### 작업
```python
# 변경 전
"type": "card-grid",
...
"char_guide": {"title": 20, "description": 100},

# 변경 후
"type": "topic-header",
...
# char_guide 제거 — 편집자가 자체 판단 (하드코딩 방지)
```

### topic-header 정합성 (에이전트 검증 완료)
| 체인 | 존재 여부 |
|------|:--------:|
| BLOCK_SLOTS (design_director.py:77~80) | ✅ `required: ["title", "description"]` |
| _apply_defaults (content_editor.py:257) | ✅ `{"title": "(소제목)", "description": ""}` |
| catalog.yaml | ✅ `id: topic-header, template: blocks/headers/topic-left-right.html` |
| 템플릿 파일 | ✅ `templates/blocks/headers/topic-left-right.html` 존재 |

### 하드코딩 점검
- 기존 `char_guide: {"title": 20, "description": 100}` → **제거**
- 편집자(3단계)가 가이드 없이 자체 판단 (CLAUDE.md: "글자 수 가이드는 참고. 의미 우선")
- 하드코딩 0개 ✅

### 충돌/회귀
- 충돌: 없음. fallback 블록 타입 변경만
- 회귀: 아님. **깨진 fallback을 수리하는 변경** (card-grid는 이미 체인에서 제거됨)

### 수정 파일
- `src/design_director.py` — `_fallback_layout()` (438행)

### 구현 결과
- `_fallback_layout()` 436~442행:
  - `"type": "card-grid"` → `"type": "topic-header"` 변경
  - `"char_guide": {"title": 20, "description": 100}` **완전 제거** (하드코딩 제거)
  - `"size": "medium"` 유지 (AI 판단 참고용)
- topic-header 정합성 검증 통과:
  - BLOCK_SLOTS ✅, _apply_defaults ✅, catalog ✅, 템플릿 ✅
- **깨진 fallback 수리 완료**: card-grid는 BLOCK_SLOTS/defaults/catalog 모두에서 제거된 블록이었음 → topic-header로 교체하여 전체 체인 정합

---

## 수정 파일 총괄

| 파일 | 항목 | 변경 성격 |
|------|------|----------|
| 신규 `templates/blocks/emphasis/details-block.html` | B-1 | HTML/CSS 템플릿 제작 |
| `templates/slide-base.html` | B-2 | `<script>` 6줄 추가 |
| `templates/catalog.yaml` | B-3 | details-block 항목 + 높이 참고표 업데이트 |
| `src/kei_client.py` | B-4, B-5 | KEI_PROMPT + fallback + manual_classify (3곳 동기화) |
| `src/design_director.py` | B-8 | _fallback_layout() 블록 타입 교체 + char_guide 제거 |

---

## 검증 체크리스트

- [ ] B-1: details-block.html이 `<details>/<summary>` 사용. CSS에 `var(--*)` 만 사용. `#직접값` 없음
- [ ] B-1: BLOCK_SLOTS 슬롯명(summary_text, detail_content, label)과 템플릿 변수명 일치
- [ ] B-2: 인쇄 시 details 자동 펼침. 화면에서는 접힌 상태 유지
- [ ] B-2: 다운로드 HTML에 `<script>` 포함
- [ ] B-3: catalog에 details-block 등록. _load_catalog_map()에서 정상 로드
- [ ] B-4: KEI_PROMPT에 images[] 스키마 추가. Kei API로 전달
- [ ] B-5: KEI_PROMPT에 tables[] 스키마 추가. Kei API로 전달
- [ ] B-4/B-5: fallback system_prompt에 role, info_structure, images[], tables[] 동기화
- [ ] B-4/B-5: manual_classify에 images:[], tables:[] 빈 배열 추가
- [ ] B-8: _fallback_layout()에서 "topic-header" 사용. char_guide 없음
- [ ] B-8: fallback 경로에서 topic-header 렌더링 정상 동작

---

## 수정 이력

| 날짜 | 내용 |
|------|------|
| 2026-03-25 | 초안. 하드코딩 제거 반영 (B-8 char_guide 제거, B-1 CSS 토큰 강제). fallback 동기화 추가. |
| 2026-03-25 | Phase B 전체 구현 완료. 검증 통과. |

## 구현 완료 확인

| 항목 | 검증 결과 |
|------|----------|
| B-1 | `details-block.html` 존재. `#직접값` 0개 — CSS 변수만 사용. 슬롯명(summary_text, detail_content, label) BLOCK_SLOTS와 정합 |
| B-2 | slide-base.html에 `onbeforeprint`/`onafterprint` JS 포함. `<details>` 없으면 무동작(안전) |
| B-3 | catalog에 `details-block` 등록. `_load_catalog_map()` 정상 로드 (총 46개 블록) |
| B-4 | KEI_PROMPT에 images[] 스키마 + 판단 규칙 추가 |
| B-5 | KEI_PROMPT에 tables[] 스키마 + 판단 규칙 추가 |
| B-4/5 동기화 | fallback system_prompt에 role, info_structure, images[], tables[] 동기화 완료. manual_classify에도 동기화 |
| B-8 | fallback 블록 `topic-header`. char_guide 없음(편집자 자체 판단). BLOCK_SLOTS/defaults/catalog/템플릿 전부 정합 |
