"""Stage별 실제 출력 데이터로 step HTML 생성. 각 step은 이전 step 위에 레이어를 쌓아가는 구조: - Step 0: Kei 꼭지 (테이블) - Step 1: 빈 컨테이너 (1280x720 슬라이드) - Step 2: Step 1 + 블록 선택 (컨테이너 안에 블록 표시) - Step 3: Step 2 + 재배분 반영 (크기 변경 + 보강) - Step 4: 최종 결과물 (final.html) """ import json import sys from pathlib import Path def _load(run: Path, name: str) -> dict: return json.loads((run / name).read_text(encoding="utf-8")) def _colors(): return {"배경": "#dc2626", "본심": "#2563eb", "첨부": "#16a34a", "결론": "#7c3aed"} def _calc_coords(containers, ratio, pad=40, gap=20, header_h=66): """컨테이너 좌표 계산. containers dict에서 실제 px 값 사용.""" inner_w = 1280 - pad * 2 body_w = int(inner_w * ratio[0] / 100) sidebar_w = inner_w - body_w - gap sidebar_left = pad + body_w + gap def get(c, key): return c.get(key, 0) if isinstance(c, dict) else getattr(c, key, 0) bg_px = get(containers.get("배경", {}), "height_px") core_px = get(containers.get("본심", {}), "height_px") sidebar_px = get(containers.get("첨부", {}), "height_px") footer_px = get(containers.get("결론", {}), "height_px") bg_top = pad + header_h + gap core_top = bg_top + bg_px + 8 footer_top = max(core_top + core_px, bg_top + sidebar_px) + gap return { "header": {"left": pad, "top": pad, "width": inner_w, "height": header_h}, "배경": {"left": pad, "top": bg_top, "width": body_w, "height": bg_px}, "본심": {"left": pad, "top": core_top, "width": body_w, "height": core_px}, "첨부": {"left": sidebar_left, "top": bg_top, "width": sidebar_w, "height": sidebar_px}, "결론": {"left": pad, "top": footer_top, "width": inner_w, "height": footer_px}, } def _box_html(coord, role, label, colors, extra_style=""): c = colors.get(role, "#333") return ( f'
' f'{label}
\n' ) def _header_html(coord, title): return ( f'
' f'{title}
\n' ) def _slide_wrap(title, subtitle, body): return f"""
{title}
{subtitle}
{body}
""" def gen_step0(run: Path, out: Path): ctx1b = _load(run, "stage_1b_context.json") topics = ctx1b.get("topics", []) ps = ctx1b.get("page_structure", {}).get("roles", {}) role_map = {} for role, info in ps.items(): for tid in info.get("topic_ids", []): role_map[tid] = role colors = _colors() rows = "" for t in topics: tid = t.get("id") role = role_map.get(tid, "?") c = colors.get(role, "#333") bg = "#f8fafc" if tid % 2 == 0 else "#fff" rows += (f'{tid}' f'{t.get("title","")}' f'{t.get("purpose","")}' f'{t.get("layer","")}' f'{t.get("relation_type","")}' f'{role}\n') html = f"""
Step 0: Kei 꼭지 추출 (Stage 1A/1B)
run: {run.name}
{rows}
ID제목purposelayerrelation_type영역
""" (out / "step0_kei_topics.html").write_text(html, encoding="utf-8") print("step0 생성") def gen_step1(run: Path, out: Path): """Step 1: 빈 컨테이너.""" ctx15a = _load(run, "stage_1_5a_context.json") containers = ctx15a.get("containers", {}) ratio = ctx15a.get("container_ratio", [65, 35]) fh = ctx15a.get("font_hierarchy", {}) colors = _colors() coords = _calc_coords(containers, ratio) body = _header_html(coords["header"], "건설산업 DX의 올바른 이해") for role in ["배경", "본심", "첨부", "결론"]: coord = coords[role] c = colors[role] font_key = {"배경": "bg", "본심": "core", "첨부": "sidebar", "결론": "key_msg"}.get(role) label = (f'
' f'{role}
' f'{coord["width"]}x{coord["height"]}px / font:{fh.get(font_key,"?")}px
') body += _box_html(coord, role, label, colors) html = _slide_wrap( "Step 1: 빈 컨테이너 (Stage 1.5a)", f'비율 {ratio[0]}:{ratio[1]}', body, ) (out / "step1_containers.html").write_text(html, encoding="utf-8") print("step1 생성") return coords, containers, ratio, fh def gen_step2(run: Path, out: Path, coords, fh): """Step 2: Step 1 컨테이너 위에 블록 선택 표시.""" ctx17 = _load(run, "stage_1_7_context.json") refs = ctx17.get("references", {}) colors = _colors() ctx15a = _load(run, "stage_1_5a_context.json") ratio = ctx15a.get("container_ratio", [65, 35]) body = _header_html(coords["header"], "건설산업 DX의 올바른 이해") for role in ["배경", "본심", "첨부", "결론"]: coord = coords[role] c = colors[role] ref_list = refs.get(role, []) if not isinstance(ref_list, list): ref_list = [ref_list] # 블록 정보를 컨테이너 안에 표시 block_lines = [] for r in ref_list: if isinstance(r, dict): bid = r.get("block_id", "?") var = r.get("variant", "default") tid = r.get("topic_id", "?") sup = r.get("supporting_topic_ids", []) hier = r.get("is_hierarchical", False) line = f'꼭지{tid}: {bid} ({var})' if hier: line += f' ★주종' if sup: line += f' [종속:{sup}]' block_lines.append(line) block_html = '
'.join(block_lines) font_key = {"배경": "bg", "본심": "core", "첨부": "sidebar", "결론": "key_msg"}.get(role) label = (f'
' f'
' f'{role} ({coord["width"]}x{coord["height"]}px)
' f'
{block_html}
' f'
') body += _box_html(coord, role, label, colors) html = _slide_wrap( "Step 2: 블록 선택 (Stage 1.7) — Step 1 컨테이너 위에 블록 표시", "layer 기반 주종 판단. 배경: 꼭지1(intro)+꼭지2(supporting) → 주종합침 블록 1개", body, ) (out / "step2_blocks.html").write_text(html, encoding="utf-8") print("step2 생성") def gen_step3(run: Path, out: Path, containers, ratio, fh): """Step 3: Step 2 위에 재배분 반영.""" ctx18 = _load(run, "stage_1_8_context.json") fit = ctx18.get("fit_result", {}) enh = ctx18.get("enhancement_result", {}) redist = fit.get("redistribution", {}) # 재배분된 containers new_containers = {} for role, c in containers.items(): h = c.get("height_px", 0) if isinstance(c, dict) else getattr(c, "height_px", 0) new_h = int(redist.get(role, h)) if isinstance(c, dict): new_containers[role] = {**c, "height_px": new_h} else: new_containers[role] = {"height_px": new_h, "width_px": getattr(c, "width_px", 0), "zone": getattr(c, "zone", "")} colors = _colors() new_coords = _calc_coords(new_containers, ratio) # 블록 선택 정보도 가져옴 ctx17 = _load(run, "stage_1_7_context.json") refs = ctx17.get("references", {}) body = _header_html(new_coords["header"], "건설산업 DX의 올바른 이해") for role in ["배경", "본심", "첨부", "결론"]: coord = new_coords[role] c = colors[role] # fit 상태 rf = fit.get("roles", {}).get(role, {}) status = rf.get("fit_status", "?") icon = {"OK": "✅", "TIGHT": "⚠️", "OVERFLOW": "❌"}.get(status, "?") needed = rf.get("total_required_px", 0) old_h = rf.get("allocated_px", 0) new_h = int(redist.get(role, old_h)) delta = new_h - old_h # 블록 정보 ref_list = refs.get(role, []) if not isinstance(ref_list, list): ref_list = [ref_list] block_lines = [] for r in ref_list: if isinstance(r, dict): bid = r.get("block_id", "?") tid = r.get("topic_id", "?") sup = r.get("supporting_topic_ids", []) hier = r.get("is_hierarchical", False) line = f'꼭지{tid}: {bid}' if hier: line += f' ★주종 [종속:{sup}]' block_lines.append(line) # 보강 정보 emps = [e for e in enh.get("emphasis_blocks", []) if e.get("role") == role] bolds = enh.get("bold_keywords", {}).get(role, []) delta_str = f" ({delta:+d}px)" if abs(delta) > 0 else "" enh_lines = [] if emps: enh_lines.append(f'강조: "{emps[0].get("sentence","")[:30]}..."') if bolds: enh_lines.append(f'bold: {bolds[:4]}') label = (f'
' f'
' f'{icon} {role} {coord["width"]}x{new_h}px{delta_str}
' f'
필요 {needed:.0f}px
' f'
{"
".join(block_lines)}
' f'
{"
".join(enh_lines)}
' f'
') body += _box_html(coord, role, label, colors) html = _slide_wrap( "Step 3: 적합성 검증 + 재배분 + 보강 (Stage 1.8)", f"재배분: {', '.join(f'{r}:{int(redist.get(r,0))}px' for r in redist)}", body, ) (out / "step3_fit_result.html").write_text(html, encoding="utf-8") print("step3 생성") def gen_step4(run: Path, out: Path): """Step 4: final.html 링크.""" html = """

Step 4: 최종 결과물 (Sonnet HTML 생성)

final.html 열기 →

첨부1 · 첨부2

""" (out / "step4_final.html").write_text(html, encoding="utf-8") print("step4 생성") def main(run_dir: str): run = Path(run_dir) out = run / "steps" out.mkdir(exist_ok=True) gen_step0(run, out) coords, containers, ratio, fh = gen_step1(run, out) gen_step2(run, out, coords, fh) gen_step3(run, out, containers, ratio, fh) gen_step4(run, out) print(f"\n전체 step: {out}/") for f in sorted(out.iterdir()): print(f" {f.name}") if __name__ == "__main__": run_dir = sys.argv[1] if len(sys.argv) > 1 else "data/runs/20260402_154745" main(run_dir)