Phase X'-1~5 완료: 제목/들여쓰기/캡션/빈칸/카드 디자인

X'-1: 제목 원본 MDX frontmatter에서 가져오기 (Kei가 바꾸지 않음)
X'-2: 들여쓰기 계층 (소제목→불릿 indent 적용)
X'-3: 이미지 캡션 normalized.images alt text에서 추출
X'-4: 상단 컨테이너 justify-content:space-between
X'-5: 카드 디자인 다크 그라데이션 + 밝은 텍스트

X'-6 미완료: 본문 표(팝업 아닌)를 하단 우측에 Kei 요약 배치 → 다음 세션

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 11:40:53 +09:00
parent c4d7212ff3
commit 56fd9fa71e
2 changed files with 53 additions and 24 deletions

View File

@@ -726,33 +726,51 @@ def _assemble_type_b(run: Path, ctx: dict):
if current_section[0] or current_section[1]: if current_section[0] or current_section[1]:
sections.append(current_section) sections.append(current_section)
# 카드형 HTML 생성 # X'-2: 카드형 HTML — 소제목별 들여쓰기 계층
# X'-5: 카드 디자인 — 다크 그라데이션 배경, 밝은 텍스트
_card_colors = [
("linear-gradient(135deg, #1a365d, #2d3748)", "#e2e8f0"),
("linear-gradient(135deg, #1e3a2f, #2d4a3e)", "#e2e8f0"),
("linear-gradient(135deg, #3b1f2b, #4a2d3b)", "#e2e8f0"),
("linear-gradient(135deg, #2d2b55, #3d3b65)", "#e2e8f0"),
]
card_pad = int(font_size * 0.6)
card_gap = max(3, int(font_size * 0.4))
indent_body = int(font_size * 1.2) # 본문 들여쓰기
bullets = "" bullets = ""
if len(sections) > 1 and sections[0][0]: if len(sections) > 1 and sections[0][0]:
# 소제목이 있는 경우 → 카드형 for ci, (sec_title, sec_items) in enumerate(sections):
card_gap = max(3, int(font_size * 0.4)) bg, text_color = _card_colors[ci % len(_card_colors)]
for sec_title, sec_items in sections:
items_html = "".join( items_html = "".join(
f'<div class="bl" style="font-size:{font_size}px;"><span class="bl-m">•</span><span class="bl-t">{item}</span></div>' f'<div style="padding-left:{indent_body}px;margin-bottom:1px;">'
f'<span style="color:{text_color};font-size:{font_size-1}px;line-height:1.5;">• {item}</span></div>'
for item in sec_items for item in sec_items
) )
if sec_title: if sec_title:
bullets += ( bullets += (
f'<div style="background:#1e293b;color:#fff;border-radius:4px;' f'<div style="background:{bg};border-radius:{int(font_size*0.4)}px;'
f'padding:{int(font_size*0.4)}px {int(font_size*0.6)}px;margin-bottom:{card_gap}px;">' f'padding:{card_pad}px {int(card_pad*1.5)}px;margin-bottom:{card_gap}px;">'
f'<div style="font-size:{font_size}px;font-weight:700;color:#fbbf24;margin-bottom:2px;">{bold(sec_title, rn)}</div>' f'<div style="font-size:{font_size}px;font-weight:700;color:#fbbf24;'
f'<div style="font-size:{font_size-1}px;line-height:1.5;">{items_html}</div></div>\n' f'margin-bottom:{int(font_size*0.3)}px;">{bold(sec_title, rn)}</div>'
f'{items_html}</div>\n'
) )
else: else:
bullets += items_html bullets += items_html
else: else:
# 소제목 없는 경우 → 일반 불릿
for sec_title, sec_items in sections: for sec_title, sec_items in sections:
for item in sec_items: for item in sec_items:
bullets += f'<div class="bl" style="font-size:{font_size}px;"><span class="bl-m">•</span><span class="bl-t">{item}</span></div>\n' bullets += (
f'<div style="padding-left:{indent_body}px;margin-bottom:1px;">'
f'<span style="font-size:{font_size}px;">• {item}</span></div>\n'
)
# 이미지 캡션: 출처 → [이미지:] 마커 → 없으면 빈 문자열 # X'-3: 이미지 캡션 — normalized.images alt → 출처 → [이미지:] 마커
img_caption = "" img_caption = ""
norm_images = ctx.get("normalized", {}).get("images", [])
if norm_images:
img_caption = norm_images[0].get("alt", "")
if not img_caption:
for line in all_text.split("\n"): for line in all_text.split("\n"):
stripped = line.strip().lstrip("") stripped = line.strip().lstrip("")
if stripped.startswith("출처:"): if stripped.startswith("출처:"):
@@ -778,11 +796,13 @@ def _assemble_type_b(run: Path, ctx: dict):
primary_topic = topic_map.get(tids[0], {}) if tids else {} primary_topic = topic_map.get(tids[0], {}) if tids else {}
topic_title = bold(primary_topic.get("title", ""), rn) topic_title = bold(primary_topic.get("title", ""), rn)
# X'-4: 상단 컨테이너 — 내용을 전체 높이에 균등 배분
top_html = ( top_html = (
f'<div style="position:relative;height:100%;padding:{gap_small}px;box-sizing:border-box;">' f'<div style="position:relative;height:100%;padding:{gap_small}px;box-sizing:border-box;'
f'display:flex;flex-direction:column;justify-content:space-between;">'
f'{popup_html}' f'{popup_html}'
f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{topic_title}</div>' f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{topic_title}</div>'
f'<div style="display:flex;gap:{max(6, int(font_size*0.8))}px;align-items:flex-start;">' f'<div style="display:flex;gap:{max(6, int(font_size*0.8))}px;align-items:flex-start;flex:1;">'
f'<div style="flex:1;font-size:{font_size}px;line-height:1.55;color:#333;">{bullets}</div>' f'<div style="flex:1;font-size:{font_size}px;line-height:1.55;color:#333;">{bullets}</div>'
f'{img_block}</div></div>' f'{img_block}</div></div>'
) )
@@ -798,19 +818,26 @@ def _assemble_type_b(run: Path, ctx: dict):
primary_topic = topic_map.get(tids[0], {}) if tids else {} primary_topic = topic_map.get(tids[0], {}) if tids else {}
topic_title = bold(primary_topic.get("title", ""), rn) topic_title = bold(primary_topic.get("title", ""), rn)
# X'-2: 들여쓰기 계층 (소제목+불릿)
bl_indent = int(font_size * 1.2)
bullets = "" bullets = ""
for line in all_text.split("\n"): for line in all_text.split("\n"):
stripped = line.strip() stripped = line.strip()
if not stripped or re.search(r'\[팝업:|\[이미지:', stripped): if not stripped or re.search(r'\[팝업:|\[이미지:', stripped):
continue continue
if stripped.startswith("### "):
# 소제목
sub_title = stripped.lstrip("# ").strip()
bullets += f'<div style="font-weight:700;font-size:{font_size}px;color:#1e40af;margin-top:{int(font_size*0.4)}px;">{bold(sub_title, rn)}</div>\n'
else:
clean = stripped.lstrip("") clean = stripped.lstrip("")
clean = bold(clean, rn) clean = bold(clean, rn)
bullets += f'<div class="bl" style="font-size:{font_size}px;"><span class="bl-m">•</span><span class="bl-t">{clean}</span></div>\n' bullets += f'<div style="padding-left:{bl_indent}px;font-size:{font_size}px;margin-bottom:1px;">• {clean}</div>\n'
bl_html = ( bl_html = (
f'<div style="height:100%;padding:{gap_small}px;box-sizing:border-box;">' f'<div style="height:100%;padding:{gap_small}px;box-sizing:border-box;">'
f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{topic_title}</div>' f'<div style="font-weight:700;font-size:{font_size+1}px;color:#1a365d;margin-bottom:4px;">{topic_title}</div>'
f'<div style="font-size:{font_size}px;line-height:1.55;color:#333;">{bullets}</div></div>' f'<div style="line-height:1.55;color:#333;">{bullets}</div></div>'
) )
# 하단 우측 # 하단 우측

View File

@@ -172,9 +172,11 @@ async def generate_slide(
page_struct_raw = analysis_raw.get("page_structure", {}) page_struct_raw = analysis_raw.get("page_structure", {})
page_structure = PageStructure(roles=page_struct_raw) page_structure = PageStructure(roles=page_struct_raw)
# X'-1: 제목은 원본 MDX frontmatter에서 가져옴 (Kei가 바꾸지 않음)
original_title = context.normalized.title or analysis_raw.get("title", "")
analysis = Analysis( analysis = Analysis(
core_message=analysis_raw.get("core_message", ""), core_message=analysis_raw.get("core_message", ""),
title=analysis_raw.get("title", ""), title=original_title,
total_pages=analysis_raw.get("total_pages", 1), total_pages=analysis_raw.get("total_pages", 1),
layout_template=analysis_raw.get("layout_template", "A"), layout_template=analysis_raw.get("layout_template", "A"),
) )