diff --git a/PHASE-X-B.md b/PHASE-X-B.md new file mode 100644 index 0000000..c6dcce0 --- /dev/null +++ b/PHASE-X-B.md @@ -0,0 +1,131 @@ +# Phase X-B: 유형 B 템플릿 추가 + +> 작성일: 2026-04-06 +> 전제: 유형 A(배경+본심+첨부+결론) 기존 코드 건드리지 않음 + +--- + +## 유형 B 구조 + +02번 MDX (DX의 시행 목표 및 기대효과) 기준: + +``` +┌──────────────────────────────────────────┐ +│ 제목 │ +├───────────────────────┬──────────────────┤ +│ 본심1: DX 궁극적 목표 │ │ +│ ┌안전과 품질──────────┐│ [그림 2] │ +│ │• 불릿 ││ 궁극적 목표 │ +│ ├생산성 향상──────────┤│ 이미지 │ +│ │• 불릿 ││ (비율 맞춤) │ +│ ├소통과 신뢰──────────┤│ │ +│ │• 불릿 ││ │ +│ └────────────────────┘│ │ +├───────────┬───────────┴──────────────────┤ +│ 본심2-좌 │ 본심2-우 │ +│ 프로세스 │ 주체별 기대효과 │ +│ 변화 │ [팝업→] │ +│ • 불릿 │ • 발주자: ... │ +│ • 불릿 │ • 시공자: ... │ +│ │ • 설계자: ... │ +├───────────┴──────────────────────────────┤ +│ 결론 배너 │ +└──────────────────────────────────────────┘ +``` + +핵심: +- 상단: 텍스트(좌) + 이미지(우) 나란히 +- 하단: 2분할 +- 배경 없음, 첨부(sidebar) 없음 +- 이미지는 원본 위치에서 가져와서 컨테이너 크기에 맞게 비율 조정 +- 결론 항상 있음 +- PNG 구조를 따르되 디자인은 그대로 따라가기 + +--- + +## 진행 현황 + +### X-B-1: KEI_PROMPT에 유형 B 옵션 추가 — ✅ 완료 +- 기존 유형 A 지시 유지 +- 유형 B 옵션 추가: "배경/첨부가 없는 콘텐츠는 본심1+본심2+결론 구조" +- Kei가 콘텐츠를 보고 A 또는 B 선택 +- 검증 결과: + - 01번 MDX → A 선택 ✅ + - 02번 MDX → B 선택 ✅ (역할: DX의_궁극적_목표/프로세스_혁신/주체별_기대효과/결론) + - 하드코딩 없음 ✅ +- 파일: `src/kei_client.py` — KEI_PROMPT 수정 + +### X-B-2: 검증기(validators.py) 완화 — ✅ 완료 +- 유형 A: "본심" 필수 유지 +- 유형 B: 결론(footer) 필수, 본심 불필요, 자유 역할명 허용 +- 섹션 수 차이 허용: 유형 A는 2, 유형 B는 4 +- 하드코딩 없음 ✅ +- 파일: `src/validators.py` + +### X-B-3: space_allocator에 유형 B 컨테이너 생성 — 미착수 +- 유형 A: 기존 `calculate_container_specs` 그대로 +- 유형 B: `build_containers_type_b()` 추가 + - 상단: 전체폭, weight 비율 + - 하단: 2분할, weight 비율 + - 이미지: 상단 우측, 실제 비율로 크기 계산 + - 결론: footer +- 파일: `src/space_allocator.py` + +### X-B-4: assemble_stage2에 유형 B 조립 로직 추가 — 미착수 +- 유형 A: 기존 코드 그대로 +- 유형 B: 새 조립 함수 + - 상단: 좌측 텍스트(카테고리별 카드) + 우측 이미지 + - 하단: 2분할 불릿 + - 이미지 아래 캡션, 팝업 링크 우측상단 + - 결론 배너 + - 좌표: 동적 계산 +- 파일: `scripts/assemble_stage2.py` + +### X-B-5: pipeline.py 분기 — 미착수 +- Stage 1.5a에서 `layout_template`에 따라 분기 + - "A" 또는 없음 → 기존 파이프라인 + - "B" → `build_containers_type_b()` +- Stage 1A에서 `layout_template`를 Analysis에 저장 (pipeline_context.py에 필드 추가 필요) +- 파일: `src/pipeline.py`, `src/pipeline_context.py` + +### X-B-6: 검증 — 미착수 +- 02번 MDX → 유형 B 선택 → 상단+이미지+하단2분할+결론 +- 01번 MDX → 유형 A 선택 → 기존과 동일 (깨지면 안 됨) +- 텍스트 컨테이너 안에 있음 +- 이미지 비율 맞음 +- 공란 최소 +- 하드코딩 없음 + +--- + +## Kei 유형 선택 결과 (검증 완료) + +### 01번 MDX → 유형 A +``` +layout_template: A +page_structure: + 배경: topics=[1], weight=0.20 + 본심: topics=[2,3,4], weight=0.55 + 첨부: topics=[5,6], weight=0.15 + 결론: topics=[7], weight=0.10 +``` + +### 02번 MDX → 유형 B +``` +layout_template: B +page_structure: + DX의_궁극적_목표: zone=top, topics=[1], weight=0.45 + 프로세스_혁신: zone=bottom_left, topics=[2], weight=0.25 + 주체별_기대효과: zone=bottom_right, topics=[3], weight=0.20 + 결론: zone=footer, topics=[4], weight=0.10 +``` + +--- + +## 주의사항 + +- 유형 A 코드 건드리지 않음 +- 유형 B는 별도 함수/분기로 추가 +- 01번이 깨지면 롤백 (git: c9677a6) +- 하드코딩 절대 금지 +- 각 단계 완료 후 반드시 검증 (하드코딩, 모면용 코드, 기존 깨짐 여부) diff --git a/src/kei_client.py b/src/kei_client.py index 3bef83c..830a69f 100644 --- a/src/kei_client.py +++ b/src/kei_client.py @@ -30,15 +30,30 @@ KEI_PROMPT = ( "## 3단계: 슬라이드 스토리라인 설계\n" "핵심 메시지를 전달하기 위한 **흐름**을 설계해줘.\n" "각 꼭지에 purpose를 부여하고, topics 배열에 기록.\n\n" - "## 4단계: 페이지 구조 판단 (비중 시스템)\n" - "콘텐츠를 분석하여 이 페이지의 **구조와 비중**을 판단하라:\n\n" - "- **본심**: 이 페이지가 말하려는 핵심. 가장 큰 공간을 차지해야 함.\n" - " 비교라면 비교표, 관계라면 관계도, 프로세스라면 흐름도로 구조화.\n" - " 비교 구조일 때 비교 목적(왜 비교하는가)을 summary에 명시.\n" - "- **배경**: 본심을 이해하기 위한 도입/배경. 간결하게. 2-3줄이면 충분.\n" - "- **첨부**: 본심을 보조하는 참조 정보 (용어 정의 등). sidebar 배치.\n" - " role: 'reference'로 표시. 본문 흐름을 방해하지 않도록.\n" - "- **결론**: 절대 잊으면 안 되는 핵심 한 줄. footer.\n\n" + "## 4단계: 레이아웃 유형 선택 + 페이지 구조 판단\n" + "먼저 콘텐츠에 맞는 **레이아웃 유형**을 선택하라:\n\n" + "### 유형 A: 배경 + 본심 + 첨부(sidebar) + 결론\n" + "- 참조자료(용어 정의, 부록 등)가 **별도로 존재**하는 콘텐츠\n" + "- 좌측 body(배경+본심) + 우측 sidebar(첨부) + 하단 결론\n" + "- page_structure 키: 배경, 본심, 첨부, 결론\n\n" + "### 유형 B: 본심1(상단) + 본심2(하단 2분할) + 결론\n" + "- 참조자료 없이 **본문 흐름만**으로 구성되는 콘텐츠\n" + "- 배경/첨부가 없거나 억지로 만들어야 하면 이 유형 선택\n" + "- 상단: 핵심 내용 전체폭 (이미지가 있으면 좌텍스트+우이미지 나란히)\n" + "- 하단: 세부 내용 2분할 (좌/우)\n" + "- page_structure 키: 자유 (예: 핵심목표, 프로세스변화, 기대효과, 결론)\n" + "- 결론 키는 반드시 '결론'\n\n" + "선택한 유형을 **layout_template** 필드에 'A' 또는 'B'로 기록하라.\n\n" + "### 역할별 규칙 (유형 A)\n" + "- **본심**: 이 페이지가 말하려는 핵심. 가장 큰 공간.\n" + "- **배경**: 본심을 이해하기 위한 도입. 간결하게.\n" + "- **첨부**: 본심을 보조하는 참조 정보. sidebar 배치. role: 'reference'.\n" + "- **결론**: 핵심 한 줄. footer.\n\n" + "### 역할별 규칙 (유형 B)\n" + "- 상단 역할: 핵심 내용. 전체폭. zone: 'top'\n" + "- 하단 좌측: zone: 'bottom_left'\n" + "- 하단 우측: zone: 'bottom_right'\n" + "- 결론: zone: 'footer'\n\n" "각 역할에 해당하는 topic_ids와 **공간 비중(weight, 합계 1.0)**을 결정하라.\n" "**콘텐츠에 따라 비중은 매번 달라진다. 고정값이 아니다.**\n" "page_structure 필드에 기록.\n\n" @@ -56,11 +71,13 @@ KEI_PROMPT = ( "- 1페이지 적정 꼭지: 5개. 분량 적으면 1페이지로.\n" "- **슬라이드 제목(title)과 첫 번째 꼭지 제목은 달라야 한다.** 슬라이드 제목은 전체 주제, 꼭지 제목은 해당 위치의 구체적 내용.\n\n" "## 출력 형식 (JSON만)\n" + "layout_template에 따라 page_structure가 달라진다.\n\n" + "유형 A 예시:\n" "```json\n" '{"title": "제목", ' - '"core_message": "이 슬라이드의 핵심 메시지 한 줄", ' + '"core_message": "핵심 메시지", ' '"total_pages": 1, ' - '"info_structure": "정보 구조 설명", ' + '"layout_template": "A", ' '"page_structure": {' '"본심": {"topic_ids": [2, 3], "weight": 0.60}, ' '"배경": {"topic_ids": [1], "weight": 0.15}, ' @@ -79,6 +96,20 @@ KEI_PROMPT = ( '"images": [{"topic_id": 1, "role": "key|supporting", "has_text": false, "description": "이미지 설명"}], ' '"tables": [{"topic_id": 2, "rows": 5, "cols": 3, "fits_single_page": true, "description": "표 설명"}]}\n' "```\n\n" + "유형 B 예시:\n" + "```json\n" + '{"title": "제목", ' + '"core_message": "핵심 메시지", ' + '"total_pages": 1, ' + '"layout_template": "B", ' + '"page_structure": {' + '"핵심목표": {"zone": "top", "topic_ids": [1], "weight": 0.35}, ' + '"프로세스변화": {"zone": "bottom_left", "topic_ids": [2], "weight": 0.25}, ' + '"기대효과": {"zone": "bottom_right", "topic_ids": [3], "weight": 0.25}, ' + '"결론": {"zone": "footer", "topic_ids": [4], "weight": 0.15}}, ' + '"topics": [...],' + '"images": [...]}\n' + "```\n\n" "## 콘텐츠:\n" ) diff --git a/src/validators.py b/src/validators.py index f7a29ab..f3fa40d 100644 --- a/src/validators.py +++ b/src/validators.py @@ -171,22 +171,38 @@ def validate_stage_1a( "instruction": f"weight 합이 1.0에 가깝도록 조정하라. 현재 합: {total_weight:.2f}", }) - # 본심 존재 + 본심 weight ≥ 0.3 - core_info = page_struct.get("본심", {}) - if not core_info or not isinstance(core_info, dict): - errors.append({ - "severity": "RETRYABLE", - "field": "page_structure.본심", - "localization": "본심 역할이 page_structure에 없음", - "instruction": "page_structure에 본심 역할을 추가하라. 본심은 슬라이드의 핵심 콘텐츠이다.", - }) - elif core_info.get("weight", 0) < 0.3: - errors.append({ - "severity": "RETRYABLE", - "field": "page_structure.본심.weight", - "localization": f"본심 weight {core_info['weight']:.2f} < 0.3", - "instruction": "본심은 슬라이드의 핵심. weight 0.3 이상 필요.", - }) + # 유형에 따른 구조 검증 + layout_template = analysis.get("layout_template", "A") + if layout_template == "A": + # 유형 A: 본심 필수 + core_info = page_struct.get("본심", {}) + if not core_info or not isinstance(core_info, dict): + errors.append({ + "severity": "RETRYABLE", + "field": "page_structure.본심", + "localization": "본심 역할이 page_structure에 없음", + "instruction": "page_structure에 본심 역할을 추가하라. 본심은 슬라이드의 핵심 콘텐츠이다.", + }) + elif core_info.get("weight", 0) < 0.3: + errors.append({ + "severity": "RETRYABLE", + "field": "page_structure.본심.weight", + "localization": f"본심 weight {core_info['weight']:.2f} < 0.3", + "instruction": "본심은 슬라이드의 핵심. weight 0.3 이상 필요.", + }) + elif layout_template == "B": + # 유형 B: 결론(footer) 필수, 나머지 자유 + has_footer = any( + isinstance(info, dict) and info.get("zone") == "footer" + for info in page_struct.values() + ) + if not has_footer and "결론" not in page_struct: + errors.append({ + "severity": "RETRYABLE", + "field": "page_structure.footer", + "localization": "결론(footer) 역할이 없음", + "instruction": "유형 B에서도 결론 역할(zone: footer)은 필수이다.", + }) # 필수 필드 검증 for t in topics: @@ -226,7 +242,9 @@ def validate_stage_1a( if clean_text: # 원본 ## 섹션 수 vs topic 수 비교 original_sections = re.findall(r"^## .+$", clean_text, re.MULTILINE) - if len(original_sections) > 0 and abs(len(topics) - len(original_sections)) > 2: + # 유형 B에서는 하나의 섹션을 여러 꼭지로 나눌 수 있으므로 허용 폭 확대 + max_diff = 4 if layout_template == "B" else 2 + if len(original_sections) > 0 and abs(len(topics) - len(original_sections)) > max_diff: errors.append({ "severity": "RETRYABLE", "field": "topics",