o2o-infinith-demo/docs/reports/landing/export_html.py
Haewon Kam 13a8fca775 chore(reports): docs/reports를 고객·단계별 폴더로 분류
- viewclinic/01_readiness_report (진단 덱 v1.0·v2_표준캔버스·v1.1 + 빌더) / 02_poc_proposal (POC 정본·v3 + 빌더) / 03_question_bank (QB 120문항·실측·엑셀) / 04_supporters (GEO 계획·콘텐츠 기획·감사 엑셀)
- taeha/ (태하 리포트 + 빌더), ibk/ (기업은행 블로그 감사), landing/ (랜딩 시안·스크린샷·HTML 내보내기)
- README.md에 구조·정본/파생본·빌드 명령 안내
- 경로 참조 갱신: 핸드오버 문서 4개, QB 스크립트 3개, 빌더 docstring, discovery 페이지 주석
- 이전에 삭제된 POC v2_표준캔버스 2파일 삭제 반영

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 13:03:53 +09:00

70 lines
3.3 KiB
Python

"""
/discovery/:id 리포트 페이지를 단일 HTML 파일로 내보낸다.
npm run build && npx vite preview --port 3012
python3 docs/reports/landing/export_html.py taeha docs/reports/taeha/Taeha_AI_Answer_Readiness_Report.html
- 헤드리스 Chrome으로 렌더된 DOM을 덤프하고 빌드 CSS를 인라인한다
- Navbar·Footer·스크립트 제거, motion 애니메이션 고정
- ScoreRing 오프셋과 카테고리 진행 바 폭을 점수 텍스트에서 다시 계산한다
"""
import glob, re, subprocess, sys
CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
BASE = "http://localhost:3012"
slug, out = sys.argv[1], sys.argv[2]
title = sys.argv[3] if len(sys.argv) > 3 else f"{slug} AI Answer Readiness Report"
dom = subprocess.run(
[CHROME, "--headless=new", "--disable-gpu", "--window-size=1440,900", "--virtual-time-budget=20000",
"--dump-dom", f"{BASE}/discovery/{slug}"],
capture_output=True, text=True).stdout
css = open(glob.glob("dist/assets/*.css")[0], encoding="utf-8").read()
m = re.search(r'<div id="root">(.*)</div>\s*</body>', dom, re.S)
body = m.group(1)
body = re.sub(r"<script.*?</script>", "", body, flags=re.S)
body = re.sub(r"<nav\b.*?</nav>", "", body, flags=re.S, count=1)
body = re.sub(r"<footer\b.*?</footer>", "", body, flags=re.S)
# ScoreRing: dashoffset = circumference * (1 - score/100)
def fix_ring(mm):
circ = float(mm.group(2)); score = int(mm.group(5))
return f"{mm.group(1)}{circ * (1 - min(score, 100) / 100):.4f}{mm.group(4)}{score}{mm.group(6)}"
body, n_ring = re.subn(
r'(stroke-dasharray="([\d.]+)"\s+stroke-dashoffset=")([\d.]+)("></circle></svg><div class="absolute inset-0 flex items-center justify-center"><span[^>]*>)(\d+)(</span>)',
fix_ring, body)
# 카테고리 바: <div class="h-full rounded-full" style="background: X; width: 0%;"> … <span class="font-semibold">NN%</span>
def fix_bar(mm):
return f'{mm.group(1)}width: {mm.group(3)}%;{mm.group(4)}{mm.group(3)}%{mm.group(5)}'
body, n_bar = re.subn(
r'(<div class="h-full rounded-full" style="[^"]*?)width:\s*[\d.]+%;?([^"]*"[^>]*></div></div><div class="flex justify-between mt-2 text-sm"><span class="font-semibold">)(\d+)(%</span>)',
lambda mm: f'{mm.group(1)}width: {mm.group(3)}%;{mm.group(2)}{mm.group(3)}{mm.group(4)}', body)
html = f"""<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,900;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css">
<style>{css}</style>
<style>
[style*="opacity"], [style*="transform"] {{ opacity: 1 !important; transform: none !important; }}
html, body {{ background: #fff; }}
</style>
</head>
<body>
<div class="min-h-screen bg-white">
{body}
</div>
</body>
</html>"""
open(out, "w", encoding="utf-8").write(html)
print(f"saved {out} ({len(html):,} bytes) rings={n_ring} bars={n_bar}")