WIP on main: 1f7579c Phase W + V' 완료: before→filled→after 파이프라인 + 조립 로직 수정

This commit is contained in:
2026-04-06 07:23:35 +09:00
13 changed files with 1036 additions and 383 deletions
+130 -44
View File
@@ -130,7 +130,7 @@ def assemble(run_dir: str):
return p
return None
def popup_to_compact_table(popup_content, font_size):
def popup_to_compact_table(popup_content, font_size, role_name=""):
"""팝업의 마크다운 표를 compact HTML 테이블로 변환."""
# 마크다운 bold → HTML (팝업 정화가 안 된 run 대응)
popup_content = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', popup_content)
@@ -164,7 +164,7 @@ def assemble(run_dir: str):
align = "center" if ci == len(row) // 2 else ("left" if ci == 0 else "right")
weight = "600" if ci == 0 else "400"
color = "#1e40af" if ci == 0 else "#64748b"
cells_html += f'<div style="padding:4px 8px;font-size:{font_size-2}px;color:{color};font-weight:{weight};text-align:{align};">{bold(cell, "본심")}</div>'
cells_html += f'<div style="padding:4px 8px;font-size:{font_size-2}px;color:{color};font-weight:{weight};text-align:{align};">{bold(cell, role_name)}</div>'
rows_html += f'<div style="display:grid;grid-template-columns:repeat({col_count},1fr);border-top:1px solid #e2e8f0;background:{bg};align-items:center;">{cells_html}</div>\n'
return (
@@ -189,8 +189,8 @@ def assemble(run_dir: str):
all_css = set()
role_htmls = {}
# ── 각 역할별 조립 ──
for role in ["배경", "본심", "첨부", "결론"]:
# ── 각 역할별 조립 (동적 — page_structure의 모든 역할) ──
for role in ps.keys():
info = ps.get(role, {})
if not isinstance(info, dict):
continue
@@ -209,8 +209,14 @@ def assemble(run_dir: str):
ci = containers.get(role, {})
h = int(redist.get(role, ci.get("height_px", 0)))
w = ci.get("width_px", 0)
font_key = {"배경": "bg", "본심": "core", "첨부": "sidebar", "결론": "key_msg"}.get(role, "core")
font_size = fh.get(font_key, 12)
# font_size: zone 기반 동적 결정
zone = ci.get("zone", "") if isinstance(ci, dict) else ""
if zone == "footer":
font_size = fh.get("key_msg", 14)
elif zone == "sidebar":
font_size = fh.get("sidebar", 10)
else:
font_size = fh.get("core", 12)
block_css, block_body = extract_block_html(ref_html)
if block_css:
@@ -241,7 +247,8 @@ def assemble(run_dir: str):
# ════════════════════════════════════
# 결론
# ════════════════════════════════════
if role == "결론":
# Phase X: zone 기반 분기 (역할명이 아닌 zone/블록타입으로)
if zone == "footer":
assembled = block_body
assembled = re.sub(r'>핵심 메시지 한 줄<', f'>{bold(core_message, role)}<', assembled)
assembled = re.sub(r'>부연 설명<', '><', assembled)
@@ -250,7 +257,7 @@ def assemble(run_dir: str):
# ════════════════════════════════════
# 첨부 — structured_text의 주불릿(•) = 카드 제목, 하위불릿( •) = 카드 설명
# ════════════════════════════════════
elif role == "첨부":
elif "block-card-num" in block_body: # 카드 넘버링 블록
st = get_text(primary_topic)
# 마크다운 bold → HTML
st = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', st)
@@ -321,7 +328,7 @@ def assemble(run_dir: str):
# ════════════════════════════════════
# 배경 — callout 구조 + 종속꼭지 인라인 + 강조
# ════════════════════════════════════
elif role == "배경":
elif "block-callout-warn" in block_body or "block-callout-sol" in block_body: # callout 블록
sub_html = ""
if is_hier and sup_tids:
for st_id in sup_tids:
@@ -371,7 +378,7 @@ def assemble(run_dir: str):
# ════════════════════════════════════
# 본심 — SVG(좌) + 텍스트(우상) + 비교표(우하) + key-msg(하단)
# ════════════════════════════════════
elif role == "본심":
elif any(sc.get("name") == "svg" for sc in scs): # 이미지 포함 블록
svg_sc = next((sc for sc in scs if sc["name"] == "svg"), None)
text_sc = next((sc for sc in scs if sc["name"] == "text_and_table"), None)
keymsg_sc = next((sc for sc in scs if sc["name"] == "keymsg"), None)
@@ -461,7 +468,7 @@ def assemble(run_dir: str):
if popup:
content = popup.get("content", "")
if content.count("|") > 3:
compact = popup_to_compact_table(content, font_size)
compact = popup_to_compact_table(content, font_size, role_name=role)
if compact:
popup_link_html = f'<div style="text-align:right;margin-bottom:2px;"><span style="color:#2563eb;font-size:{font_size-2}px;cursor:pointer;">[{pr}→]</span></div>'
table_html += f'<div style="margin-top:{int(font_size*0.5)}px;">{popup_link_html}{compact}</div>'
@@ -502,23 +509,120 @@ def assemble(run_dir: str):
f'{keymsg_html}</div>'
)
# ── 슬라이드 좌표 ──
bg_h = int(redist.get("배경", containers.get("배경", {}).get("height_px", 0)))
core_h = int(redist.get("본심", containers.get("본심", {}).get("height_px", 0)))
sb_h = int(redist.get("첨부", containers.get("첨부", {}).get("height_px", 0)))
concl_h = int(redist.get("결론", containers.get("결론", {}).get("height_px", 0)))
else:
# 범용 텍스트 조립 (블록 타입 무관)
all_text = "\n".join(get_text(topic_map.get(tid, {})) for tid in tids if topic_map.get(tid))
bullets_html, _ = structured_to_bullets(all_text, role, font_size)
role_htmls[role] = (
f'<div style="height:100%;padding:{int(font_size * 0.5)}px;font-size:{font_size}px;line-height:1.4;">'
f'<div style="font-weight:700;font-size:{font_size+1}px;margin-bottom:4px;">{topic_title}</div>'
f'{bullets_html}</div>'
)
bg_top = pad + header_h + gap_block
core_top = bg_top + bg_h + gap_small
sb_top = bg_top
# ── Phase X: 동적 좌표 계산 (block_assembler와 동일 로직) ──
# zone별로 역할 그룹핑
zone_roles = {}
footer_role = None
for role_name in ps.keys():
ci = containers.get(role_name, {})
z = ci.get("zone", "") if isinstance(ci, dict) else ""
if z == "footer":
footer_role = role_name
else:
if z not in zone_roles:
zone_roles[z] = []
zone_roles[z].append(role_name)
# #9: 결론 바로 위까지 body/sidebar 모두 채움 — 공란 제거
ft_top = slide_h - pad - concl_h - gap_block # 결론 위치: 슬라이드 바닥 - pad - 결론높이 - gap
column_bottom = ft_top - gap_block # body/sidebar 바닥: 결론 위 gap만큼 위
core_h = column_bottom - core_top # 본심: 배경 아래~column 바닥
sb_h = column_bottom - sb_top # 첨부: column 바닥까지
# row 그룹핑
row_map = {}
for zone_name in zone_roles:
if zone_name.startswith("top"):
row = "top"
elif zone_name.startswith("bottom"):
row = "bottom"
else:
row = "main"
if row not in row_map:
row_map[row] = []
if zone_name not in row_map[row]:
row_map[row].append(zone_name)
# footer 높이
footer_h = 0
if footer_role:
ci = containers.get(footer_role, {})
footer_h = int(redist.get(footer_role, ci.get("height_px", 0)))
ft_top = slide_h - pad - footer_h
content_top = pad + header_h + gap_block
content_bottom = ft_top - gap_block
# row별 높이
row_weights = {}
for row_name, zone_list in row_map.items():
w = 0
for zn in zone_list:
for rn in zone_roles.get(zn, []):
w += containers.get(rn, {}).get("weight", 0) if isinstance(containers.get(rn), dict) else 0
row_weights[row_name] = w
total_rw = sum(row_weights.values()) or 1
num_rows = len(row_map)
row_gap_total = gap_small * max(0, num_rows - 1)
available_h = content_bottom - content_top - row_gap_total
row_layout = {}
current_top = content_top
for row_name in sorted(row_map.keys()):
rw = row_weights.get(row_name, 1)
rh = int(available_h * rw / total_rw)
row_layout[row_name] = {"top": current_top, "height": rh}
current_top += rh + gap_small
# 컨테이너 좌표
container_boxes = {}
_color_palette = ["#dc2626", "#2563eb", "#16a34a", "#7c3aed", "#d97706", "#0891b2", "#be185d", "#4f46e5"]
all_role_names = list(ps.keys())
for row_name, zone_list in row_map.items():
rl = row_layout[row_name]
num_cols = len(zone_list)
col_gap_total = gap_block * max(0, num_cols - 1)
col_available = inner_w - col_gap_total
zone_widths = []
for zn in zone_list:
roles_in_zone = zone_roles.get(zn, [])
if roles_in_zone:
w = containers.get(roles_in_zone[0], {}).get("width_px", col_available // num_cols) if isinstance(containers.get(roles_in_zone[0]), dict) else col_available // num_cols
else:
w = col_available // num_cols
zone_widths.append(w)
total_zw = sum(zone_widths) or 1
zone_widths = [int(col_available * w / total_zw) for w in zone_widths]
current_left = pad
for i, zn in enumerate(zone_list):
zw = zone_widths[i]
for rn in zone_roles.get(zn, []):
container_boxes[rn] = {"left": current_left, "top": rl["top"], "width": zw, "height": rl["height"]}
current_left += zw + gap_block
if footer_role:
container_boxes[footer_role] = {"left": pad, "top": ft_top, "width": inner_w, "height": footer_h}
# HTML 조립
css_block = "\n".join(all_css)
containers_html = ""
for rn in all_role_names:
box = container_boxes.get(rn)
if not box:
continue
idx = all_role_names.index(rn) % len(_color_palette)
color = _color_palette[idx]
containers_html += (
f'\n<div style="position:absolute;left:{box["left"]}px;top:{box["top"]}px;'
f'width:{box["width"]}px;height:{box["height"]}px;border-radius:6px;overflow:hidden;">'
f'{role_htmls.get(rn, "")}</div>\n'
)
html = f"""<!DOCTYPE html><html><head><meta charset="UTF-8">
<style>
@@ -528,28 +632,10 @@ body{{background:#e5e5e5;padding:10px;font-family:'Pretendard Variable','Noto Sa
.bl-sub{{padding-left:1em;}}
{css_block}
</style></head><body>
<div style="font-size:14px;font-weight:bold;margin-bottom:4px;">Stage 2: 코드 조립 결과 (context 데이터만, Sonnet 없음)</div>
<div style="font-size:11px;color:#666;margin-bottom:10px;">sub_layouts + design_reference_html + structured_text + V-7~V-10 + popups</div>
<div style="font-size:14px;font-weight:bold;margin-bottom:4px;">Stage 2: 코드 조립 결과</div>
<div style="width:{slide_w}px;height:{slide_h}px;background:white;position:relative;border:1px solid #ccc;">
<div style="position:absolute;left:{pad}px;top:{pad}px;width:{inner_w}px;height:{header_h}px;background:#f8fafc;border-bottom:3px solid #2563eb;display:flex;align-items:center;padding:0 20px;font-size:22px;font-weight:900;color:#1e293b;">{title}</div>
<div style="position:absolute;left:{pad}px;top:{bg_top}px;width:{body_w}px;height:{bg_h}px;border-radius:6px;overflow:hidden;">
{role_htmls.get("배경", "")}
</div>
<div style="position:absolute;left:{pad}px;top:{core_top}px;width:{body_w}px;height:{core_h}px;border-radius:6px;overflow:hidden;">
{role_htmls.get("본심", "")}
</div>
<div style="position:absolute;left:{pad + body_w + gap_block}px;top:{sb_top}px;width:{sidebar_w}px;height:{sb_h}px;border-radius:6px;overflow:hidden;">
{role_htmls.get("첨부", "")}
</div>
<div style="position:absolute;left:{pad}px;top:{ft_top}px;width:{inner_w}px;height:{concl_h}px;border-radius:8px;overflow:hidden;">
{role_htmls.get("결론", "")}
</div>
{containers_html}
</div></body></html>"""
out = run / "steps" / "stage_2_code_assembled.html"