WIP on main: 1f7579c Phase W + V' 완료: before→filled→after 파이프라인 + 조립 로직 수정
This commit is contained in:
+130
-44
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user