diff --git a/scripts/assemble_stage2.py b/scripts/assemble_stage2.py
index e4c501c..2658e25 100644
--- a/scripts/assemble_stage2.py
+++ b/scripts/assemble_stage2.py
@@ -877,11 +877,50 @@ def _assemble_type_b(run: Path, ctx: dict):
clean = bold(clean, rn)
bullets += f'
•{clean}
\n'
+ # X'-6: 본문 표 요약이 있으면 하단 우측에 추가
+ table_summaries = enh.get("table_summaries", {})
+ table_html_br = ""
+ for ts_key, ts_data in table_summaries.items():
+ fmt = ts_data.get("format", "text")
+ if fmt == "table":
+ cols = ts_data.get("columns", [])
+ data = ts_data.get("data", [])
+ col_count = len(cols)
+ if col_count > 0 and data:
+ header_cells = "".join(
+ f'{c}
'
+ for c in cols
+ )
+ rows_html = ""
+ for ri, row in enumerate(data):
+ bg = "#f8fafc" if ri % 2 == 0 else "#fff"
+ cells = ""
+ for ci, cell in enumerate(row):
+ c_color = "#1e40af" if ci == 0 else "#475569"
+ c_weight = "600" if ci == 0 else "400"
+ cells += f'{bold(str(cell), rn)}
'
+ rows_html += f'{cells}
\n'
+ table_html_br = (
+ f''
+ f'
{header_cells}
'
+ f'{rows_html}
'
+ )
+ elif fmt == "bullets":
+ items = ts_data.get("items", [])
+ table_html_br = "".join(
+ f'• {bold(str(item), rn)}
'
+ for item in items
+ )
+ elif fmt == "text":
+ table_html_br = f'{bold(str(ts_data.get("summary", "")), rn)}
'
+
br_html = (
- f''
+ f'
'
f'{popup_html_br}'
f'
{topic_title}
'
- f'
{bullets}
'
+ f'
{bullets}
'
+ f'{table_html_br}
'
)
# 결론
diff --git a/src/pipeline.py b/src/pipeline.py
index 8480c35..7ce8674 100644
--- a/src/pipeline.py
+++ b/src/pipeline.py
@@ -780,6 +780,41 @@ async def generate_slide(
popup_summaries[pr] = summary
logger.info(f"[V'-2] {pr}: format={summary.get('format')}")
+ # X'-6: 본문 표 요약 (유형 B — normalized.tables가 있으면)
+ table_summaries = {}
+ norm_tables = context.normalized.tables or []
+ if norm_tables and context.analysis.layout_template == "B":
+ from src.kei_client import call_kei_summarize_popup
+ for ti, table_data in enumerate(norm_tables):
+ headers = table_data.get("headers", [])
+ rows = table_data.get("rows", [])
+ if not headers or not rows:
+ continue
+ # 표를 마크다운 형태로 변환하여 Kei에게 전달
+ md_table = "| " + " | ".join(headers) + " |\n"
+ md_table += "| " + " | ".join(["---"] * len(headers)) + " |\n"
+ for row in rows:
+ md_table += "| " + " | ".join(str(c) for c in row) + " |\n"
+
+ # 하단 우측 공간 계산
+ bottom_roles = [r for r, ci in updated_containers.items() if ci.zone in ("bottom_left", "bottom_right")]
+ if bottom_roles:
+ br_ci = next((ci for r, ci in updated_containers.items() if ci.zone == "bottom_right"), None)
+ if br_ci:
+ available_h = br_ci.height_px - 30 # 제목 + padding
+ available_w = br_ci.width_px
+ fs = font_h.get("core", 12)
+ summary = await call_kei_summarize_popup(
+ popup_title=f"본문표{ti+1}",
+ popup_content=md_table,
+ available_width_px=available_w,
+ available_height_px=available_h,
+ font_size=fs,
+ )
+ if summary:
+ table_summaries[f"table_{ti}"] = summary
+ logger.info(f"[X'-6] 본문표{ti+1}: format={summary.get('format')}")
+
# 결과를 context에 저장 (Stage 2에서 사용)
return {
"containers": updated_containers,
@@ -811,6 +846,7 @@ async def generate_slide(
"emphasis_blocks": enhancements.emphasis_blocks,
"bold_keywords": enhancements.bold_keywords,
"popup_summaries": popup_summaries,
+ "table_summaries": table_summaries,
},
}