#!/usr/bin/env python3 """ Phase Z run 산출물을 frontend prototype 의 client/public/data/ 로 정적 export. Usage: python scripts/sync_phase_z_run_to_frontend.py [run_id] python scripts/sync_phase_z_run_to_frontend.py mdx03_f29_fix_check python scripts/sync_phase_z_run_to_frontend.py --list # 사용 가능한 runs 목록 성격: - Static export (Phase 1 — 보고용 read-only viewer prototype). - 향후 Phase 2 에서 Express endpoint (server/index.ts) 로 대체 가능. - Frontend 의 designAgentApi.ts 의 loadRun(runId) 가 본 산출물을 fetch. 복사 대상 (Phase Z runtime 결과 → frontend type 매핑에 필요한 최소 set): root: final.html / preview.png / debug.json steps: step01_mdx_source.md (MDX 원문) step01_mdx_upload.json (run metadata) step02_normalized.json (sections) step05_v4_evidence.json (V4 후보 list) step06_composition_plan.json (composition units) step07_layout.json (layout preset + candidates) step08_zone_region_ratios.json (zone/region/display) step09_application_plan.json (적용 계획 — 핵심) step20_slide_status.json (최종 상태) """ from __future__ import annotations import argparse import shutil import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parent.parent RUNS_DIR = PROJECT_ROOT / "data" / "runs" FRONTEND_DATA_DIR = Path( "D:/ad-hoc/kei/design_agent_front/design-agent/client/public/data/runs" ) ROOT_FILES = ["final.html", "preview.png", "debug.json"] STEP_FILES = [ "step01_mdx_source.md", "step01_mdx_upload.json", "step02_normalized.json", "step05_v4_evidence.json", "step06_composition_plan.json", "step07_layout.json", "step08_zone_region_ratios.json", "step09_application_plan.json", "step20_slide_status.json", ] def list_runs() -> list[Path]: """data/runs/*/phase_z2 가 있는 run dir 목록.""" if not RUNS_DIR.exists(): return [] return sorted( p for p in RUNS_DIR.iterdir() if p.is_dir() and (p / "phase_z2").exists() ) def sync_run(run_id: str, *, verbose: bool = True) -> int: """run_id 의 phase_z2 산출물을 frontend 로 복사. 복사된 파일 수 반환.""" src = RUNS_DIR / run_id / "phase_z2" if not src.exists(): print(f"\n[error] source not found: {src}", file=sys.stderr) return 0 dst = FRONTEND_DATA_DIR / run_id dst.mkdir(parents=True, exist_ok=True) (dst / "steps").mkdir(exist_ok=True) copied = 0 missing: list[str] = [] for fname in ROOT_FILES: src_f = src / fname if src_f.exists(): shutil.copy2(src_f, dst / fname) copied += 1 else: missing.append(fname) for fname in STEP_FILES: src_f = src / "steps" / fname if src_f.exists(): shutil.copy2(src_f, dst / "steps" / fname) copied += 1 else: missing.append(f"steps/{fname}") if verbose: print(f"\n[ok] {run_id}") print(f" src : {src}") print(f" dst : {dst}") print(f" copied : {copied} files") if missing: print(f" missing: {len(missing)} files (산출물 없음 — 정상일 수 있음)") for m in missing: print(f" - {m}") return copied def main() -> None: parser = argparse.ArgumentParser( description="Phase Z run → frontend prototype 정적 export" ) parser.add_argument( "run_id", nargs="?", default=None, help="복사할 run id. 미지정 시 가장 최근 mdx03_* run 사용.", ) parser.add_argument( "--list", action="store_true", help="사용 가능한 run 목록만 출력 후 종료.", ) parser.add_argument( "--all-mdx03", action="store_true", help="mdx03_* 로 시작하는 모든 run 복사.", ) args = parser.parse_args() if args.list: runs = list_runs() print(f"\nAvailable runs ({len(runs)}):") for r in runs: print(f" - {r.name}") return # client/public 자체는 존재해야 (frontend repo 가 정상이라는 sanity check) frontend_public = FRONTEND_DATA_DIR.parent.parent # = client/public if not frontend_public.exists(): print( f"\n[error] frontend client/public 폴더 없음: {frontend_public}", file=sys.stderr, ) print( " FRONTEND_DATA_DIR 경로 본 script 안에서 확인 필요.", file=sys.stderr, ) sys.exit(1) # data/runs 가 없으면 자동 생성 (정적 export 첫 실행 시) FRONTEND_DATA_DIR.mkdir(parents=True, exist_ok=True) if args.all_mdx03: runs = [r for r in list_runs() if r.name.startswith("mdx03_")] print(f"\nSyncing {len(runs)} mdx03_* runs ...") total = 0 for r in runs: total += sync_run(r.name, verbose=True) print(f"\n[done] total: {total} files across {len(runs)} runs") return if args.run_id is None: # default: 가장 최근 mdx03_* run candidates = [r for r in list_runs() if r.name.startswith("mdx03_")] if not candidates: print( "\n[error] mdx03_* run 없음. 명시적으로 run_id 지정 필요.", file=sys.stderr, ) sys.exit(1) # mtime 기준 최신 latest = max(candidates, key=lambda p: p.stat().st_mtime) args.run_id = latest.name print(f"\n[default] 가장 최근 mdx03_* run = {args.run_id}") sync_run(args.run_id, verbose=True) if __name__ == "__main__": main()