5단계 AI 파이프라인: 1. Kei 실장(Opus via Kei API) — 꼭지 추출 + 정보 구조 파악 2. 디자인 팀장 — FAISS 블록 검색 + Opus 추천 + Sonnet 블록 매핑 3. Kei 편집자(Kei API) — 도메인 전문 텍스트 정리 4. 디자인 실무자(Sonnet + Jinja2) — CSS 변수 조정 + HTML 조립 5. 디자인 팀장(Sonnet) — 균형 재검토 (최대 2회 루프) 블록 라이브러리 46개 (6 카테고리) + _legacy 13개 FAISS 블록 검색 (bge-m3, 1024차원) SVG N개 동적 배치 (cos/sin 좌표 계산) Pillow 이미지 크기 측정 + base64 인라인 컨테이너 예산 기반 블록 배치 (zone별 높이 px) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 KiB
15 KiB
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 신규 제작
구조:
<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> 직전에 삽입:
<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 배열에 추가:
- 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행)
프롬프트 본문에 이미지/표 판단 규칙 보강 + 출력 형식에 필드 추가:
현재 출력 형식:
{"title": "...", "total_pages": 1, "info_structure": "...",
"topics": [{"id": 1, ..., "content_type": "text|image|table|mixed", "detail_target": false, "page": 1}]}
변경:
{"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행) — 기본값 추가
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:3840행 프롬프트 본문, 4656행 출력 형식)
- 프롬프트 본문: "이미지/표가 있으면 그것도 판단해줘" → 구체화
- "이미지가 있으면: 몇 개인지, 어떤 꼭지 소속인지, 핵심인지 보조인지, 텍스트 포함 여부 판단"
- "표가 있으면: 행/열 규모, 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": "..."}]
- images:
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 경로가 이미 깨져있음 (정합성 분석으로 확인)
작업
# 변경 전
"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/템플릿 전부 정합 |