5단계 파이프라인 전면 재작성 + Figma 추출 계획 업데이트

DA-12: 1단계 Kei 실장 — 꼭지 2~5개 추출 + 레이어/강조/배치/이미지/표/자세히보기 판단
DA-13: 2단계 디자인 팀장 — catalog 연동 + 블록 매핑 + 공간 배분 + 글자 수 가이드
DA-13b: 3단계 텍스트 편집자 — 글자 수 가이드 참고, 의미 우선 편집 + 자세히보기(요약+상세)
DA-14: 4단계 실무자(AI+코드) + 5단계 팀장 재검토 (균형 점검 → 2차 조정)

문서:
- CLAUDE.md: 5단계 프로세스 + 이미지/표/자세히보기 처리 원칙
- PLAN.md: DA-12~14 태스크 전면 재작성
- PROGRESS.md: 동기화
- FIGMA-COMPONENT-EXTRACTION-PLAN.md: 모드 독립 블록, 변환 규칙, image-block/details-block, MCP, 토큰 매핑

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 08:44:10 +09:00
parent 9a780828df
commit 33bd3a56c6
9 changed files with 1015 additions and 397 deletions

View File

@@ -1,8 +1,7 @@
"""DA-12: Kei API 연동 + Opus 직접 분류.
"""DA-12: 1단계 — Kei 실장 (꼭지 추출 + 분석).
1차: Opus API를 직접 호출하여 콘텐츠 유형을 분한다 (안정적).
2차: Kei API 연동은 향후 RAG 통합 시 활용.
Opus 실패 시: 수동 분류 fallback.
본문에서 핵심 꼭지를 추출하고, 각 꼭지의 레이어/강조/배치 방향을 분한다.
이미지/표/상세 콘텐츠도 판단한다.
"""
from __future__ import annotations
@@ -17,45 +16,56 @@ from src.config import settings
logger = logging.getLogger(__name__)
CLASSIFICATION_PROMPT = """당신은 콘텐츠를 분석하여 슬라이드 레이아웃을 결정하는 실장이다.
CLASSIFICATION_PROMPT = """당신은 콘텐츠를 분석하여 슬라이드 구조를 설계하는 실장이다.
## 사용 가능한 블록 타입
- comparison: 2단 병렬 비교 (A vs B)
- card-grid: 카드 배열 (용어 정의, 개념 설명)
- relationship: 벤 다이어그램 (상위/하위, 포함 관계)
- process: 단계 흐름 (절차, 워크플로우)
- quote-block: 강조 인용 (문제 제기, 핵심 메시지)
- conclusion-bar: 결론 바 (핵심 한 줄)
- comparison-table: 다항목 비교 테이블
## 역할
본문에서 핵심 꼭지를 추출하고, 각 꼭지의 성격을 분석하여 슬라이드 구조를 설계한다.
## 배치 영역
grid-template-areas로 정의. 사용 가능한 영역명: header, left, right, center, main, footer
## 꼭지 추출 규칙
- 본문에서 2~5개의 핵심 꼭지(파트)를 추출한다
- 1페이지 적정 꼭지 수: 5개
- 꼭지가 5개를 넘고 중요도가 동등하면 → 2페이지로 분리 (의미 기반 분할)
- 5개인데 내용이 많으면 → 세부 내용은 "자세히보기" 대상으로 표시
## 규칙
- 콘텐츠를 분석하여 각 덩어리의 유형을 판단한다
- 한 슬라이드에 블록 4~6개가 적절하다
- 정보 계층: 위→아래 (문제 제기 → 분석 → 결론)
- 반드시 JSON으로만 응답한다. 설명 없이 JSON만.
## 각 꼭지 분석 항목
1. **레이어 수준**: 도입(문제 제기, 배경) / 핵심(핵심 내용, 정의) / 보조(사례, 근거) / 결론(요약, 핵심 메시지)
2. **강조**: 눈에 띄게 해야 하는 꼭지 표시 (true/false)
3. **배치 방향**: 세로로 긴 내용(vertical) / 가로로 나열(horizontal) / 유연(flexible)
4. **콘텐츠 유형**: text(텍스트) / image(이미지) / table(표) / mixed(혼합)
5. **이미지 정보** (이미지가 있는 경우):
- 핵심인지 보조인지 (core/supplementary)
- 텍스트 포함 여부 (도표/차트는 true)
6. **표 정보** (표가 있는 경우):
- 대략적 행/열 수
- 전체 표시 가능한지 판단
7. **자세히보기 대상**: 너무 구체적/세부적인 내용은 detail_target: true
## 출력 형식
## 출력 형식 (반드시 JSON만. 설명 없이.)
```json
{
"title": "슬라이드 제목",
"grid_areas": "'header header' 'left right' 'footer footer'",
"grid_columns": "1fr 1fr",
"grid_rows": "auto 1fr auto",
"blocks": [
{"area": "header", "type": "quote-block", "reason": "문제"},
{"area": "left", "type": "comparison", "reason": "정책 비교"},
{"area": "right", "type": "card-grid", "reason": "용어 정의 3개"},
{"area": "footer", "type": "conclusion-bar", "reason": "핵심 결론"}
"total_pages": 1,
"topics": [
{
"id": 1,
"title": "꼭지",
"summary": "꼭지 내용 요약 (1~2줄)",
"layer": "intro|core|supporting|conclusion",
"emphasis": true,
"direction": "vertical|horizontal|flexible",
"content_type": "text|image|table|mixed",
"image_info": {"role": "core|supplementary", "has_text": true},
"table_info": {"rows": 5, "cols": 3, "fits_page": true},
"detail_target": false,
"page": 1
}
]
}
```"""
async def classify_content(content: str) -> dict[str, Any] | None:
"""Opus API를 직접 호출하여 콘텐츠를한다.
"""1단계: 본문에서 꼭지를 추출하고한다.
Args:
content: 원본 텍스트 콘텐츠
@@ -75,34 +85,38 @@ async def classify_content(content: str) -> dict[str, Any] | None:
max_tokens=2048,
system=CLASSIFICATION_PROMPT,
messages=[
{"role": "user", "content": f"다음 콘텐츠의 레이아웃을 결정해줘:\n\n{content}"}
{
"role": "user",
"content": f"다음 콘텐츠를 분석하여 꼭지를 추출하고 구조를 설계해줘:\n\n{content}",
}
],
)
result_text = response.content[0].text
layout = _parse_layout_json(result_text)
analysis = _parse_json(result_text)
if layout and "blocks" in layout:
if analysis and "topics" in analysis:
logger.info(
f"콘텐츠 분류 완료: {layout.get('title', 'untitled')}, "
f"{len(layout['blocks'])}블록"
f"꼭지 추출 완료: {analysis.get('title', 'untitled')}, "
f"{len(analysis['topics'])}꼭지, "
f"{analysis.get('total_pages', 1)}페이지"
)
return layout
return analysis
else:
logger.warning(f"분류 JSON 파싱 실패. 응답: {result_text[:200]}")
return None
except Exception as e:
logger.warning(f"Opus 분류 호출 실패: {e}")
logger.warning(f"실장 분류 호출 실패: {e}")
return None
def _parse_layout_json(text: str) -> dict[str, Any] | None:
"""텍스트에서 레이아웃 JSON을 추출한다."""
def _parse_json(text: str) -> dict[str, Any] | None:
"""텍스트에서 JSON을 추출한다."""
patterns = [
r'```json\s*(.*?)```',
r'```\s*(.*?)```',
r'(\{.*\})',
r"```json\s*(.*?)```",
r"```\s*(.*?)```",
r"(\{.*\})",
]
for pattern in patterns:
match = re.search(pattern, text, re.DOTALL)
@@ -115,27 +129,21 @@ def _parse_layout_json(text: str) -> dict[str, Any] | None:
def manual_classify(content: str) -> dict[str, Any]:
"""Opus 실패 시 기본 레이아웃을 반환하는 fallback."""
"""실장 분류 실패 시 기본 구조를 반환하는 fallback."""
return {
"title": "슬라이드",
"grid_areas": "'header' 'main' 'footer'",
"grid_columns": "1fr",
"grid_rows": "auto 1fr auto",
"blocks": [
"total_pages": 1,
"topics": [
{
"area": "header",
"type": "quote-block",
"reason": "기본 인용 블록",
},
{
"area": "main",
"type": "card-grid",
"reason": "기본 카드 그리드",
},
{
"area": "footer",
"type": "conclusion-bar",
"reason": "기본 결론",
"id": 1,
"title": "핵심 내용",
"summary": content[:100],
"layer": "core",
"emphasis": False,
"direction": "flexible",
"content_type": "text",
"detail_target": False,
"page": 1,
},
],
}