DA-13a + DA-13b 구현: 팀장 2-Step 분리
Step A (규칙 기반, LLM 불필요):
- select_preset(): reference→sidebar-right, 비교→two-column,
고강조→hero-detail, 기본→single-column
- LAYOUT_PRESETS: 4개 프리셋 CSS grid 정의
Step B (Sonnet, 프리셋 CSS 포함):
- 프리셋의 zone에 꼭지 배정 (flow→body, reference→sidebar)
- 프리셋 CSS가 프롬프트에 포함되어 팀장이 변경 불가
- "grid를 변경하지 마라" 명시
fallback: 프리셋 기반 기본 배치 (LLM 실패 시)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,7 @@
|
|||||||
"""DA-13: 2단계 — 디자인 팀장 (레이아웃 설계).
|
"""DA-13a + DA-13b: 2단계 — 디자인 팀장.
|
||||||
|
|
||||||
실장의 꼭지 분석 결과를 받아,
|
Step A: 레이아웃 프리셋 선택 (규칙 기반, LLM 불필요)
|
||||||
각 꼭지에 적합한 블록을 매핑하고 공간 배분 + 글자 수 가이드를 결정한다.
|
Step B: 프리셋 안에서 블록 매핑 + 글자 수 가이드 (Sonnet)
|
||||||
텍스트 정리는 하지 않는다.
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -18,7 +17,9 @@ from src.config import settings
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 블록별 슬롯 정의 (content_editor, renderer에서도 참조)
|
# ──────────────────────────────────────
|
||||||
|
# 블록별 슬롯 정의
|
||||||
|
# ──────────────────────────────────────
|
||||||
BLOCK_SLOTS = {
|
BLOCK_SLOTS = {
|
||||||
"comparison": {
|
"comparison": {
|
||||||
"required": ["left_title", "left_content", "right_title", "right_content"],
|
"required": ["left_title", "left_content", "right_title", "right_content"],
|
||||||
@@ -58,100 +59,161 @@ BLOCK_SLOTS = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────
|
||||||
|
# 레이아웃 프리셋 정의
|
||||||
|
# ──────────────────────────────────────
|
||||||
|
LAYOUT_PRESETS = {
|
||||||
|
"sidebar-right": {
|
||||||
|
"description": "좌측 본문 흐름 + 우측 참조 사이드바",
|
||||||
|
"grid_areas": "'title title' 'body sidebar' 'footer footer'",
|
||||||
|
"grid_columns": "65fr 35fr",
|
||||||
|
"grid_rows": "auto 1fr auto",
|
||||||
|
"zones": {
|
||||||
|
"body": "flow 꼭지 배치 (위→아래 순서)",
|
||||||
|
"sidebar": "reference 꼭지 배치 (독립 참조)",
|
||||||
|
"footer": "결론 꼭지",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"two-column": {
|
||||||
|
"description": "대등한 2단 비교",
|
||||||
|
"grid_areas": "'title title' 'left right' 'footer footer'",
|
||||||
|
"grid_columns": "1fr 1fr",
|
||||||
|
"grid_rows": "auto 1fr auto",
|
||||||
|
"zones": {
|
||||||
|
"left": "첫 번째 비교 대상",
|
||||||
|
"right": "두 번째 비교 대상",
|
||||||
|
"footer": "결론 꼭지",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"hero-detail": {
|
||||||
|
"description": "고강조 1개 + 보조 상세",
|
||||||
|
"grid_areas": "'title title' 'hero hero' 'detail detail' 'footer footer'",
|
||||||
|
"grid_columns": "1fr 1fr",
|
||||||
|
"grid_rows": "auto 2fr 1fr auto",
|
||||||
|
"zones": {
|
||||||
|
"hero": "고강조 꼭지 (크게)",
|
||||||
|
"detail": "나머지 보조 꼭지",
|
||||||
|
"footer": "결론 꼭지",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"single-column": {
|
||||||
|
"description": "단일 컬럼 순차 배치",
|
||||||
|
"grid_areas": "'title' 'body' 'footer'",
|
||||||
|
"grid_columns": "1fr",
|
||||||
|
"grid_rows": "auto 1fr auto",
|
||||||
|
"zones": {
|
||||||
|
"body": "모든 꼭지 위→아래 순서",
|
||||||
|
"footer": "결론 꼭지",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────
|
||||||
|
# Step A: 프리셋 선택 (규칙 기반)
|
||||||
|
# ──────────────────────────────────────
|
||||||
|
def select_preset(analysis: dict[str, Any]) -> str:
|
||||||
|
"""실장의 role 분석을 보고 레이아웃 프리셋을 자동 선택한다.
|
||||||
|
|
||||||
|
LLM 호출 불필요. 규칙 기반.
|
||||||
|
"""
|
||||||
|
topics = analysis.get("topics", [])
|
||||||
|
|
||||||
|
has_reference = any(
|
||||||
|
t.get("role") == "reference" for t in topics
|
||||||
|
)
|
||||||
|
flow_topics = [t for t in topics if t.get("role", "flow") == "flow"]
|
||||||
|
high_emphasis = [t for t in flow_topics if t.get("emphasis")]
|
||||||
|
|
||||||
|
# reference 꼭지가 있으면 sidebar
|
||||||
|
if has_reference:
|
||||||
|
preset = "sidebar-right"
|
||||||
|
# flow 꼭지가 정확히 2개이고 대등 비교이면 two-column
|
||||||
|
elif (
|
||||||
|
len(flow_topics) == 2
|
||||||
|
and all(t.get("layer") == "core" for t in flow_topics)
|
||||||
|
):
|
||||||
|
preset = "two-column"
|
||||||
|
# 고강조 1개 + 나머지가 보조이면 hero
|
||||||
|
elif (
|
||||||
|
len(high_emphasis) == 1
|
||||||
|
and len(flow_topics) >= 3
|
||||||
|
):
|
||||||
|
preset = "hero-detail"
|
||||||
|
# 기본: single-column
|
||||||
|
else:
|
||||||
|
preset = "single-column"
|
||||||
|
|
||||||
|
logger.info(f"[Step A] 프리셋 선택: {preset}")
|
||||||
|
return preset
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────
|
||||||
|
# Step B: 프리셋 내 블록 매핑 (Sonnet)
|
||||||
|
# ──────────────────────────────────────
|
||||||
def _load_catalog() -> str:
|
def _load_catalog() -> str:
|
||||||
"""catalog.yaml이 있으면 로드하여 프롬프트용 텍스트 반환. 없으면 기본 블록 목록."""
|
"""catalog.yaml 로드."""
|
||||||
catalog_path = Path(__file__).parent.parent / "templates" / "catalog.yaml"
|
catalog_path = Path(__file__).parent.parent / "templates" / "catalog.yaml"
|
||||||
if catalog_path.exists():
|
if catalog_path.exists():
|
||||||
return catalog_path.read_text(encoding="utf-8")
|
return catalog_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
# fallback: 기본 블록 목록
|
|
||||||
return """사용 가능한 블록:
|
return """사용 가능한 블록:
|
||||||
- quote-block: 좌측 컬러 라인 + 인용 텍스트. 문제 제기, 핵심 주장할 때.
|
- quote-block: 좌측 컬러 라인 + 인용 텍스트. 문제 제기할 때.
|
||||||
- card-grid: 2~4열 카드. 용어 정의, 개념 나열할 때.
|
- card-grid: 2~4열 카드. 용어 정의, 개념 나열할 때.
|
||||||
- comparison: 2단 병렬. A vs B 비교할 때.
|
- comparison: 2단 병렬. A vs B 비교할 때.
|
||||||
- comparison-table: 다항목 비교 테이블. 행/열 많을 때.
|
- comparison-table: 다항목 비교 테이블.
|
||||||
- relationship: 벤 다이어그램. 포함/상위-하위 관계할 때.
|
- relationship: 벤 다이어그램. 포함/상위-하위 관계할 때.
|
||||||
- process: 단계 흐름. 절차, 워크플로우할 때.
|
- process: 단계 흐름. 절차할 때.
|
||||||
- conclusion-bar: 하단 결론 바. 핵심 한 줄.
|
- conclusion-bar: 하단 결론 바."""
|
||||||
- image-block: 이미지 + 캡션. full(전체너비)/side(텍스트옆)/thumb(썸네일) 3변형.
|
|
||||||
- details-block: 자세히보기. 요약 표면 + 펼치면 상세."""
|
|
||||||
|
|
||||||
|
|
||||||
DIRECTOR_PROMPT = """당신은 디자인 팀장이다. 실장이 분석한 꼭지 목록을 받아 레이아웃을 설계한다.
|
STEP_B_PROMPT = """당신은 디자인 팀장이다. 레이아웃 프리셋이 이미 선택되었다. 당신은 프리셋 안에서 블록을 배정하기만 하면 된다.
|
||||||
|
|
||||||
|
## 선택된 레이아웃 프리셋: {preset_name}
|
||||||
|
{preset_description}
|
||||||
|
|
||||||
|
### CSS Grid (변경하지 마라):
|
||||||
|
grid-template-areas: {grid_areas}
|
||||||
|
grid-template-columns: {grid_columns}
|
||||||
|
grid-template-rows: {grid_rows}
|
||||||
|
|
||||||
|
### Zone 설명:
|
||||||
|
{zone_descriptions}
|
||||||
|
|
||||||
## 역할
|
## 역할
|
||||||
- 실장의 info_structure(정보 구조)와 각 꼭지의 role(flow/reference)을 **반드시 존중**한다
|
- 각 꼭지를 위 zone 중 하나에 배정한다
|
||||||
- 각 꼭지에 적합한 블록을 매핑한다
|
- flow 꼭지 → body/main/left/hero zone
|
||||||
- 전체 공간을 배분하고 겹침을 방지한다
|
- reference 꼭지 → sidebar zone
|
||||||
- 각 블록의 글자 수 가이드를 결정한다
|
- detail_target 꼭지 → 생략 (popup으로 분리, 현재 미구현)
|
||||||
- **텍스트는 절대 정리하지 않는다** (텍스트 편집자가 별도로 한다)
|
- conclusion 꼭지 → footer zone
|
||||||
|
- 각 꼭지에 적합한 블록 타입을 catalog에서 선택한다
|
||||||
## 정보 구조 기반 배치 (가장 중요한 규칙)
|
- 같은 내용이 두 블록에 중복되면 안 된다
|
||||||
실장이 각 꼭지에 role을 부여했다. 이 role에 따라 배치 영역이 결정된다:
|
- 각 블록의 대략적 글자 수 가이드를 제시한다
|
||||||
- **role: "flow"** (본문 흐름) → 좌측 또는 메인 영역에 배치. 위→아래 순서대로.
|
|
||||||
- **role: "reference"** (참조 정보) → 우측 사이드 영역에 독립 배치. 본문 흐름과 분리.
|
|
||||||
- **detail_target: true** (상세 내용) → 본문에 넣지 않는다. popup/자세히보기로 분리.
|
|
||||||
|
|
||||||
배치 예시:
|
|
||||||
- 본문 흐름(flow) 꼭지 3개 + 참조(reference) 꼭지 1개 → 좌측에 flow 3개, 우측에 reference 1개
|
|
||||||
- 모든 꼭지가 flow → 단일 컬럼 또는 균등 분할
|
|
||||||
- detail_target 꼭지 → 해당 블록에 연결된 별도 영역 (현재 블록 없으면 생략)
|
|
||||||
|
|
||||||
## 중복 방지 규칙
|
|
||||||
- 같은 내용이 두 개 블록에 나오면 안 된다
|
|
||||||
- 예: 용어 정의가 카드에도 있고 비교 블록에도 있으면 → 하나만 선택
|
|
||||||
- 블록 타입이 다르더라도 같은 내용이면 중복
|
|
||||||
|
|
||||||
## {catalog}
|
## {catalog}
|
||||||
|
|
||||||
## 이미지 처리 규칙
|
|
||||||
- 원본 이미지를 그대로 사용한다 (crop 안 함, 크기만 조절)
|
|
||||||
- 가로형 이미지(비율 > 1.2) → 전체 너비(image-full)
|
|
||||||
- 세로형 이미지(비율 < 0.8) → 텍스트 옆(image-side)
|
|
||||||
- 텍스트 포함 도표 → 너무 작게 축소하면 안 됨
|
|
||||||
|
|
||||||
## 표 처리 규칙
|
|
||||||
- 표는 표로 유지한다 (다른 형태로 전환하지 않음)
|
|
||||||
- 공간에 안 들어가면 → 요약 요청 또는 페이지 분리
|
|
||||||
|
|
||||||
## 자세히보기 규칙
|
|
||||||
- detail_target: true인 꼭지는 본문에 넣지 않는다
|
|
||||||
- 관련된 블록 근처에 popup/링크로 연결
|
|
||||||
|
|
||||||
## 공간 배분 규칙
|
|
||||||
- CSS grid-template-areas 형식으로 배치
|
|
||||||
- 영역명: header, left, right, center, main, footer 등
|
|
||||||
- flow 꼭지는 좌측/메인, reference 꼭지는 우측/사이드
|
|
||||||
- 꼭지끼리 겹치지 않도록 설계
|
|
||||||
- 각 블록에 대략적 크기 감(small/medium/large) 제시
|
|
||||||
|
|
||||||
## 글자 수 가이드 규칙
|
|
||||||
- 블록의 공간에 따라 대략적 글자 수 가이드를 제시
|
|
||||||
- 이것은 하드코딩 기준이 아니라 참고 가이드
|
|
||||||
- 텍스트 편집자가 의미를 우선하여 가이드와 다를 수 있음
|
|
||||||
|
|
||||||
## 출력 형식 (반드시 JSON만. 설명 없이.)
|
## 출력 형식 (반드시 JSON만. 설명 없이.)
|
||||||
|
grid_areas, grid_columns, grid_rows는 위에 정해진 것을 그대로 사용한다.
|
||||||
```json
|
```json
|
||||||
{{
|
{{{{
|
||||||
"pages": [
|
"pages": [
|
||||||
{{
|
{{{{
|
||||||
"grid_areas": "'header header' 'left right' 'footer footer'",
|
"grid_areas": "{grid_areas}",
|
||||||
"grid_columns": "1fr 1fr",
|
"grid_columns": "{grid_columns}",
|
||||||
"grid_rows": "auto 1fr auto",
|
"grid_rows": "{grid_rows}",
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{{
|
{{{{
|
||||||
"area": "header",
|
"area": "zone이름",
|
||||||
"type": "quote-block",
|
"type": "블록타입",
|
||||||
"topic_id": 1,
|
"topic_id": 1,
|
||||||
"reason": "문제 제기 꼭지",
|
"reason": "이유",
|
||||||
"size": "small",
|
"size": "small|medium|large",
|
||||||
"char_guide": {{"quote_text": 80, "source": 30}}
|
"char_guide": {{{{"slot": 글자수}}}}
|
||||||
}}
|
}}}}
|
||||||
]
|
]
|
||||||
}}
|
}}}}
|
||||||
]
|
]
|
||||||
}}
|
}}}}
|
||||||
```"""
|
```"""
|
||||||
|
|
||||||
|
|
||||||
@@ -159,58 +221,63 @@ async def create_layout_concept(
|
|||||||
content: str,
|
content: str,
|
||||||
analysis: dict[str, Any],
|
analysis: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""2단계: 디자인 팀장이 레이아웃 컨셉을 설계한다.
|
"""2단계: Step A(프리셋) + Step B(블록 매핑).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content: 원본 텍스트 (분량 참고용)
|
content: 원본 텍스트
|
||||||
analysis: 1단계 실장의 꼭지 분석 결과
|
analysis: 1단계 실장의 꼭지 분석 결과
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
레이아웃 컨셉:
|
레이아웃 컨셉 JSON
|
||||||
{"title": "...", "pages": [{"grid_areas": "...", "blocks": [...]}]}
|
|
||||||
"""
|
"""
|
||||||
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
|
# Step A: 프리셋 선택 (규칙 기반)
|
||||||
|
preset_name = select_preset(analysis)
|
||||||
|
preset = LAYOUT_PRESETS[preset_name]
|
||||||
|
|
||||||
|
# Step B: 프리셋 내 블록 매핑 (Sonnet)
|
||||||
|
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
|
||||||
catalog_text = _load_catalog()
|
catalog_text = _load_catalog()
|
||||||
|
|
||||||
# 꼭지 요약 (role과 detail_target 포함)
|
# zone 설명 텍스트
|
||||||
|
zone_desc = "\n".join(
|
||||||
|
f"- {name}: {desc}" for name, desc in preset["zones"].items()
|
||||||
|
)
|
||||||
|
|
||||||
|
# 꼭지 요약
|
||||||
topics_summary = []
|
topics_summary = []
|
||||||
for t in analysis.get("topics", []):
|
for t in analysis.get("topics", []):
|
||||||
role = t.get("role", "flow")
|
role = t.get("role", "flow")
|
||||||
line = (
|
line = (
|
||||||
f"꼭지 {t['id']}: {t['title']} "
|
f"꼭지 {t.get('id', '?')}: {t.get('title', '?')} "
|
||||||
f"[{t.get('layer', '?')}, ROLE:{role}, "
|
f"[{t.get('layer', '?')}, ROLE:{role}, "
|
||||||
f"강조:{t.get('emphasis', False)}, "
|
f"강조:{t.get('emphasis', False)}]"
|
||||||
f"방향:{t.get('direction', '?')}, 유형:{t.get('content_type', 'text')}]"
|
|
||||||
)
|
)
|
||||||
if t.get("detail_target"):
|
if t.get("detail_target"):
|
||||||
line += " → ★자세히보기 대상 (본문에 넣지 마라)"
|
line += " → ★detail_target (생략)"
|
||||||
if t.get("image_info"):
|
|
||||||
line += f" 이미지:{t['image_info']}"
|
|
||||||
if t.get("table_info"):
|
|
||||||
line += f" 표:{t['table_info']}"
|
|
||||||
if t.get("detail_target"):
|
|
||||||
line += " → 자세히보기 대상"
|
|
||||||
topics_summary.append(line)
|
topics_summary.append(line)
|
||||||
|
|
||||||
system = DIRECTOR_PROMPT.replace("{catalog}", catalog_text)
|
system = STEP_B_PROMPT.format(
|
||||||
|
preset_name=preset_name,
|
||||||
|
preset_description=preset["description"],
|
||||||
|
grid_areas=preset["grid_areas"],
|
||||||
|
grid_columns=preset["grid_columns"],
|
||||||
|
grid_rows=preset["grid_rows"],
|
||||||
|
zone_descriptions=zone_desc,
|
||||||
|
catalog=catalog_text,
|
||||||
|
)
|
||||||
|
|
||||||
info_structure = analysis.get("info_structure", "정보 구조 미분석")
|
info_structure = analysis.get("info_structure", "")
|
||||||
|
|
||||||
user_prompt = (
|
user_prompt = (
|
||||||
f"## 실장 분석 결과\n"
|
f"## 실장 분석 결과\n"
|
||||||
f"제목: {analysis.get('title', '')}\n"
|
f"제목: {analysis.get('title', '')}\n"
|
||||||
f"페이지 수: {analysis.get('total_pages', 1)}\n"
|
|
||||||
f"정보 구조: {info_structure}\n\n"
|
f"정보 구조: {info_structure}\n\n"
|
||||||
f"꼭지 목록:\n" + "\n".join(topics_summary) +
|
f"꼭지 목록:\n" + "\n".join(topics_summary) +
|
||||||
f"\n\n## 원본 콘텐츠 (분량 참고)\n{content[:2000]}\n\n"
|
f"\n\n## 원본 콘텐츠 (분량 참고)\n{content[:2000]}\n\n"
|
||||||
f"## 요청\n"
|
f"## 요청\n"
|
||||||
f"위 꼭지를 어떤 블록으로, 어디에 배치할지 설계해줘.\n"
|
f"위 꼭지를 프리셋의 zone에 배정하고 블록 타입을 선택해줘.\n"
|
||||||
f"반드시 각 꼭지의 ROLE(flow/reference)에 따라 영역을 배정해라.\n"
|
f"grid_areas/columns/rows는 위에 정해진 것을 그대로 써라. 변경하지 마라.\n"
|
||||||
f"flow → 좌측/메인, reference → 우측/사이드.\n"
|
f"JSON만."
|
||||||
f"detail_target → 본문에 넣지 마라.\n"
|
|
||||||
f"같은 내용이 두 블록에 중복되면 안 된다.\n"
|
|
||||||
f"텍스트는 채우지 마. 구조만 JSON으로."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -225,35 +292,50 @@ async def create_layout_concept(
|
|||||||
concept = _parse_json(result_text)
|
concept = _parse_json(result_text)
|
||||||
|
|
||||||
if concept and "pages" in concept:
|
if concept and "pages" in concept:
|
||||||
total_blocks = sum(len(p.get("blocks", [])) for p in concept["pages"])
|
total_blocks = sum(
|
||||||
|
len(p.get("blocks", [])) for p in concept["pages"]
|
||||||
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"레이아웃 설계 완료: {len(concept['pages'])}페이지, "
|
f"[Step B] 블록 매핑 완료: {preset_name}, "
|
||||||
f"{total_blocks}개 블록"
|
f"{len(concept['pages'])}페이지, {total_blocks}개 블록"
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"title": analysis.get("title", "슬라이드"),
|
"title": analysis.get("title", "슬라이드"),
|
||||||
**concept,
|
**concept,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
logger.warning("레이아웃 설계 파싱 실패. fallback 사용.")
|
logger.warning("블록 매핑 JSON 파싱 실패. fallback.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"디자인 팀장 호출 실패: {e}", exc_info=True)
|
logger.error(f"Step B 호출 실패: {e}", exc_info=True)
|
||||||
|
|
||||||
# fallback
|
# fallback: 프리셋 기반 기본 배치
|
||||||
return _fallback_layout(analysis)
|
return _fallback_layout(analysis, preset_name, preset)
|
||||||
|
|
||||||
|
|
||||||
def _fallback_layout(analysis: dict[str, Any]) -> dict[str, Any]:
|
def _fallback_layout(
|
||||||
"""팀장 실패 시 기본 레이아웃."""
|
analysis: dict[str, Any],
|
||||||
|
preset_name: str,
|
||||||
|
preset: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Step B 실패 시 프리셋 기반 기본 배치."""
|
||||||
blocks = []
|
blocks = []
|
||||||
areas = ["header", "main", "footer"]
|
for topic in analysis.get("topics", []):
|
||||||
for i, topic in enumerate(analysis.get("topics", [])[:3]):
|
if topic.get("detail_target"):
|
||||||
area = areas[min(i, len(areas) - 1)]
|
continue
|
||||||
|
|
||||||
|
role = topic.get("role", "flow")
|
||||||
|
if role == "reference" and preset_name == "sidebar-right":
|
||||||
|
area = "sidebar"
|
||||||
|
elif topic.get("layer") == "conclusion":
|
||||||
|
area = "footer"
|
||||||
|
else:
|
||||||
|
area = "body" if preset_name != "two-column" else "left"
|
||||||
|
|
||||||
blocks.append({
|
blocks.append({
|
||||||
"area": area,
|
"area": area,
|
||||||
"type": "card-grid",
|
"type": "card-grid",
|
||||||
"topic_id": topic.get("id", i + 1),
|
"topic_id": topic.get("id", 0),
|
||||||
"reason": topic.get("title", ""),
|
"reason": topic.get("title", ""),
|
||||||
"size": "medium",
|
"size": "medium",
|
||||||
"char_guide": {"title": 20, "description": 100},
|
"char_guide": {"title": 20, "description": 100},
|
||||||
@@ -262,9 +344,9 @@ def _fallback_layout(analysis: dict[str, Any]) -> dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
"title": analysis.get("title", "슬라이드"),
|
"title": analysis.get("title", "슬라이드"),
|
||||||
"pages": [{
|
"pages": [{
|
||||||
"grid_areas": "'header' 'main' 'footer'",
|
"grid_areas": preset["grid_areas"],
|
||||||
"grid_columns": "1fr",
|
"grid_columns": preset["grid_columns"],
|
||||||
"grid_rows": "auto 1fr auto",
|
"grid_rows": preset["grid_rows"],
|
||||||
"blocks": blocks,
|
"blocks": blocks,
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ async def generate_slide(
|
|||||||
page_count = analysis.get("total_pages", 1)
|
page_count = analysis.get("total_pages", 1)
|
||||||
logger.info(f"1단계 완료: {topic_count}개 꼭지, {page_count}페이지")
|
logger.info(f"1단계 완료: {topic_count}개 꼭지, {page_count}페이지")
|
||||||
|
|
||||||
# 2단계: 디자인 팀장 — 레이아웃 설계
|
# 2단계: 디자인 팀장 — Step A(프리셋) + Step B(블록 매핑)
|
||||||
yield {"event": "progress", "data": "2/5 디자인 팀장이 레이아웃을 설계 중..."}
|
yield {"event": "progress", "data": "2/5 디자인 팀장이 레이아웃을 설계 중..."}
|
||||||
|
|
||||||
layout_concept = await create_layout_concept(content, analysis)
|
layout_concept = await create_layout_concept(content, analysis)
|
||||||
|
|||||||
Reference in New Issue
Block a user