Phase P~S 전체 작업물: 검증 스크립트, 블록 템플릿, 설계 문서, 코드 수정

포함 내용:
- Phase P/Q/R/S 설계 문서 (IMPROVEMENT-PHASE-*.md)
- 영역별 검증 스크립트 (scripts/verify_*.py, test_*.py)
- 블록 템플릿 추가 (cards, emphasis 변형)
- 코드 수정: block_search, content_editor, design_director, slide_measurer
- catalog.yaml 블록 목록 업데이트
- CLAUDE.md, PROGRESS.md, README.md 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 08:38:06 +09:00
parent 0e4b8c091c
commit 29f56187c0
44 changed files with 9431 additions and 313 deletions

617
scripts/test_3approaches.py Normal file
View File

@@ -0,0 +1,617 @@
"""3가지 접근법 비교: 같은 콘텐츠, 다른 생성 방식.
접근 A: Few-Shot 직접 생성 — Claude가 디자인 토큰 안에서 HTML 직접 작성
접근 B: 레이아웃 프리미티브 조합 — 15개 기본 요소를 조합
접근 C: 참조 기반 생성 — ideal_v2를 참조하여 구조 유지하되 콘텐츠만 교체
Kei API 불필요 — 순수 렌더링만.
"""
from __future__ import annotations
import asyncio, json, sys, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
# ═══════════════════════════════════════
# 공통 디자인 토큰 (3가지 접근 모두 이 토큰만 사용)
# ═══════════════════════════════════════
DESIGN_TOKENS_CSS = """
:root {
--color-primary: #1e293b;
--color-accent: #2563eb;
--color-accent-light: #93c5fd;
--color-bg: #ffffff;
--color-bg-subtle: #f8fafc;
--color-bg-dark: #1e293b;
--color-bg-dark-deep: #0f172a;
--color-border: #e2e8f0;
--color-danger: #dc2626;
--color-warning: #fbbf24;
--color-text: #1e293b;
--color-text-secondary: #64748b;
--color-text-light: #94a3b8;
--color-text-on-dark: #e2e8f0;
--color-text-on-accent: #ffffff;
--font-title: 28px;
--font-section: 14px;
--font-body: 13px;
--font-small: 11px;
--font-caption: 10px;
--weight-normal: 400;
--weight-medium: 500;
--weight-bold: 700;
--weight-black: 900;
--spacing-page: 36px 40px 24px;
--spacing-section: 16px;
--spacing-block: 12px;
--spacing-inner: 10px;
--spacing-small: 6px;
--radius: 8px;
--radius-small: 6px;
--line-height: 1.6;
}
"""
SLIDE_BASE_CSS = """
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
.slide {
width: 1280px; height: 720px; overflow: hidden;
background: var(--color-bg);
font-family: 'Pretendard Variable', sans-serif;
color: var(--color-text);
font-size: var(--font-body);
line-height: var(--line-height);
word-break: keep-all;
display: grid;
grid-template-areas: 'header header' 'body sidebar' 'footer footer';
grid-template-columns: 65fr 35fr;
grid-template-rows: auto 1fr auto;
gap: var(--spacing-section);
padding: var(--spacing-page);
}
.header {
grid-area: header;
font-size: var(--font-title);
font-weight: var(--weight-black);
color: var(--color-primary);
border-bottom: 3px solid var(--color-accent);
padding-bottom: 8px;
}
.body { grid-area: body; display: flex; flex-direction: column; gap: var(--spacing-block); overflow: hidden; }
.sidebar { grid-area: sidebar; display: flex; flex-direction: column; gap: var(--spacing-block); border-left: 1px solid var(--color-border); padding-left: 20px; overflow: hidden; }
.footer { grid-area: footer; background: linear-gradient(135deg, #006aff, #00aaff); border-radius: var(--radius); padding: 14px 30px; text-align: center; color: var(--color-text-on-accent); }
.footer-text { font-size: 15px; font-weight: var(--weight-bold); }
.footer-sub { font-size: var(--font-small); opacity: 0.85; margin-top: 2px; }
"""
# ═══════════════════════════════════════
# 접근 A: Few-Shot 직접 생성
# Claude가 디자인 토큰만 보고 자유롭게 HTML 구성
# (여기서는 "Claude가 만들었을 법한" 결과를 시뮬레이션)
# ═══════════════════════════════════════
APPROACH_A_HTML = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8"><title>접근 A</title>
<style>{DESIGN_TOKENS_CSS}{SLIDE_BASE_CSS}
/* 접근 A: Claude가 콘텐츠에 맞게 자유 구성 */
.intro-bar {{
background: linear-gradient(135deg, var(--color-bg-dark), var(--color-bg-dark-deep));
border-radius: var(--radius);
padding: 14px 20px;
color: var(--color-text-on-dark);
}}
.intro-bar h3 {{
font-size: var(--font-body);
font-weight: var(--weight-bold);
color: var(--color-accent-light);
margin-bottom: var(--spacing-small);
}}
.intro-bar p {{
font-size: var(--font-body);
line-height: 1.7;
}}
.intro-bar strong {{ color: var(--color-warning); }}
.cases-row {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-inner);
margin-top: var(--spacing-inner);
}}
.case {{
background: rgba(255,255,255,0.07);
border-radius: var(--radius-small);
padding: 8px 12px;
border-left: 3px solid var(--color-accent-light);
}}
.case-title {{ font-size: var(--font-small); font-weight: var(--weight-bold); color: var(--color-accent-light); margin-bottom: 3px; }}
.case-text {{ font-size: var(--font-small); color: var(--color-text-light); line-height: 1.5; }}
.core-title {{ font-size: var(--font-section); font-weight: var(--weight-black); color: var(--color-accent); text-align: center; margin-bottom: var(--spacing-small); }}
.dx-container {{
border: 3px solid var(--color-accent);
border-radius: 14px;
padding: 14px 16px 12px;
background: linear-gradient(135deg, #eff6ff, #dbeafe);
position: relative;
flex: 1;
display: flex;
flex-direction: column;
}}
.dx-badge {{
position: absolute; top: -11px; left: 16px;
background: var(--color-accent); color: white;
font-size: var(--font-small); font-weight: var(--weight-black);
padding: 2px 14px; border-radius: var(--radius-small);
}}
.dx-desc {{
font-size: var(--font-small); color: #1e40af;
text-align: center; margin-bottom: var(--spacing-inner);
}}
.tech-grid {{
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: var(--spacing-inner);
flex: 1;
}}
.tech {{
background: white; border: 2px solid var(--color-accent-light);
border-radius: var(--radius); padding: 8px; text-align: center;
display: flex; flex-direction: column; align-items: center;
}}
.tech-circle {{
width: 32px; height: 32px; border-radius: 50%;
background: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
color: white; font-size: 15px; font-weight: var(--weight-black);
display: flex; align-items: center; justify-content: center;
margin-bottom: 4px;
}}
.tech b {{ font-size: var(--font-body); color: var(--color-primary); }}
.tech span {{ font-size: var(--font-caption); color: var(--color-text-secondary); line-height: 1.4; margin-top: 2px; }}
.key-msg {{
background: #f0f9ff; border: 2px solid #bae6fd;
border-radius: var(--radius); padding: 8px 14px; text-align: center;
}}
.key-msg p {{ font-size: var(--font-body); font-weight: var(--weight-bold); color: #0c4a6e; }}
.key-msg em {{ color: var(--color-danger); font-style: normal; font-weight: var(--weight-black); }}
.sidebar-label {{
display: flex; align-items: center; gap: 10px;
font-size: var(--font-small); font-weight: var(--weight-medium); color: var(--color-text-light);
}}
.sidebar-label::before, .sidebar-label::after {{ content: ''; flex: 1; height: 1px; background: var(--color-border); }}
.def {{
background: var(--color-bg-subtle); border: 1px solid var(--color-border);
border-radius: var(--radius); padding: 10px 12px;
}}
.def-head {{ display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }}
.def-num {{
width: 22px; height: 22px; border-radius: 50%; background: var(--color-accent);
color: white; font-size: var(--font-small); font-weight: var(--weight-black);
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}}
.def-title {{ font-size: var(--font-body); font-weight: var(--weight-bold); }}
.def-desc {{ font-size: var(--font-small); color: var(--color-text-secondary); line-height: 1.6; }}
.def-src {{ font-size: var(--font-caption); color: var(--color-text-light); font-style: italic; margin-top: 3px; }}
</style></head><body>
<div class="slide">
<div class="header">건설산업 DX의 올바른 이해</div>
<div class="body">
<div class="intro-bar">
<h3>현실 — 용어의 혼용</h3>
<p>건설산업에서 <strong>DX와 BIM이 동일 개념으로 인식</strong>되고 있다. DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 3차원 모델 기반의 정보 관리 도구로서 DX의 하위 기술에 해당한다.</p>
<div class="cases-row">
<div class="case">
<div class="case-title">스마트 건설 활성화 방안 (2022.07)</div>
<div class="case-text">추진과제: 건설산업 디지털화<br>실행과제: BIM 전면 도입, BIM 전문인력 양성</div>
</div>
<div class="case">
<div class="case-title">제7차 건설기술진흥 기본계획 (2023.12)</div>
<div class="case-text">추진방향: 디지털 전환을 통한 스마트 건설 확산<br>추진과제: BIM 도입으로 건설산업 디지털화</div>
</div>
</div>
</div>
<div class="core-title">DX와 핵심기술의 올바른 관계</div>
<div class="dx-container">
<div class="dx-badge">DX — 디지털 전환 (상위개념)</div>
<div class="dx-desc">BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능</div>
<div class="tech-grid">
<div class="tech">
<div class="tech-circle">G</div>
<b>GIS</b>
<span>지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</span>
</div>
<div class="tech">
<div class="tech-circle">B</div>
<b>BIM</b>
<span>시설물 생애주기 정보를 3차원 모델 기반으로 통합·관리하는 도구</span>
</div>
<div class="tech">
<div class="tech-circle">T</div>
<b>디지털 트윈</b>
<span>현실 세계의 물리적 객체를 디지털 환경에 동일하게 구현</span>
</div>
</div>
</div>
<div class="key-msg">
<p><em>BIM ≠ DX</em> — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다</p>
</div>
</div>
<div class="sidebar">
<div class="sidebar-label">용어 정의</div>
<div class="def">
<div class="def-head"><span class="def-num">1</span><span class="def-title">건설산업</span></div>
<div class="def-desc">부동산 개발, 설계, 시공, 유지보수를 포괄하는 종합산업으로, 광범위한 기술을 통합·융합하여 인프라를 만드는 산업</div>
</div>
<div class="def">
<div class="def-head"><span class="def-num">2</span><span class="def-title">BIM</span></div>
<div class="def-desc">형상정보와 속성정보가 포함된 3D 모델로, 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="def-src">건설산업 BIM 기본지침, 국토교통부, 2020</div>
</div>
<div class="def">
<div class="def-head"><span class="def-num">3</span><span class="def-title">DX (디지털 전환)</span></div>
<div class="def-desc">디지털 기술을 활용하여 업무방식과 가치 창출 구조를 전환하는 과정 및 결과. 단순한 기술 도입이 아닌, 산업의 새로운 방향을 정립</div>
<div class="def-src">IBM Institute for Business Value, 2011</div>
</div>
</div>
<div class="footer">
<div class="footer-text">BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다</div>
<div class="footer-sub">각 용어의 정의, 역할, 상호관계에 대한 체계적 정립 필요</div>
</div>
</div>
</body></html>"""
# ═══════════════════════════════════════
# 접근 B: 레이아웃 프리미티브 조합
# 15개 기본 요소 중 선택하여 조합
# ═══════════════════════════════════════
APPROACH_B_HTML = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8"><title>접근 B</title>
<style>{DESIGN_TOKENS_CSS}{SLIDE_BASE_CSS}
/* 접근 B: 프리미티브 조합 — callout + comparison-table + card-row + definition-list */
.prim-callout {{
background: linear-gradient(135deg, var(--color-bg-dark), var(--color-bg-dark-deep));
border-radius: var(--radius); padding: 12px 18px; color: var(--color-text-on-dark);
}}
.prim-callout-title {{ font-size: var(--font-body); font-weight: var(--weight-bold); color: var(--color-accent-light); margin-bottom: 4px; }}
.prim-callout-text {{ font-size: var(--font-small); line-height: 1.6; }}
.prim-callout-text strong {{ color: var(--color-warning); }}
.prim-compare {{
display: grid; grid-template-columns: 1fr 1fr; gap: 2px;
border-radius: var(--radius); overflow: hidden; margin-top: var(--spacing-small);
}}
.prim-compare-head {{
font-size: var(--font-small); font-weight: var(--weight-bold);
padding: 6px 10px; text-align: center;
}}
.prim-compare-head.left {{ background: var(--color-accent); color: white; }}
.prim-compare-head.right {{ background: #475569; color: white; }}
.prim-compare-row {{ display: contents; }}
.prim-compare-cell {{
font-size: var(--font-caption); padding: 5px 10px;
background: var(--color-bg-subtle); border-bottom: 1px solid var(--color-border);
line-height: 1.5;
}}
.prim-compare-cell.left {{ color: var(--color-accent); font-weight: var(--weight-medium); }}
.prim-section-title {{
font-size: var(--font-section); font-weight: var(--weight-black);
color: var(--color-accent); text-align: center; padding: 4px 0;
}}
.prim-card-row {{
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--spacing-inner);
flex: 1;
}}
.prim-card {{
background: var(--color-bg-subtle); border: 1px solid var(--color-border);
border-radius: var(--radius); padding: 10px; text-align: center;
display: flex; flex-direction: column; align-items: center;
}}
.prim-card-icon {{
width: 32px; height: 32px; border-radius: 50%;
background: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
color: white; font-size: 15px; font-weight: var(--weight-black);
display: flex; align-items: center; justify-content: center; margin-bottom: 4px;
}}
.prim-card b {{ font-size: var(--font-body); }}
.prim-card span {{ font-size: var(--font-caption); color: var(--color-text-secondary); line-height: 1.4; margin-top: 2px; }}
.prim-highlight {{
background: #fef2f2; border: 2px solid #fecaca; border-radius: var(--radius);
padding: 8px 14px; text-align: center;
}}
.prim-highlight p {{ font-size: var(--font-body); font-weight: var(--weight-bold); color: var(--color-danger); }}
.sidebar-label {{ display: flex; align-items: center; gap: 10px; font-size: var(--font-small); font-weight: var(--weight-medium); color: var(--color-text-light); }}
.sidebar-label::before, .sidebar-label::after {{ content: ''; flex: 1; height: 1px; background: var(--color-border); }}
.prim-deflist {{ display: flex; flex-direction: column; gap: var(--spacing-inner); }}
.prim-def {{
background: var(--color-bg-subtle); border: 1px solid var(--color-border);
border-radius: var(--radius); padding: 10px 12px;
}}
.prim-def-head {{ display: flex; align-items: center; gap: 6px; margin-bottom: 3px; }}
.prim-def-num {{
width: 20px; height: 20px; border-radius: 50%; background: var(--color-accent);
color: white; font-size: 10px; font-weight: var(--weight-black);
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}}
.prim-def-title {{ font-size: var(--font-body); font-weight: var(--weight-bold); }}
.prim-def-desc {{ font-size: var(--font-small); color: var(--color-text-secondary); line-height: 1.5; }}
.prim-def-src {{ font-size: var(--font-caption); color: var(--color-text-light); font-style: italic; margin-top: 2px; }}
</style></head><body>
<div class="slide">
<div class="header">건설산업 DX의 올바른 이해</div>
<div class="body">
<!-- 프리미티브 1: callout (문제 제기) -->
<div class="prim-callout">
<div class="prim-callout-title">현실 — 용어의 혼용</div>
<div class="prim-callout-text">건설산업에서 <strong>DX와 BIM이 동일 개념으로 인식</strong>되고 있다. DX는 상위개념이며 BIM은 하위 기술에 해당한다.</div>
<!-- 프리미티브 2: compare (사례 비교) -->
<div class="prim-compare">
<div class="prim-compare-head left">스마트건설 활성화 방안 (2022.07)</div>
<div class="prim-compare-head right">제7차 건설기술진흥 기본계획 (2023.12)</div>
<div class="prim-compare-cell left">추진과제: 건설산업 디지털화</div>
<div class="prim-compare-cell">추진방향: 디지털 전환을 통한 스마트 건설 확산</div>
<div class="prim-compare-cell left">실행과제: BIM 전면 도입, 전문인력 양성</div>
<div class="prim-compare-cell">추진과제: BIM 도입으로 건설산업 디지털화</div>
</div>
</div>
<!-- 프리미티브 3: section-title -->
<div class="prim-section-title">DX와 핵심기술의 올바른 관계</div>
<!-- 프리미티브 4: card-row (기술 카드 3열) -->
<div class="prim-card-row">
<div class="prim-card">
<div class="prim-card-icon">G</div>
<b>GIS</b>
<span>지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</span>
</div>
<div class="prim-card">
<div class="prim-card-icon">B</div>
<b>BIM</b>
<span>시설물 생애주기 정보를 3차원 모델 기반으로 통합·관리하는 도구</span>
</div>
<div class="prim-card">
<div class="prim-card-icon">T</div>
<b>디지털 트윈</b>
<span>현실 세계의 물리적 객체를 디지털 환경에 동일하게 구현</span>
</div>
</div>
<!-- 프리미티브 5: highlight (핵심 메시지) -->
<div class="prim-highlight">
<p>BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다</p>
</div>
</div>
<div class="sidebar">
<div class="sidebar-label">용어 정의</div>
<div class="prim-deflist">
<div class="prim-def">
<div class="prim-def-head"><span class="prim-def-num">1</span><span class="prim-def-title">건설산업</span></div>
<div class="prim-def-desc">부동산 개발, 설계, 시공, 유지보수를 포괄하는 종합산업으로, 광범위한 기술을 통합·융합하여 인프라를 만드는 산업</div>
</div>
<div class="prim-def">
<div class="prim-def-head"><span class="prim-def-num">2</span><span class="prim-def-title">BIM</span></div>
<div class="prim-def-desc">형상정보와 속성정보가 포함된 3D 모델로, 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="prim-def-src">건설산업 BIM 기본지침, 국토교통부, 2020</div>
</div>
<div class="prim-def">
<div class="prim-def-head"><span class="prim-def-num">3</span><span class="prim-def-title">DX (디지털 전환)</span></div>
<div class="prim-def-desc">디지털 기술을 활용하여 업무방식과 가치 창출 구조를 전환하는 과정 및 결과. 단순한 기술 도입이 아닌, 산업의 새로운 방향을 정립</div>
<div class="prim-def-src">IBM Institute for Business Value, 2011</div>
</div>
</div>
</div>
<div class="footer">
<div class="footer-text">BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다</div>
<div class="footer-sub">각 용어의 정의, 역할, 상호관계에 대한 체계적 정립 필요</div>
</div>
</div>
</body></html>"""
# ═══════════════════════════════════════
# 접근 C: 참조 기반 생성
# ideal_v2의 구조를 참조하되, 디자인 토큰으로 스타일 통일
# + 포함관계를 더 명확하게 (DX 큰 원 안에 3개)
# ═══════════════════════════════════════
APPROACH_C_HTML = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8"><title>접근 C</title>
<style>{DESIGN_TOKENS_CSS}{SLIDE_BASE_CSS}
/* 접근 C: 참조(ideal_v2) 기반 + 디자인 토큰 통일 */
.ref-problem {{
background: linear-gradient(135deg, var(--color-bg-dark), var(--color-bg-dark-deep));
border-radius: var(--radius); padding: 14px 20px; color: var(--color-text-on-dark);
}}
.ref-problem h3 {{ font-size: var(--font-body); font-weight: var(--weight-bold); color: var(--color-accent-light); margin-bottom: var(--spacing-small); }}
.ref-problem p {{ font-size: var(--font-body); line-height: 1.7; }}
.ref-problem strong {{ color: var(--color-warning); }}
.ref-cases {{ display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-inner); margin-top: var(--spacing-inner); }}
.ref-case {{ background: rgba(255,255,255,0.07); border-radius: var(--radius-small); padding: 8px 12px; border-left: 3px solid var(--color-accent-light); }}
.ref-case-title {{ font-size: var(--font-small); font-weight: var(--weight-bold); color: var(--color-accent-light); margin-bottom: 3px; }}
.ref-case-text {{ font-size: var(--font-small); color: var(--color-text-light); line-height: 1.5; }}
.ref-core-title {{ font-size: var(--font-section); font-weight: var(--weight-black); color: var(--color-accent); text-align: center; }}
/* 포함관계: SVG 기반 벤 다이어그램 스타일 */
.ref-hierarchy {{
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
position: relative;
}}
.ref-dx-ring {{
width: 100%; max-width: 600px;
border: 3px solid var(--color-accent); border-radius: 20px;
padding: 20px 16px 14px; position: relative;
background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
}}
.ref-dx-tag {{
position: absolute; top: -12px; left: 50%; transform: translateX(-50%);
background: var(--color-accent); color: white;
font-size: 12px; font-weight: var(--weight-black);
padding: 3px 20px; border-radius: 12px; white-space: nowrap;
}}
.ref-dx-sub {{
text-align: center; font-size: var(--font-small); color: #1e40af;
margin-bottom: var(--spacing-inner);
}}
.ref-techs {{
display: flex; justify-content: center; gap: 16px;
}}
.ref-tech {{
width: 130px; text-align: center;
}}
.ref-tech-bubble {{
width: 50px; height: 50px; border-radius: 50%;
background: linear-gradient(135deg, var(--color-accent-light), var(--color-accent));
color: white; font-size: 20px; font-weight: var(--weight-black);
display: flex; align-items: center; justify-content: center;
margin: 0 auto 6px; box-shadow: 0 2px 8px rgba(37,99,235,0.3);
}}
.ref-tech b {{ display: block; font-size: var(--font-body); color: var(--color-primary); margin-bottom: 2px; }}
.ref-tech span {{ font-size: var(--font-caption); color: var(--color-text-secondary); line-height: 1.4; }}
.ref-arrow {{
text-align: center; font-size: 12px; color: var(--color-accent);
font-weight: var(--weight-bold); margin: 4px 0;
}}
.ref-msg {{
background: #f0f9ff; border: 2px solid #bae6fd;
border-radius: var(--radius); padding: 10px 16px; text-align: center;
margin-top: var(--spacing-small);
}}
.ref-msg p {{ font-size: var(--font-body); font-weight: var(--weight-bold); color: #0c4a6e; }}
.ref-msg em {{ color: var(--color-danger); font-style: normal; font-weight: var(--weight-black); }}
.sidebar-label {{ display: flex; align-items: center; gap: 10px; font-size: var(--font-small); font-weight: var(--weight-medium); color: var(--color-text-light); }}
.sidebar-label::before, .sidebar-label::after {{ content: ''; flex: 1; height: 1px; background: var(--color-border); }}
.ref-def {{
background: var(--color-bg-subtle); border: 1px solid var(--color-border);
border-radius: var(--radius); padding: 10px 12px;
}}
.ref-def-head {{ display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }}
.ref-def-num {{
width: 22px; height: 22px; border-radius: 50%; background: var(--color-accent);
color: white; font-size: var(--font-small); font-weight: var(--weight-black);
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}}
.ref-def-title {{ font-size: var(--font-body); font-weight: var(--weight-bold); }}
.ref-def-desc {{ font-size: var(--font-small); color: var(--color-text-secondary); line-height: 1.6; }}
.ref-def-src {{ font-size: var(--font-caption); color: var(--color-text-light); font-style: italic; margin-top: 3px; }}
</style></head><body>
<div class="slide">
<div class="header">건설산업 DX의 올바른 이해</div>
<div class="body">
<div class="ref-problem">
<h3>현실 — 용어의 혼용</h3>
<p>건설산업에서 <strong>DX와 BIM이 동일 개념으로 인식</strong>되고 있다. DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 하위 기술에 해당한다.</p>
<div class="ref-cases">
<div class="ref-case">
<div class="ref-case-title">스마트 건설 활성화 방안 (2022.07)</div>
<div class="ref-case-text">추진과제: 건설산업 디지털화<br>실행과제: BIM 전면 도입, BIM 전문인력 양성</div>
</div>
<div class="ref-case">
<div class="ref-case-title">제7차 건설기술진흥 기본계획 (2023.12)</div>
<div class="ref-case-text">추진방향: 디지털 전환을 통한 스마트 건설 확산<br>추진과제: BIM 도입으로 건설산업 디지털화</div>
</div>
</div>
</div>
<div class="ref-core-title">DX와 핵심기술의 올바른 관계</div>
<div class="ref-hierarchy">
<div class="ref-dx-ring">
<div class="ref-dx-tag">DX — 디지털 전환 (상위개념)</div>
<div class="ref-dx-sub">업무방식과 가치 창출 구조를 근본적으로 전환하는 과정</div>
<div class="ref-techs">
<div class="ref-tech">
<div class="ref-tech-bubble">G</div>
<b>GIS</b>
<span>지리적 데이터를 공간 분석, 위치기반 정보 제공</span>
</div>
<div class="ref-tech">
<div class="ref-tech-bubble">B</div>
<b>BIM</b>
<span>시설물 생애주기 정보를 3차원 모델로 통합·관리</span>
</div>
<div class="ref-tech">
<div class="ref-tech-bubble">T</div>
<b>디지털 트윈</b>
<span>현실 객체를 디지털 환경에 동일하게 구현</span>
</div>
</div>
</div>
</div>
<div class="ref-msg">
<p><em>BIM ≠ DX</em> — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다</p>
</div>
</div>
<div class="sidebar">
<div class="sidebar-label">용어 정의</div>
<div class="ref-def">
<div class="ref-def-head"><span class="ref-def-num">1</span><span class="ref-def-title">건설산업</span></div>
<div class="ref-def-desc">부동산 개발, 설계, 시공, 유지보수를 포괄하는 종합산업으로, 광범위한 기술을 통합·융합하여 인프라를 만드는 산업</div>
</div>
<div class="ref-def">
<div class="ref-def-head"><span class="ref-def-num">2</span><span class="ref-def-title">BIM</span></div>
<div class="ref-def-desc">형상정보와 속성정보가 포함된 3D 모델로, 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="ref-def-src">건설산업 BIM 기본지침, 국토교통부, 2020</div>
</div>
<div class="ref-def">
<div class="ref-def-head"><span class="ref-def-num">3</span><span class="ref-def-title">DX (디지털 전환)</span></div>
<div class="ref-def-desc">디지털 기술을 활용하여 업무방식과 가치 창출 구조를 전환하는 과정 및 결과. 단순한 기술 도입이 아닌, 산업의 새로운 방향을 정립</div>
<div class="ref-def-src">IBM Institute for Business Value, 2011</div>
</div>
</div>
<div class="footer">
<div class="footer-text">BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다</div>
<div class="footer-sub">각 용어의 정의, 역할, 상호관계에 대한 체계적 정립 필요</div>
</div>
</div>
</body></html>"""
async def main():
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / "3approaches"
out_dir.mkdir(parents=True, exist_ok=True)
for name, html in [("A_fewshot", APPROACH_A_HTML), ("B_primitives", APPROACH_B_HTML), ("C_reference", APPROACH_C_HTML)]:
print(f"\n=== 접근 {name} ===")
m = await asyncio.to_thread(measure_rendered_heights, html)
s = await asyncio.to_thread(capture_slide_screenshot, html)
(out_dir / f"{name}.html").write_text(html, encoding="utf-8")
if s:
(out_dir / f"{name}.png").write_bytes(base64.b64decode(s))
slide = m.get("slide", {})
print(f" slide: {slide.get('scrollHeight', 0)}px / 720px {'' if not slide.get('overflowed') else ''}")
print(f"\n결과물: {out_dir}")
print(" A_fewshot.png — 접근 A: Claude가 디자인 토큰 안에서 자유 생성")
print(" B_primitives.png — 접근 B: 15개 프리미티브 조합")
print(" C_reference.png — 접근 C: ideal_v2 참조 기반 + 더 큰 포함관계 시각화")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,474 @@
"""3가지 접근법 비교 — 콘텐츠 2: DX 시행 목표 및 기대 효과
이전 콘텐츠(포함 관계)와 성격이 다름:
- 목표 3가지 (안전/품질, 생산성, 소통/신뢰)
- 프로세스 변화 4가지 (생산방식, 인지검토, 협업구조, 검증대응)
- 주체별 기대효과 (DxEffect 컴포넌트 — 텍스트로 대체)
- 핵심 결론 1줄
"""
from __future__ import annotations
import asyncio, json, sys, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
DESIGN_TOKENS_CSS = """
:root {
--color-primary: #1e293b;
--color-accent: #2563eb;
--color-accent-light: #93c5fd;
--color-bg: #ffffff;
--color-bg-subtle: #f8fafc;
--color-bg-dark: #1e293b;
--color-bg-dark-deep: #0f172a;
--color-border: #e2e8f0;
--color-danger: #dc2626;
--color-success: #16a34a;
--color-warning: #f59e0b;
--color-text: #1e293b;
--color-text-secondary: #64748b;
--color-text-light: #94a3b8;
--color-text-on-dark: #e2e8f0;
--color-text-on-accent: #ffffff;
--font-title: 28px;
--font-section: 14px;
--font-body: 13px;
--font-small: 11px;
--font-caption: 10px;
--weight-normal: 400;
--weight-medium: 500;
--weight-bold: 700;
--weight-black: 900;
--spacing-page: 36px 40px 24px;
--spacing-section: 16px;
--spacing-block: 12px;
--spacing-inner: 10px;
--spacing-small: 6px;
--radius: 8px;
--radius-small: 6px;
--line-height: 1.6;
}
"""
SLIDE_BASE_CSS = """
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
.slide {
width: 1280px; height: 720px; overflow: hidden;
background: var(--color-bg);
font-family: 'Pretendard Variable', sans-serif;
color: var(--color-text);
font-size: var(--font-body);
line-height: var(--line-height);
word-break: keep-all;
display: grid;
grid-template-areas: 'header header' 'body sidebar' 'footer footer';
grid-template-columns: 65fr 35fr;
grid-template-rows: auto 1fr auto;
gap: var(--spacing-section);
padding: var(--spacing-page);
}
.header { grid-area: header; font-size: var(--font-title); font-weight: var(--weight-black); color: var(--color-primary); border-bottom: 3px solid var(--color-accent); padding-bottom: 8px; }
.body { grid-area: body; display: flex; flex-direction: column; gap: var(--spacing-block); overflow: hidden; }
.sidebar { grid-area: sidebar; display: flex; flex-direction: column; gap: var(--spacing-block); border-left: 1px solid var(--color-border); padding-left: 20px; overflow: hidden; }
.footer { grid-area: footer; background: linear-gradient(135deg, #006aff, #00aaff); border-radius: var(--radius); padding: 14px 30px; text-align: center; color: var(--color-text-on-accent); }
.footer-text { font-size: 15px; font-weight: var(--weight-bold); }
.footer-sub { font-size: var(--font-small); opacity: 0.85; margin-top: 2px; }
"""
# ═══════════════════════════════════════
# 접근 A: Few-Shot 직접 생성
# ═══════════════════════════════════════
APPROACH_A = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8"><title>접근 A — DX 목표</title>
<style>{DESIGN_TOKENS_CSS}{SLIDE_BASE_CSS}
.goal-cards {{ display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--spacing-inner); }}
.goal {{
border-radius: var(--radius); padding: 12px; text-align: center;
display: flex; flex-direction: column; align-items: center;
}}
.goal-1 {{ background: linear-gradient(135deg, #eff6ff, #dbeafe); border: 2px solid var(--color-accent-light); }}
.goal-2 {{ background: linear-gradient(135deg, #f0fdf4, #dcfce7); border: 2px solid #86efac; }}
.goal-3 {{ background: linear-gradient(135deg, #fefce8, #fef9c3); border: 2px solid #fde047; }}
.goal-icon {{ font-size: 24px; margin-bottom: 4px; }}
.goal-title {{ font-size: var(--font-body); font-weight: var(--weight-black); margin-bottom: 4px; }}
.goal-desc {{ font-size: var(--font-small); color: var(--color-text-secondary); line-height: 1.5; }}
.section-title {{ font-size: var(--font-section); font-weight: var(--weight-black); color: var(--color-accent); margin-bottom: 2px; }}
.process-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 8px; flex: 1; }}
.process-item {{
background: var(--color-bg-subtle); border: 1px solid var(--color-border);
border-radius: var(--radius); padding: 10px 12px;
display: flex; gap: 10px; align-items: flex-start;
}}
.process-arrow {{
display: flex; align-items: center; justify-content: center;
font-size: 18px; color: var(--color-accent); font-weight: var(--weight-black);
width: 28px; flex-shrink: 0;
}}
.process-content {{ flex: 1; }}
.process-label {{ font-size: var(--font-small); font-weight: var(--weight-bold); color: var(--color-accent); margin-bottom: 2px; }}
.process-before {{ font-size: var(--font-caption); color: var(--color-text-light); text-decoration: line-through; }}
.process-after {{ font-size: var(--font-small); color: var(--color-text); font-weight: var(--weight-medium); margin-top: 2px; }}
.sidebar-label {{ display: flex; align-items: center; gap: 10px; font-size: var(--font-small); font-weight: var(--weight-medium); color: var(--color-text-light); }}
.sidebar-label::before, .sidebar-label::after {{ content: ''; flex: 1; height: 1px; background: var(--color-border); }}
.effect-table {{ width: 100%; border-collapse: collapse; font-size: var(--font-small); flex: 1; }}
.effect-table th {{ background: var(--color-accent); color: white; padding: 6px 8px; text-align: left; font-weight: var(--weight-bold); font-size: var(--font-small); }}
.effect-table td {{ padding: 5px 8px; border-bottom: 1px solid var(--color-border); line-height: 1.5; font-size: var(--font-caption); }}
.effect-table tr:nth-child(even) {{ background: var(--color-bg-subtle); }}
.effect-role {{ font-weight: var(--weight-bold); color: var(--color-accent); white-space: nowrap; }}
</style></head><body>
<div class="slide">
<div class="header">DX 시행 목표 및 기대 효과</div>
<div class="body">
<div class="section-title">DX를 통한 궁극적 목표</div>
<div class="goal-cards">
<div class="goal goal-1">
<div class="goal-icon">🛡️</div>
<div class="goal-title">안전과 품질</div>
<div class="goal-desc">설계-시공-운영 전 과정에서 디지털로 검증하여 안전성 확보. 하자 최소화로 고품질 성과물 제공</div>
</div>
<div class="goal goal-2">
<div class="goal-icon">⚡</div>
<div class="goal-title">생산성 향상</div>
<div class="goal-desc">Analogue → Digital 프로세스 전환. 비용 절감, 기간 단축, 인력투입 최소화로 부가가치 제고</div>
</div>
<div class="goal goal-3">
<div class="goal-icon">🤝</div>
<div class="goal-title">소통과 신뢰</div>
<div class="goal-desc">협업 강화로 의사소통 효율 증진. 3D 모델·데이터 기반 검증으로 오류 최소화 및 Claim 예방</div>
</div>
</div>
<div class="section-title">업무 수행 과정(Process)의 변화</div>
<div class="process-grid">
<div class="process-item">
<div class="process-arrow">→</div>
<div class="process-content">
<div class="process-label">생산 방식</div>
<div class="process-before">수작업 의존의 반복 업무</div>
<div class="process-after">SW를 활용한 체계화된 방식으로 전환</div>
</div>
</div>
<div class="process-item">
<div class="process-arrow">→</div>
<div class="process-content">
<div class="process-label">인지·검토</div>
<div class="process-before">2D 도면 해석 중심</div>
<div class="process-after">3D 모델 기반의 직관적 인지·검토 체계</div>
</div>
</div>
<div class="process-item">
<div class="process-arrow">→</div>
<div class="process-content">
<div class="process-label">협업 구조</div>
<div class="process-before">개별 문서 중심 협업</div>
<div class="process-after">데이터 통합 기반의 정보 공유·관리 환경</div>
</div>
</div>
<div class="process-item">
<div class="process-arrow">→</div>
<div class="process-content">
<div class="process-label">검증·대응</div>
<div class="process-before">사후 대응 중심의 문제 처리</div>
<div class="process-after">사전 검증 중심의 예방적 업무 방식</div>
</div>
</div>
</div>
</div>
<div class="sidebar">
<div class="sidebar-label">주체별 기대효과</div>
<table class="effect-table">
<tr><th>주체</th><th>기대효과</th></tr>
<tr><td class="effect-role">발주처</td><td>품질 향상, 비용·기간 절감, 투명한 관리</td></tr>
<tr><td class="effect-role">설계사</td><td>오류 감소, 설계 품질 제고, 재작업 최소화</td></tr>
<tr><td class="effect-role">시공사</td><td>공정 최적화, 안전 강화, 현장 생산성 향상</td></tr>
<tr><td class="effect-role">감리·CM</td><td>실시간 모니터링, 데이터 기반 의사결정</td></tr>
<tr><td class="effect-role">유지관리</td><td>디지털 트윈 기반 예방 정비, 자산 관리 효율화</td></tr>
</table>
</div>
<div class="footer">
<div class="footer-text">고품질의 성과품, 비용 절감, 시간 단축, 의사소통에 도움이 안 되면 DX가 아니다</div>
</div>
</div>
</body></html>"""
# ═══════════════════════════════════════
# 접근 B: 프리미티브 조합
# ═══════════════════════════════════════
APPROACH_B = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8"><title>접근 B — DX 목표</title>
<style>{DESIGN_TOKENS_CSS}{SLIDE_BASE_CSS}
/* 프리미티브: icon-card-row */
.p-icon-row {{ display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--spacing-inner); }}
.p-icon-card {{
background: var(--color-bg-subtle); border: 1px solid var(--color-border);
border-radius: var(--radius); padding: 10px; text-align: center;
}}
.p-icon-card .icon {{ font-size: 22px; margin-bottom: 4px; }}
.p-icon-card b {{ font-size: var(--font-body); display: block; margin-bottom: 3px; }}
.p-icon-card span {{ font-size: var(--font-small); color: var(--color-text-secondary); line-height: 1.5; }}
/* 프리미티브: section-title */
.p-sec {{ font-size: var(--font-section); font-weight: var(--weight-black); color: var(--color-accent); }}
/* 프리미티브: bullet-list */
.p-bullets {{ display: flex; flex-direction: column; gap: 4px; flex: 1; }}
.p-bullet {{
font-size: var(--font-small); line-height: 1.6; padding-left: 14px; position: relative;
}}
.p-bullet::before {{ content: ''; position: absolute; left: 0; color: var(--color-accent); font-weight: var(--weight-bold); }}
.p-bullet strong {{ color: var(--color-accent); }}
/* 프리미티브: sidebar table */
.sidebar-label {{ display: flex; align-items: center; gap: 10px; font-size: var(--font-small); font-weight: var(--weight-medium); color: var(--color-text-light); }}
.sidebar-label::before, .sidebar-label::after {{ content: ''; flex: 1; height: 1px; background: var(--color-border); }}
.p-table {{ width: 100%; border-collapse: collapse; font-size: var(--font-small); }}
.p-table th {{ background: var(--color-accent); color: white; padding: 6px 8px; text-align: left; font-size: var(--font-small); }}
.p-table td {{ padding: 5px 8px; border-bottom: 1px solid var(--color-border); font-size: var(--font-caption); line-height: 1.5; }}
.p-table tr:nth-child(even) {{ background: var(--color-bg-subtle); }}
.p-table .role {{ font-weight: var(--weight-bold); color: var(--color-accent); }}
</style></head><body>
<div class="slide">
<div class="header">DX 시행 목표 및 기대 효과</div>
<div class="body">
<div class="p-sec">DX를 통한 궁극적 목표</div>
<div class="p-icon-row">
<div class="p-icon-card">
<div class="icon">🛡️</div>
<b>안전과 품질</b>
<span>디지털 검증으로 안전성 확보, 하자 최소화로 고품질 성과물</span>
</div>
<div class="p-icon-card">
<div class="icon">⚡</div>
<b>생산성 향상</b>
<span>Digital 프로세스 전환, 비용 절감·기간 단축·부가가치 제고</span>
</div>
<div class="p-icon-card">
<div class="icon">🤝</div>
<b>소통과 신뢰</b>
<span>협업 강화, 3D·데이터 기반 검증으로 오류 최소화·Claim 예방</span>
</div>
</div>
<div class="p-sec">업무 수행 과정(Process)의 변화</div>
<div class="p-bullets">
<div class="p-bullet"><strong>생산 방식</strong>: 수작업 의존 반복 업무 → SW를 활용한 체계화된 방식으로 전환</div>
<div class="p-bullet"><strong>인지·검토</strong>: 2D 도면 해석 중심 → 3D 모델 기반의 직관적 인지·검토 체계로 전환</div>
<div class="p-bullet"><strong>협업 구조</strong>: 개별 문서 중심 → 데이터 통합 기반의 정보 공유·관리 협업 환경으로 전환</div>
<div class="p-bullet"><strong>검증·대응</strong>: 사후 대응 중심 → 사전 검증 중심의 예방적 업무 방식으로 전환</div>
</div>
</div>
<div class="sidebar">
<div class="sidebar-label">주체별 기대효과</div>
<table class="p-table">
<tr><th>주체</th><th>기대효과</th></tr>
<tr><td class="role">발주처</td><td>품질 향상, 비용·기간 절감, 투명한 관리</td></tr>
<tr><td class="role">설계사</td><td>오류 감소, 설계 품질 제고, 재작업 최소화</td></tr>
<tr><td class="role">시공사</td><td>공정 최적화, 안전 강화, 현장 생산성 향상</td></tr>
<tr><td class="role">감리·CM</td><td>실시간 모니터링, 데이터 기반 의사결정</td></tr>
<tr><td class="role">유지관리</td><td>디지털 트윈 기반 예방 정비, 자산 관리 효율화</td></tr>
</table>
</div>
<div class="footer">
<div class="footer-text">고품질의 성과품, 비용 절감, 시간 단축, 의사소통에 도움이 안 되면 DX가 아니다</div>
</div>
</div>
</body></html>"""
# ═══════════════════════════════════════
# 접근 C: 참조 기반 생성
# ideal_v2의 디자인 패턴(다크배경+포함박스+사이드바 정의) 참조하되
# 이 콘텐츠에 맞게 구조 변형
# ═══════════════════════════════════════
APPROACH_C = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8"><title>접근 C — DX 목표</title>
<style>{DESIGN_TOKENS_CSS}{SLIDE_BASE_CSS}
/* 참조 패턴: 상단 다크 배경 요약 */
.ref-summary {{
background: linear-gradient(135deg, var(--color-bg-dark), var(--color-bg-dark-deep));
border-radius: var(--radius); padding: 14px 20px; color: var(--color-text-on-dark);
}}
.ref-summary h3 {{ font-size: var(--font-body); font-weight: var(--weight-bold); color: var(--color-accent-light); margin-bottom: var(--spacing-small); }}
.ref-goals {{
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--spacing-inner); margin-top: var(--spacing-inner);
}}
.ref-goal {{
background: rgba(255,255,255,0.07); border-radius: var(--radius-small);
padding: 8px 10px; text-align: center;
}}
.ref-goal-icon {{ font-size: 20px; margin-bottom: 2px; }}
.ref-goal-title {{ font-size: var(--font-small); font-weight: var(--weight-bold); color: var(--color-accent-light); }}
.ref-goal-desc {{ font-size: var(--font-caption); color: var(--color-text-light); line-height: 1.5; margin-top: 2px; }}
/* 참조 패턴: 포함 박스 (DX 프레임 안에 4가지 변화) */
.ref-section-title {{ font-size: var(--font-section); font-weight: var(--weight-black); color: var(--color-accent); text-align: center; }}
.ref-dx-frame {{
flex: 1; border: 3px solid var(--color-accent); border-radius: 14px;
padding: 16px 14px 12px; background: linear-gradient(180deg, #eff6ff, #dbeafe);
position: relative; display: flex; flex-direction: column;
}}
.ref-dx-badge {{
position: absolute; top: -11px; left: 50%; transform: translateX(-50%);
background: var(--color-accent); color: white;
font-size: var(--font-small); font-weight: var(--weight-black);
padding: 2px 16px; border-radius: var(--radius-small); white-space: nowrap;
}}
.ref-dx-sub {{ text-align: center; font-size: var(--font-small); color: #1e40af; margin-bottom: var(--spacing-inner); }}
.ref-changes {{
display: grid; grid-template-columns: 1fr 1fr; gap: 8px; flex: 1;
}}
.ref-change {{
background: white; border: 1px solid var(--color-accent-light);
border-radius: var(--radius); padding: 8px 10px;
}}
.ref-change-label {{ font-size: var(--font-small); font-weight: var(--weight-bold); color: var(--color-accent); margin-bottom: 3px; }}
.ref-change-from {{ font-size: var(--font-caption); color: var(--color-text-light); text-decoration: line-through; }}
.ref-change-to {{ font-size: var(--font-small); color: var(--color-text); font-weight: var(--weight-medium); margin-top: 2px; }}
/* 참조 패턴: 사이드바 테이블 */
.sidebar-label {{ display: flex; align-items: center; gap: 10px; font-size: var(--font-small); font-weight: var(--weight-medium); color: var(--color-text-light); }}
.sidebar-label::before, .sidebar-label::after {{ content: ''; flex: 1; height: 1px; background: var(--color-border); }}
.ref-effects {{ display: flex; flex-direction: column; gap: 6px; flex: 1; }}
.ref-effect {{
background: var(--color-bg-subtle); border: 1px solid var(--color-border);
border-radius: var(--radius); padding: 8px 10px;
display: flex; gap: 8px; align-items: flex-start;
}}
.ref-effect-role {{
background: var(--color-accent); color: white;
font-size: var(--font-caption); font-weight: var(--weight-bold);
padding: 2px 8px; border-radius: 4px; white-space: nowrap; flex-shrink: 0;
}}
.ref-effect-desc {{ font-size: var(--font-small); color: var(--color-text-secondary); line-height: 1.5; }}
</style></head><body>
<div class="slide">
<div class="header">DX 시행 목표 및 기대 효과</div>
<div class="body">
<!-- 참조 패턴 1: 다크 배경 요약 + 목표 3카드 -->
<div class="ref-summary">
<h3>DX를 통한 궁극적 목표</h3>
<div class="ref-goals">
<div class="ref-goal">
<div class="ref-goal-icon">🛡️</div>
<div class="ref-goal-title">안전과 품질</div>
<div class="ref-goal-desc">디지털 검증으로 안전성 확보<br>하자 최소화, 고품질 성과물</div>
</div>
<div class="ref-goal">
<div class="ref-goal-icon">⚡</div>
<div class="ref-goal-title">생산성 향상</div>
<div class="ref-goal-desc">Digital 프로세스 전환<br>비용 절감, 기간 단축, 부가가치 제고</div>
</div>
<div class="ref-goal">
<div class="ref-goal-icon">🤝</div>
<div class="ref-goal-title">소통과 신뢰</div>
<div class="ref-goal-desc">협업 강화, 의사소통 효율<br>데이터 검증으로 Claim 예방</div>
</div>
</div>
</div>
<!-- 참조 패턴 2: DX 프레임 안에 프로세스 변화 4가지 -->
<div class="ref-section-title">DX 기반 Process 혁신</div>
<div class="ref-dx-frame">
<div class="ref-dx-badge">업무 수행 과정의 변화</div>
<div class="ref-dx-sub">Analogue 기반 → Digital 기반 프로세스 전환</div>
<div class="ref-changes">
<div class="ref-change">
<div class="ref-change-label">생산 방식</div>
<div class="ref-change-from">수작업 의존의 반복 업무</div>
<div class="ref-change-to">SW를 활용한 체계화된 방식</div>
</div>
<div class="ref-change">
<div class="ref-change-label">인지·검토</div>
<div class="ref-change-from">2D 도면 해석 중심</div>
<div class="ref-change-to">3D 모델 기반의 직관적 인지·검토</div>
</div>
<div class="ref-change">
<div class="ref-change-label">협업 구조</div>
<div class="ref-change-from">개별 문서 중심 협업</div>
<div class="ref-change-to">데이터 통합 기반 정보 공유·관리</div>
</div>
<div class="ref-change">
<div class="ref-change-label">검증·대응</div>
<div class="ref-change-from">사후 대응 중심 문제 처리</div>
<div class="ref-change-to">사전 검증 중심 예방적 업무 방식</div>
</div>
</div>
</div>
</div>
<div class="sidebar">
<div class="sidebar-label">주체별 기대효과</div>
<div class="ref-effects">
<div class="ref-effect">
<span class="ref-effect-role">발주처</span>
<span class="ref-effect-desc">품질 향상, 비용·기간 절감, 투명한 관리</span>
</div>
<div class="ref-effect">
<span class="ref-effect-role">설계사</span>
<span class="ref-effect-desc">오류 감소, 설계 품질 제고, 재작업 최소화</span>
</div>
<div class="ref-effect">
<span class="ref-effect-role">시공사</span>
<span class="ref-effect-desc">공정 최적화, 안전 강화, 현장 생산성 향상</span>
</div>
<div class="ref-effect">
<span class="ref-effect-role">감리·CM</span>
<span class="ref-effect-desc">실시간 모니터링, 데이터 기반 의사결정</span>
</div>
<div class="ref-effect">
<span class="ref-effect-role">유지관리</span>
<span class="ref-effect-desc">디지털 트윈 기반 예방 정비, 자산 관리 효율화</span>
</div>
</div>
</div>
<div class="footer">
<div class="footer-text">고품질의 성과품, 비용 절감, 시간 단축, 의사소통에 도움이 안 되면 DX가 아니다</div>
</div>
</div>
</body></html>"""
async def main():
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / "3approaches_dx2"
out_dir.mkdir(parents=True, exist_ok=True)
for name, html in [("A_fewshot", APPROACH_A), ("B_primitives", APPROACH_B), ("C_reference", APPROACH_C)]:
print(f"\n=== 접근 {name} ===")
m = await asyncio.to_thread(measure_rendered_heights, html)
s = await asyncio.to_thread(capture_slide_screenshot, html)
(out_dir / f"{name}.html").write_text(html, encoding="utf-8")
if s:
(out_dir / f"{name}.png").write_bytes(base64.b64decode(s))
slide = m.get("slide", {})
print(f" slide: {slide.get('scrollHeight', 0)}px / 720px {'' if not slide.get('overflowed') else ''}")
print(f"\n결과물: {out_dir}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

325
scripts/test_3directions.py Normal file
View File

@@ -0,0 +1,325 @@
"""3가지 방향 비교 테스트.
기존 run의 step1 결과 + 6차 테스트의 블록 선택/텍스트를 재사용.
컨테이너 배분만 3가지 방향으로 달리하여 렌더링 비교.
방향 1: 컨테이너 고정, 블록을 컨테이너에 맞춤 (폰트 축소 + 간격 압축)
방향 2: 텍스트 분량 기반 컨테이너 재조정 (비중 ±조정)
방향 3: Two-Pass (텍스트 먼저 → 컨테이너 재조정)
사용법:
python scripts/test_3directions.py
"""
from __future__ import annotations
import asyncio
import json
import copy
import sys
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.renderer import render_slide
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
from src.design_director import select_preset, LAYOUT_PRESETS
from src.space_allocator import calculate_container_specs
import base64
# 기존 데이터 로딩
run_dir = ROOT / "data" / "runs" / "1774736083771"
analysis = json.loads((run_dir / "step1_analysis.json").read_text(encoding="utf-8"))
concepts = json.loads((run_dir / "step1b_concepts.json").read_text(encoding="utf-8"))
# concepts 병합
concept_map = {c["id"]: c for c in concepts.get("concepts", [])}
for topic in analysis.get("topics", []):
tid = topic["id"]
if tid in concept_map:
topic["relation_type"] = concept_map[tid].get("relation_type", "none")
topic["source_data"] = concept_map[tid].get("source_data", "")
topics = analysis["topics"]
page_structure = analysis["page_structure"]
preset_name = select_preset(analysis)
preset = LAYOUT_PRESETS[preset_name]
out_dir = ROOT / "data" / "runs" / "direction_comparison"
out_dir.mkdir(parents=True, exist_ok=True)
# 6차 결과의 텍스트 데이터 (이미 Kei가 채운 것)
# 실제 원본 수준의 풍부한 텍스트를 직접 구성
filled_data = {
1: {
"type": "dark-bullet-list",
"area": "body",
"purpose": "문제제기",
"data": {
"title": "용어의 혼용",
"bullets": [
"건설산업에서 DX와 BIM이 동일 개념으로 인식되고 있다",
"DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 3차원 모델 기반의 정보 관리 도구로서 DX의 하위 기술에 해당한다",
"BIM 도입만으로 DX가 완성된 것으로 오인하는 사례가 빈번하다"
]
}
},
2: {
"type": "card-numbered",
"area": "body",
"purpose": "근거사례",
"data": {
"items": [
{
"title": "스마트 건설 활성화 방안(2022.07)",
"description": "• 추진과제: 건설산업 디지털화\n• 실행과제: BIM 전면 도입, BIM 전문인력 양성"
},
{
"title": "제7차 건설기술진흥 기본계획(2023.12)",
"description": "• 추진방향: 디지털 전환을 통한 스마트 건설 확산\n• 추진과제: BIM 도입으로 건설산업 디지털화"
}
]
}
},
3: {
"type": "keyword-circle-row",
"area": "body",
"purpose": "핵심전달",
"data": {
"keywords": [
{"letter": "D", "label": "DX", "description": "BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능한 상위개념"},
{"letter": "G", "label": "GIS", "description": "지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공"},
{"letter": "B", "label": "BIM", "description": "시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리"},
{"letter": "T", "label": "디지털트윈", "description": "현실 세계의 물리적 객체를 디지털 환경에 동일하게 구현하는 기술"}
]
}
},
4: {
"type": "card-numbered",
"area": "sidebar",
"purpose": "용어정의",
"data": {
"items": [
{
"title": "건설산업",
"description": "부동산 개발, 설계, 시공, 유지보수를 포괄하는 종합산업으로, 광범위한 기술을 통합·융합하여 인프라를 만드는 산업"
},
{
"title": "BIM",
"description": "형상정보와 속성정보가 포함된 3D 모델로 건설 정보 기반의 Process와 Product를 제공하는 도구"
},
{
"title": "DX",
"description": "디지털 기술을 활용하여 업무방식과 가치 창출 구조를 전환하는 과정 및 결과"
}
]
}
},
5: {
"type": "banner-gradient",
"area": "footer",
"purpose": "결론강조",
"data": {
"text": "BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다",
"sub_text": "각 용어의 정의, 역할, 상호관계에 대한 체계적 정립 필요"
}
}
}
# ═══════════════════════════════════════
# 방향 1: 컨테이너 고정, 블록을 맞춤 (폰트 축소 + 간격 압축)
# ═══════════════════════════════════════
print("=== 방향 1: 컨테이너 고정, 블록 축소 ===")
container_specs_1 = calculate_container_specs(page_structure, topics, preset)
blocks_1 = _build_blocks(filled_data, topics)
layout_1 = _build_layout(analysis, preset, blocks_1, container_specs_1)
# body area에 강제 축소 CSS
layout_1["pages"][0]["area_styles"] = {
"body": "--font-body: 0.7rem; --spacing-inner: 6px; --spacing-block: 6px; --font-subtitle: 0.9rem;",
"sidebar": "--font-body: 0.8rem; --spacing-inner: 10px;",
"footer": "",
}
html_1 = render_slide(layout_1)
m_1 = await asyncio.to_thread(measure_rendered_heights, html_1)
s_1 = await asyncio.to_thread(capture_slide_screenshot, html_1)
_save_result(out_dir, "direction_1", html_1, s_1, m_1)
_print_measurement(m_1, "방향 1")
# ═══════════════════════════════════════
# 방향 2: 텍스트 분량 기반 컨테이너 재조정
# ═══════════════════════════════════════
print("\n=== 방향 2: 컨테이너 재조정 (텍스트 기반) ===")
# 배경 비중을 올리고 본심을 줄임
adjusted_structure = copy.deepcopy(page_structure)
adjusted_structure["배경"]["weight"] = 0.45 # 0.3 → 0.45
adjusted_structure["본심"]["weight"] = 0.40 # 0.5 → 0.40
adjusted_structure["결론"]["weight"] = 0.05 # 0.1 → 0.05
container_specs_2 = calculate_container_specs(adjusted_structure, topics, preset)
blocks_2 = _build_blocks(filled_data, topics)
layout_2 = _build_layout(analysis, preset, blocks_2, container_specs_2)
layout_2["pages"][0]["area_styles"] = {
"body": "--font-body: 0.85rem; --spacing-inner: 10px; --spacing-block: 12px;",
"sidebar": "--font-body: 0.85rem; --spacing-inner: 10px;",
"footer": "--font-body: 0.85rem;",
}
html_2 = render_slide(layout_2)
m_2 = await asyncio.to_thread(measure_rendered_heights, html_2)
s_2 = await asyncio.to_thread(capture_slide_screenshot, html_2)
_save_result(out_dir, "direction_2", html_2, s_2, m_2)
_print_measurement(m_2, "방향 2")
# ═══════════════════════════════════════
# 방향 3: Two-Pass (텍스트 기반 + Kei 비중 보정)
# ═══════════════════════════════════════
print("\n=== 방향 3: Two-Pass (Kei 비중 ± 보정) ===")
# 1st pass: 원래 비중으로 컨테이너 계산
container_specs_3_raw = calculate_container_specs(page_structure, topics, preset)
# 텍스트 분량 추정 (글자 수 기반)
topic_char_counts = {}
for tid, data in filled_data.items():
chars = len(json.dumps(data["data"], ensure_ascii=False))
topic_char_counts[tid] = chars
# 각 역할의 텍스트 총량
role_chars = {}
for role, spec in container_specs_3_raw.items():
total = sum(topic_char_counts.get(tid, 0) for tid in spec.topic_ids)
role_chars[role] = total
# 2nd pass: 텍스트 비율로 비중 보정 (Kei 비중 ±20% 범위)
total_chars = sum(role_chars.values()) or 1
adjusted_structure_3 = copy.deepcopy(page_structure)
for role in adjusted_structure_3:
if not isinstance(adjusted_structure_3[role], dict):
continue
original_weight = adjusted_structure_3[role].get("weight", 0.25)
char_ratio = role_chars.get(role, 0) / total_chars
# Kei 비중과 텍스트 비율의 가중 평균 (Kei 60%, 텍스트 40%)
adjusted_weight = original_weight * 0.6 + char_ratio * 0.4
# ±20% 범위 제한
min_w = original_weight * 0.8
max_w = original_weight * 1.2
adjusted_weight = max(min_w, min(max_w, adjusted_weight))
adjusted_structure_3[role]["weight"] = round(adjusted_weight, 3)
# 비중 합계 정규화
total_w = sum(
v["weight"] for v in adjusted_structure_3.values() if isinstance(v, dict) and "weight" in v
)
if total_w > 0:
for role in adjusted_structure_3:
if isinstance(adjusted_structure_3[role], dict) and "weight" in adjusted_structure_3[role]:
adjusted_structure_3[role]["weight"] = round(
adjusted_structure_3[role]["weight"] / total_w, 3
)
container_specs_3 = calculate_container_specs(adjusted_structure_3, topics, preset)
blocks_3 = _build_blocks(filled_data, topics)
layout_3 = _build_layout(analysis, preset, blocks_3, container_specs_3)
layout_3["pages"][0]["area_styles"] = {
"body": "--font-body: 0.85rem; --spacing-inner: 10px; --spacing-block: 12px;",
"sidebar": "--font-body: 0.85rem; --spacing-inner: 10px;",
"footer": "--font-body: 0.85rem;",
}
html_3 = render_slide(layout_3)
m_3 = await asyncio.to_thread(measure_rendered_heights, html_3)
s_3 = await asyncio.to_thread(capture_slide_screenshot, html_3)
_save_result(out_dir, "direction_3", html_3, s_3, m_3)
_print_measurement(m_3, "방향 3")
# 비중 비교 출력
print("\n=== 비중 비교 ===")
print(f"{'역할':<6} {'원본':<8} {'방향2':<8} {'방향3':<8}")
for role in ["본심", "배경", "첨부", "결론"]:
orig = page_structure.get(role, {}).get("weight", 0)
d2 = adjusted_structure.get(role, {}).get("weight", 0)
d3 = adjusted_structure_3.get(role, {}).get("weight", 0)
print(f"{role:<6} {orig:<8.2f} {d2:<8.2f} {d3:<8.3f}")
print(f"\n결과물: {out_dir}")
print(" direction_1_screenshot.png — 방향 1: 컨테이너 고정, 폰트/간격 축소")
print(" direction_2_screenshot.png — 방향 2: 컨테이너 재조정 (수동)")
print(" direction_3_screenshot.png — 방향 3: Two-Pass (자동 보정)")
def _build_blocks(filled_data, topics):
blocks = []
# sidebar label
sidebar_tids = [tid for tid, d in filled_data.items() if d["area"] == "sidebar"]
if sidebar_tids:
blocks.append({
"area": "sidebar", "type": "divider-text",
"topic_id": None, "purpose": "_label",
"data": {"text": "용어 정의"}, "size": "compact",
})
role_order = {"배경": [1, 2], "본심": [3], "첨부": [4], "결론": [5]}
for role, tids in role_order.items():
for tid in tids:
if tid in filled_data:
block = {
"type": filled_data[tid]["type"],
"topic_id": tid,
"area": filled_data[tid]["area"],
"purpose": filled_data[tid]["purpose"],
"data": filled_data[tid]["data"],
}
blocks.append(block)
return blocks
def _build_layout(analysis, preset, blocks, container_specs):
return {
"title": analysis.get("title", "슬라이드"),
"_container_specs": container_specs,
"pages": [{
"grid_areas": preset["grid_areas"],
"grid_columns": preset["grid_columns"],
"grid_rows": preset["grid_rows"],
"blocks": blocks,
"area_styles": {},
}],
}
def _save_result(out_dir, name, html, screenshot_b64, measurement):
import base64
(out_dir / f"{name}.html").write_text(html, encoding="utf-8")
if screenshot_b64:
(out_dir / f"{name}_screenshot.png").write_bytes(base64.b64decode(screenshot_b64))
(out_dir / f"{name}_measurement.json").write_text(
json.dumps(measurement, ensure_ascii=False, indent=2), encoding="utf-8"
)
def _print_measurement(m, label):
for name, data in m.get("containers", {}).items():
status = "" if not data.get("overflowed") else f"❌ +{data.get('excess_px', 0)}px"
print(f" {name}: {data.get('scrollHeight', 0)}px / {data.get('allocatedHeight', 0)}px {status}")
slide = m.get("slide", {})
status = "" if not slide.get("overflowed") else ""
print(f" slide: {slide.get('scrollHeight', 0)}px / 720px {status}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

369
scripts/test_hybrid.py Normal file
View File

@@ -0,0 +1,369 @@
"""하이브리드 시뮬레이션: 기존 블록 활용 + 필요 시 변형/조합.
블록 사용 현황:
- card-icon-desc: 목표 3카드 ← 기존 블록 그대로
- dark-bullet-list: 변형 — 불릿 대신 Before→After 구조 (CSS만 추가)
- table-simple-striped: 주체별 효과 ← 기존 블록 그대로
- banner-gradient: 결론 ← 기존 블록 그대로
- 섹션 구분: divider-text 스타일 활용
블록 사용률: ~70% 기존 블록 + ~30% 변형/자유
"""
from __future__ import annotations
import asyncio, json, sys, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
HYBRID_HTML = """<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>하이브리드 — DX 시행 목표 및 기대 효과</title>
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
.slide {
width: 1280px; height: 720px; overflow: hidden;
background: #ffffff;
font-family: 'Pretendard Variable', sans-serif;
color: #1e293b;
font-size: 13px;
line-height: 1.6;
word-break: keep-all;
display: grid;
grid-template-areas: 'header header' 'body sidebar' 'footer footer';
grid-template-columns: 65fr 35fr;
grid-template-rows: auto 1fr auto;
gap: 16px;
padding: 36px 40px 24px;
}
/* ── 슬라이드 제목 (기존 base.css) ── */
.slide-title {
grid-area: header;
font-size: 28px;
font-weight: 900;
color: #1e293b;
border-bottom: 3px solid #2563eb;
padding-bottom: 8px;
}
/* ── Body ── */
.area-body {
grid-area: body;
display: flex;
flex-direction: column;
gap: 10px;
overflow: hidden;
}
/* ── Sidebar ── */
.area-sidebar {
grid-area: sidebar;
display: flex;
flex-direction: column;
gap: 12px;
border-left: 1px solid #e2e8f0;
padding-left: 20px;
overflow: hidden;
}
/* ── Footer ── */
.area-footer {
grid-area: footer;
}
/* ════════════════════════════════════════
블록 1: card-icon-desc (기존 블록 100% 재사용)
목표 3카드
════════════════════════════════════════ */
.block-card-icon {
display: grid;
grid-template-columns: repeat(var(--ci-count, 3), 1fr);
gap: 16px;
}
.cid-card {
text-align: center;
padding: 14px 12px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.cid-icon {
font-size: 2rem;
margin-bottom: 6px;
}
.cid-title {
font-size: 14px;
font-weight: 700;
color: #1e293b;
margin-bottom: 4px;
}
.cid-desc {
font-size: 11px;
color: #475569;
line-height: 1.6;
white-space: pre-line;
}
/* ════════════════════════════════════════
블록 2: dark-bullet-list 변형 — Before→After 구조
기존 dark-bullet-list의 색상/배경/radius 그대로 사용
불릿 대신 label + before + after 구조로 변형
════════════════════════════════════════ */
.block-dark-bullets {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-radius: 8px;
padding: 14px 20px;
color: #ffffff;
}
.db-title {
font-size: 13px;
font-weight: 700;
margin-bottom: 8px;
color: #93c5fd;
}
/* 변형: Before→After 그리드 */
.db-changes {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.db-change {
background: rgba(255,255,255,0.06);
border-radius: 6px;
padding: 8px 10px;
border-left: 3px solid #60a5fa;
}
.db-change-label {
font-size: 11px;
font-weight: 700;
color: #93c5fd;
margin-bottom: 3px;
}
.db-change-before {
font-size: 10px;
color: #94a3b8;
text-decoration: line-through;
}
.db-change-after {
font-size: 11px;
color: #e2e8f0;
font-weight: 500;
margin-top: 2px;
}
/* ════════════════════════════════════════
블록 3: divider-text (기존 블록 100% 재사용)
════════════════════════════════════════ */
.block-divider-text {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 0;
}
.dt-line {
flex: 1;
height: 1px;
background: #cbd5e1;
}
.dt-text {
font-size: 13px;
font-weight: 600;
color: #64748b;
white-space: nowrap;
}
/* ════════════════════════════════════════
블록 4: table-simple-striped (기존 블록 100% 재사용)
주체별 기대효과
════════════════════════════════════════ */
.block-table-striped table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
line-height: 1.6;
}
.block-table-striped thead th {
background: #1e293b;
color: #ffffff;
font-weight: 700;
padding: 8px 12px;
text-align: left;
font-size: 12px;
}
.block-table-striped tbody td {
padding: 7px 12px;
border-bottom: 1px solid #e2e8f0;
white-space: pre-line;
color: #334155;
font-size: 11px;
}
.block-table-striped tbody tr:nth-child(even) {
background: #f8fafc;
}
.block-table-striped tbody td:first-child {
font-weight: 600;
color: #1e293b;
}
/* ════════════════════════════════════════
블록 5: banner-gradient (기존 블록 100% 재사용)
결론
════════════════════════════════════════ */
.block-banner-grad {
background: linear-gradient(135deg, #006aff 0%, #00aaff 100%);
border-radius: 8px;
padding: 14px 30px;
text-align: center;
color: #ffffff;
}
.bg-text {
font-size: 15px;
font-weight: 700;
line-height: 1.5;
}
/* ════════════════════════════════════════
섹션 타이틀 (기존 디자인 토큰 활용)
════════════════════════════════════════ */
.section-label {
font-size: 13px;
font-weight: 900;
color: #2563eb;
}
</style>
</head>
<body>
<div class="slide">
<div class="slide-title">DX 시행 목표 및 기대 효과</div>
<div class="area-body">
<!-- 블록 1: card-icon-desc (기존 블록 그대로) → 목표 3카드 -->
<div class="section-label">DX를 통한 궁극적 목표</div>
<div class="block-card-icon" style="--ci-count: 3">
<div class="cid-card">
<div class="cid-icon">🛡️</div>
<div class="cid-title">안전과 품질</div>
<div class="cid-desc">설계-시공-운영 전 과정에서 디지털로 검증하여 안전성 확보
하자 최소화로 고품질 성과물 제공</div>
</div>
<div class="cid-card">
<div class="cid-icon">⚡</div>
<div class="cid-title">생산성 향상</div>
<div class="cid-desc">Analogue → Digital 프로세스 전환
비용 절감, 기간 단축, 인력투입 최소화로 부가가치 제고</div>
</div>
<div class="cid-card">
<div class="cid-icon">🤝</div>
<div class="cid-title">소통과 신뢰</div>
<div class="cid-desc">협업 강화로 의사소통 효율 증진
3D 모델·데이터 기반 검증으로 오류 최소화 및 Claim 예방</div>
</div>
</div>
<!-- 블록 2: dark-bullet-list 변형 → 프로세스 변화 4가지 (Before→After) -->
<div class="block-dark-bullets">
<div class="db-title">업무 수행 과정(Process)의 변화</div>
<div class="db-changes">
<div class="db-change">
<div class="db-change-label">생산 방식</div>
<div class="db-change-before">수작업 의존의 반복 업무</div>
<div class="db-change-after">→ SW를 활용한 체계화된 방식으로 전환</div>
</div>
<div class="db-change">
<div class="db-change-label">인지·검토</div>
<div class="db-change-before">2D 도면 해석 중심</div>
<div class="db-change-after">→ 3D 모델 기반의 직관적 인지·검토 체계</div>
</div>
<div class="db-change">
<div class="db-change-label">협업 구조</div>
<div class="db-change-before">개별 문서 중심 협업</div>
<div class="db-change-after">→ 데이터 통합 기반 정보 공유·관리 환경</div>
</div>
<div class="db-change">
<div class="db-change-label">검증·대응</div>
<div class="db-change-before">사후 대응 중심 문제 처리</div>
<div class="db-change-after">→ 사전 검증 중심의 예방적 업무 방식</div>
</div>
</div>
</div>
</div>
<div class="area-sidebar">
<!-- 블록 3: divider-text (기존 블록 그대로) -->
<div class="block-divider-text">
<div class="dt-line"></div>
<div class="dt-text">주체별 기대효과</div>
<div class="dt-line"></div>
</div>
<!-- 블록 4: table-simple-striped (기존 블록 그대로) -->
<div class="block-table-striped">
<table>
<thead>
<tr><th>주체</th><th>기대효과</th></tr>
</thead>
<tbody>
<tr><td>발주처</td><td>품질 향상, 비용·기간 절감, 투명한 관리</td></tr>
<tr><td>설계사</td><td>오류 감소, 설계 품질 제고, 재작업 최소화</td></tr>
<tr><td>시공사</td><td>공정 최적화, 안전 강화, 현장 생산성 향상</td></tr>
<tr><td>감리·CM</td><td>실시간 모니터링, 데이터 기반 의사결정</td></tr>
<tr><td>유지관리</td><td>디지털 트윈 기반 예방 정비, 자산 관리 효율화</td></tr>
</tbody>
</table>
</div>
</div>
<!-- 블록 5: banner-gradient (기존 블록 그대로) → 결론 -->
<div class="area-footer">
<div class="block-banner-grad">
<div class="bg-text">고품질의 성과품, 비용 절감, 시간 단축, 의사소통에 도움이 안 되면 DX가 아니다</div>
</div>
</div>
</div>
</body>
</html>"""
async def main():
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / "hybrid_simulation"
out_dir.mkdir(parents=True, exist_ok=True)
m = await asyncio.to_thread(measure_rendered_heights, HYBRID_HTML)
s = await asyncio.to_thread(capture_slide_screenshot, HYBRID_HTML)
(out_dir / "hybrid.html").write_text(HYBRID_HTML, encoding="utf-8")
if s:
(out_dir / "hybrid_screenshot.png").write_bytes(base64.b64decode(s))
slide = m.get("slide", {})
print(f"slide: {slide.get('scrollHeight', 0)}px / 720px {'' if not slide.get('overflowed') else ''}")
print(f"""
블록 사용 현황:
card-icon-desc → 목표 3카드 (기존 블록 100%)
dark-bullet-list → 프로세스 변화 (기존 색상/구조 + Before→After 변형)
divider-text → 섹션 구분 (기존 블록 100%)
table-simple-striped → 주체별 기대효과 (기존 블록 100%)
banner-gradient → 결론 (기존 블록 100%)
블록 활용률: 4/5 기존 블록 그대로 + 1/5 변형
결과: {out_dir}/hybrid_screenshot.png
""")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,201 @@
"""해법 방향 시뮬레이션: 콘텐츠 전달 의도에 맞는 블록 배치.
사용자가 지적한 문제:
- t1 (문제제기): 불릿 3줄 → 짧은 1-2줄이면 충분
- t2 (사례 비교): 세로 card-numbered → 가로 2열 비교
- t3 (핵심 DX≠BIM): 약어 원형 → DX와 BIM의 차이/관계를 보여주는 비교
- t4 (용어 정의): 태그 짧은 요약 → 풀 정의
시뮬레이션: 블록을 "전달 의도"에 맞게 수동 선택하여 렌더링.
Kei API 불필요 — 렌더링만.
"""
from __future__ import annotations
import asyncio
import json
import sys
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.renderer import render_slide
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
from src.design_director import select_preset, LAYOUT_PRESETS
from src.space_allocator import calculate_container_specs
import base64
import copy
run_dir = ROOT / "data" / "runs" / "1774736083771"
analysis = json.loads((run_dir / "step1_analysis.json").read_text(encoding="utf-8"))
concepts = json.loads((run_dir / "step1b_concepts.json").read_text(encoding="utf-8"))
concept_map = {c["id"]: c for c in concepts.get("concepts", [])}
for topic in analysis.get("topics", []):
tid = topic["id"]
if tid in concept_map:
topic["relation_type"] = concept_map[tid].get("relation_type", "none")
topics = analysis["topics"]
preset_name = select_preset(analysis)
preset = LAYOUT_PRESETS[preset_name]
out_dir = ROOT / "data" / "runs" / "ideal_simulation"
out_dir.mkdir(parents=True, exist_ok=True)
# ═══════════════════════════════════════
# 시뮬레이션 A: 전달 의도에 맞는 블록 선택
# 컨테이너 비중도 콘텐츠에 맞게 조정
# ═══════════════════════════════════════
print("=== 시뮬레이션 A: 전달 의도 기반 블록 배치 ===")
# 비중 조정: 본심(핵심 비교)이 가장 크고, 배경은 간결하게
adjusted_structure = copy.deepcopy(analysis["page_structure"])
adjusted_structure["본심"]["weight"] = 0.55
adjusted_structure["배경"]["weight"] = 0.25
adjusted_structure["결론"]["weight"] = 0.10
adjusted_structure["첨부"]["weight"] = 0.10
container_specs = calculate_container_specs(adjusted_structure, topics, preset)
blocks = []
# sidebar label
blocks.append({
"area": "sidebar", "type": "divider-text",
"topic_id": None, "purpose": "_label",
"data": {"text": "용어 정의"}, "size": "compact",
})
# t1 (배경 - 문제제기): 짧은 인용 한 줄 — quote-big-mark
blocks.append({
"type": "quote-big-mark",
"topic_id": 1,
"area": "body",
"purpose": "문제제기",
"data": {
"quote_text": "건설산업에서 DX와 BIM이 동일 개념으로 인식되고 있다",
"source": ""
}
})
# t2 (배경 - 사례 비교): 가로 2열 비교 — comparison-2col
blocks.append({
"type": "comparison-2col",
"topic_id": 2,
"area": "body",
"purpose": "근거사례",
"data": {
"left_title": "스마트건설 활성화 방안",
"left_subtitle": "2022.07",
"left_content": "• 추진과제: 건설산업 디지털화\n• 실행과제: BIM 전면 도입, BIM 전문인력 양성",
"right_title": "제7차 건설기술진흥 기본계획",
"right_subtitle": "2023.12",
"right_content": "• 추진방향: 디지털 전환을 통한 스마트 건설 확산\n• 추진과제: BIM 도입으로 건설산업 디지털화"
}
})
# t3 (본심 - 핵심): DX vs BIM 차이 — comparison-2col (큰 비교)
blocks.append({
"type": "comparison-2col",
"topic_id": 3,
"area": "body",
"purpose": "핵심전달",
"data": {
"left_title": "DX (상위개념)",
"left_subtitle": "Digital Transformation",
"left_content": "• BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능한 상위개념\n• Engineering + Management 통합\n• 근본적 문제의식을 통한 개선\n• 전 생애주기 활용 시스템\n• 자체 수행 능력 — 지속가능성 확보",
"right_title": "BIM (하위기술)",
"right_subtitle": "Building Information Modeling",
"right_content": "• 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구\n• Only 3D (형상 구현 중심)\n• 기존 2D 설계 방식 유지\n• (설계/시공/운영) 분야별 단절\n• S/W 제작사 판매 정책에 의존"
}
})
# t4 (sidebar - 용어 정의): 풀 정의 — card-numbered
blocks.append({
"type": "card-numbered",
"topic_id": 4,
"area": "sidebar",
"purpose": "용어정의",
"data": {
"items": [
{
"title": "건설산업",
"description": "부동산 개발, 설계, 시공, 유지보수를 포괄하는 종합산업으로, 광범위한 기술을 통합·융합하여 인프라를 만드는 산업"
},
{
"title": "BIM",
"description": "형상정보와 속성정보가 포함된 3D 모델로, 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구"
},
{
"title": "DX",
"description": "디지털 기술을 활용하여 업무방식과 가치 창출 구조를 전환하는 과정 및 결과. BIM, GIS, 디지털 트윈의 기술융합을 통해서만 실현 가능한 상위개념"
}
]
}
})
# t5 (footer - 결론): 원문 그대로 — banner-gradient
blocks.append({
"type": "banner-gradient",
"topic_id": 5,
"area": "footer",
"purpose": "결론강조",
"data": {
"text": "BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다",
"sub_text": "각 용어의 정의, 역할, 상호관계에 대한 체계적 정립 필요"
}
})
layout = {
"title": analysis.get("title", "슬라이드"),
"_container_specs": container_specs,
"pages": [{
"grid_areas": preset["grid_areas"],
"grid_columns": preset["grid_columns"],
"grid_rows": preset["grid_rows"],
"blocks": blocks,
"area_styles": {
"body": "--font-body: 0.85rem; --spacing-inner: 10px; --spacing-block: 10px;",
"sidebar": "--font-body: 0.82rem; --spacing-inner: 8px; --spacing-block: 10px;",
"footer": "",
},
}],
}
html = render_slide(layout)
m = await asyncio.to_thread(measure_rendered_heights, html)
s = await asyncio.to_thread(capture_slide_screenshot, html)
_save(out_dir, "sim_a.html", html)
if s:
import base64 as b64
(out_dir / "sim_a_screenshot.png").write_bytes(b64.b64decode(s))
_save(out_dir, "sim_a_measurement.json", m)
print("컨테이너:")
for name, data in m.get("containers", {}).items():
status = "" if not data.get("overflowed") else f"❌ +{data.get('excess_px', 0)}px"
print(f" {name}: {data.get('scrollHeight', 0)}px / {data.get('allocatedHeight', 0)}px {status}")
slide = m.get("slide", {})
print(f" slide: {slide.get('scrollHeight', 0)}px / 720px {'' if not slide.get('overflowed') else ''}")
print(f"\n결과: {out_dir}/sim_a_screenshot.png")
def _save(out_dir, name, data):
path = out_dir / name
if isinstance(data, str):
path.write_text(data, encoding="utf-8")
else:
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

426
scripts/test_ideal_v2.py Normal file
View File

@@ -0,0 +1,426 @@
"""이상적인 슬라이드 시뮬레이션 v2.
콘텐츠의 전달 의도를 정확히 반영한 블록 배치.
핵심: "DX와 BIM은 다르다. BIM은 DX의 일부다."를 독자가 이해하게 하는 것.
Kei API 불필요 — 순수 렌더링만.
"""
from __future__ import annotations
import asyncio
import json
import sys
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
# 직접 HTML을 작성하여 렌더링
SLIDE_HTML = """<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>건설산업 DX의 올바른 이해</title>
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* { margin: 0; padding: 0; box-sizing: border-box; }
.slide {
width: 1280px;
height: 720px;
overflow: hidden;
background: #ffffff;
font-family: 'Pretendard Variable', sans-serif;
color: #1e293b;
font-size: 14px;
line-height: 1.6;
word-break: keep-all;
display: grid;
grid-template-areas:
'header header'
'body sidebar'
'footer footer';
grid-template-columns: 65fr 35fr;
grid-template-rows: auto 1fr auto;
gap: 16px;
padding: 36px 40px 24px;
}
/* ── 제목 ── */
.header {
grid-area: header;
font-size: 28px;
font-weight: 900;
color: #1e293b;
border-bottom: 3px solid #2563eb;
padding-bottom: 8px;
}
/* ── Body ── */
.body {
grid-area: body;
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
}
/* ── 배경: 문제 제기 (간결한 1블록) ── */
.problem-box {
background: linear-gradient(135deg, #1e293b, #0f172a);
border-radius: 8px;
padding: 16px 24px;
color: #fff;
}
.problem-title {
font-size: 13px;
font-weight: 700;
color: #93c5fd;
margin-bottom: 6px;
}
.problem-text {
font-size: 13px;
line-height: 1.7;
color: #e2e8f0;
}
.problem-text strong {
color: #fbbf24;
font-weight: 700;
}
.problem-cases {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 10px;
}
.case-card {
background: rgba(255,255,255,0.08);
border-radius: 6px;
padding: 10px 14px;
border-left: 3px solid #60a5fa;
}
.case-label {
font-size: 11px;
font-weight: 700;
color: #93c5fd;
margin-bottom: 4px;
}
.case-content {
font-size: 11px;
color: #cbd5e1;
line-height: 1.6;
}
/* ── 본심: 핵심 관계 시각화 ── */
.core-section {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
}
.core-label {
font-size: 14px;
font-weight: 800;
color: #2563eb;
text-align: center;
}
/* 포함 관계 시각화 */
.hierarchy-visual {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.dx-outer {
width: 100%;
max-width: 620px;
border: 3px solid #2563eb;
border-radius: 16px;
padding: 16px 20px 14px;
position: relative;
background: linear-gradient(135deg, #eff6ff, #dbeafe);
}
.dx-label {
position: absolute;
top: -12px;
left: 20px;
background: #2563eb;
color: white;
font-size: 13px;
font-weight: 800;
padding: 2px 16px;
border-radius: 10px;
}
.dx-desc {
font-size: 11px;
color: #1e40af;
margin-bottom: 10px;
text-align: center;
}
.tech-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
}
.tech-card {
background: white;
border: 2px solid #93c5fd;
border-radius: 10px;
padding: 10px;
text-align: center;
}
.tech-icon {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #93c5fd, #2563eb);
color: white;
font-size: 16px;
font-weight: 900;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 6px;
}
.tech-name {
font-size: 13px;
font-weight: 700;
color: #1e293b;
margin-bottom: 3px;
}
.tech-desc {
font-size: 10px;
color: #64748b;
line-height: 1.5;
}
/* 핵심 메시지 */
.core-message {
background: #f0f9ff;
border: 2px solid #bae6fd;
border-radius: 8px;
padding: 10px 16px;
text-align: center;
}
.core-message-text {
font-size: 13px;
font-weight: 700;
color: #0c4a6e;
line-height: 1.5;
}
.core-message-text em {
color: #dc2626;
font-style: normal;
font-weight: 800;
}
/* ── Sidebar ── */
.sidebar {
grid-area: sidebar;
display: flex;
flex-direction: column;
gap: 12px;
border-left: 1px solid #e2e8f0;
padding-left: 20px;
}
.sidebar-label {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
font-weight: 600;
color: #94a3b8;
}
.sidebar-label::before, .sidebar-label::after {
content: '';
flex: 1;
height: 1px;
background: #e2e8f0;
}
.def-item {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px 14px;
}
.def-num {
display: inline-flex;
width: 24px;
height: 24px;
border-radius: 50%;
background: #2563eb;
color: white;
font-size: 12px;
font-weight: 800;
align-items: center;
justify-content: center;
margin-right: 8px;
vertical-align: middle;
}
.def-title {
font-size: 14px;
font-weight: 700;
color: #1e293b;
display: inline;
vertical-align: middle;
}
.def-desc {
font-size: 12px;
color: #475569;
line-height: 1.6;
margin-top: 6px;
}
.def-source {
font-size: 10px;
color: #94a3b8;
font-style: italic;
margin-top: 4px;
}
/* ── Footer ── */
.footer {
grid-area: footer;
background: linear-gradient(135deg, #006aff, #00aaff);
border-radius: 8px;
padding: 14px 30px;
text-align: center;
color: white;
}
.footer-text {
font-size: 15px;
font-weight: 700;
}
.footer-sub {
font-size: 11px;
opacity: 0.85;
margin-top: 2px;
}
</style>
</head>
<body>
<div class="slide">
<div class="header">건설산업 DX의 올바른 이해</div>
<div class="body">
<!-- 배경: 문제 제기 + 사례 (1블록에 통합) -->
<div class="problem-box">
<div class="problem-title">현실 — 용어의 혼용</div>
<div class="problem-text">
건설산업에서 <strong>DX와 BIM이 동일 개념으로 인식</strong>되고 있다.
DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 하위 기술에 해당한다.
</div>
<div class="problem-cases">
<div class="case-card">
<div class="case-label">스마트 건설 활성화 방안 (2022.07)</div>
<div class="case-content">추진과제: 건설산업 디지털화<br>실행과제: BIM 전면 도입, BIM 전문인력 양성</div>
</div>
<div class="case-card">
<div class="case-label">제7차 건설기술진흥 기본계획 (2023.12)</div>
<div class="case-content">추진방향: 디지털 전환을 통한 스마트 건설 확산<br>추진과제: BIM 도입으로 건설산업 디지털화</div>
</div>
</div>
</div>
<!-- 본심: DX ⊃ BIM 포함 관계 시각화 -->
<div class="core-section">
<div class="core-label">DX와 핵심기술의 올바른 관계</div>
<div class="hierarchy-visual">
<div class="dx-outer">
<div class="dx-label">DX — 디지털 전환 (상위개념)</div>
<div class="dx-desc">BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능</div>
<div class="tech-row">
<div class="tech-card">
<div class="tech-icon">G</div>
<div class="tech-name">GIS</div>
<div class="tech-desc">지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</div>
</div>
<div class="tech-card">
<div class="tech-icon">B</div>
<div class="tech-name">BIM</div>
<div class="tech-desc">시설물 생애주기 정보를 3차원 모델 기반으로 통합·관리하는 도구</div>
</div>
<div class="tech-card">
<div class="tech-icon">T</div>
<div class="tech-name">디지털 트윈</div>
<div class="tech-desc">현실 세계의 물리적 객체를 디지털 환경에 동일하게 구현</div>
</div>
</div>
</div>
</div>
<div class="core-message">
<div class="core-message-text">
DX는 이들 기술을 통합하여 업무방식과 가치 창출 구조를 근본적으로 전환하는 과정이다.<br>
<em>BIM ≠ DX</em> — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다.
</div>
</div>
</div>
</div>
<div class="sidebar">
<div class="sidebar-label">용어 정의</div>
<div class="def-item">
<span class="def-num">1</span>
<span class="def-title">건설산업</span>
<div class="def-desc">부동산 개발, 설계, 시공, 유지보수를 포괄하는 종합산업으로, 광범위한 기술을 통합·융합하여 인프라를 만드는 산업</div>
</div>
<div class="def-item">
<span class="def-num">2</span>
<span class="def-title">BIM</span>
<div class="def-desc">형상정보와 속성정보가 포함된 3D 모델로, 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="def-source">건설산업 BIM 기본지침, 국토교통부, 2020</div>
</div>
<div class="def-item">
<span class="def-num">3</span>
<span class="def-title">DX (디지털 전환)</span>
<div class="def-desc">디지털 기술을 활용하여 업무방식과 가치 창출 구조를 전환하는 과정 및 결과. 단순한 기술 도입이 아닌, 산업의 새로운 방향을 정립</div>
<div class="def-source">IBM Institute for Business Value, 2011</div>
</div>
</div>
<div class="footer">
<div class="footer-text">BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다</div>
<div class="footer-sub">각 용어의 정의, 역할, 상호관계에 대한 체계적 정립 필요</div>
</div>
</div>
</body>
</html>"""
async def main():
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
import base64
out_dir = ROOT / "data" / "runs" / "ideal_v2"
out_dir.mkdir(parents=True, exist_ok=True)
# 렌더링 + 측정
m = await asyncio.to_thread(measure_rendered_heights, SLIDE_HTML)
s = await asyncio.to_thread(capture_slide_screenshot, SLIDE_HTML)
(out_dir / "ideal_v2.html").write_text(SLIDE_HTML, encoding="utf-8")
if s:
(out_dir / "ideal_v2_screenshot.png").write_bytes(base64.b64decode(s))
print("=== 이상적인 슬라이드 v2 ===")
slide = m.get("slide", {})
print(f" slide: {slide.get('scrollHeight', 0)}px / 720px {'' if not slide.get('overflowed') else ''}")
for name, data in m.get("zones", {}).items():
status = "" if not data.get("overflowed") else f"❌ +{data.get('excess_px', 0)}px"
print(f" {name}: {data.get('scrollHeight', 0)}px / {data.get('clientHeight', 0)}px {status}")
print(f"\n결과: {out_dir}/ideal_v2_screenshot.png")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

346
scripts/test_phase_q.py Normal file
View File

@@ -0,0 +1,346 @@
"""Phase Q 단독 테스트 스크립트.
기존 run의 step1 결과물(analysis, concepts)을 재사용하여
블록 선택 → 콘텐츠 채우기 → 렌더링만 실행한다.
Kei 분석(~13분)을 건너뛰고 Phase Q 로직만 검증.
사용법:
python scripts/test_phase_q.py [run_id]
python scripts/test_phase_q.py 1774736083771
"""
from __future__ import annotations
import asyncio
import json
import sys
import time
from pathlib import Path
# 프로젝트 루트를 sys.path에 추가
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def run_phase_q_test(run_id: str):
"""기존 run의 step1 결과를 사용하여 Phase Q만 실행."""
from src.block_selector import select_block_candidates, select_fallback_candidates, load_catalog
from src.space_allocator import (
calculate_container_specs, finalize_block_specs, find_container_for_topic,
calculate_char_budget, calculate_budgets_for_candidates,
)
from src.design_director import select_preset, LAYOUT_PRESETS
from src.renderer import render_slide
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
run_dir = ROOT / "data" / "runs" / run_id
# 매 실행마다 새 폴더 생성 (타임스탬프)
import datetime
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
out_dir = ROOT / "data" / "runs" / f"{run_id}_q_{timestamp}"
out_dir.mkdir(parents=True, exist_ok=True)
print(f"[Phase Q 테스트] run={run_id}")
print(f" 입력: {run_dir}")
print(f" 출력: {out_dir}")
print()
# ── Step 1 결과 로딩 (기존 것 재사용) ──
analysis = json.loads((run_dir / "step1_analysis.json").read_text(encoding="utf-8"))
concepts = json.loads((run_dir / "step1b_concepts.json").read_text(encoding="utf-8"))
# concepts에서 relation_type을 analysis topics에 병합
concept_map = {c["id"]: c for c in concepts.get("concepts", [])}
for topic in analysis.get("topics", []):
tid = topic["id"]
if tid in concept_map:
topic["relation_type"] = concept_map[tid].get("relation_type", "none")
topic["expression_hint"] = concept_map[tid].get("expression_hint", "")
topic["source_data"] = concept_map[tid].get("source_data", "")
# 원본 콘텐츠 (step1에 저장 안 되어 있으면 직접 입력)
content_file = run_dir / "input_content.txt"
if content_file.exists():
content = content_file.read_text(encoding="utf-8")
else:
content = """# 건설산업 DX의 올바른 이해
## 용어의 혼용
건설산업에서 DX(Digital Transformation)와 BIM(Building Information Modeling)이 동일 개념으로 인식되고 있다.
실질적으로 DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 3차원 모델 기반의 정보 관리 도구로서 DX의 하위 기술에 해당한다.
## 혼용 대표 사례
1. 스마트 건설 활성화 방안(2022.07): 추진과제를 건설산업 디지털화로 명시하면서 실행과제는 BIM 전면 도입에 국한
2. 제7차 건설기술진흥 기본계획(2023.12): 추진방향을 디지털 전환으로 제시하면서 추진과제는 BIM 도입으로 한정
## DX와 핵심기술의 올바른 관계
DX는 BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능한 상위개념이다.
- GIS: 지리적 데이터를 공간 분석하여 시각적으로 표현
- BIM: 시설물 생애주기 정보를 3차원 모델로 통합 관리
- 디지털 트윈: 현실 객체를 디지털로 동일하게 구현
## 용어별 정의
- 건설산업: 광범위한 기술을 통합 융합하여 만드는 종합산업
- BIM: 3차원 모델 기반으로 통합 관리하는 정보 관리 도구
- DX: 업무방식과 가치 창출 구조를 전환하는 과정 및 결과
## 핵심 요약
BIM은 DX의 기초가 되는 일부분이다. 각 용어의 정의와 상호관계에 대한 체계적 정립이 필요하다.
"""
topics = analysis.get("topics", [])
page_structure = analysis.get("page_structure", {})
print(f" topics: {len(topics)}")
for t in topics:
print(f" t{t['id']}: {t['title']} (relation={t.get('relation_type', '?')}, purpose={t.get('purpose', '?')})")
print()
# ── 컨테이너 계산 ──
t0 = time.time()
preset_name = select_preset(analysis)
preset = LAYOUT_PRESETS.get(preset_name, {})
container_specs = calculate_container_specs(page_structure, topics, preset)
print(f"[{time.time()-t0:.1f}s] 컨테이너 계산 완료:")
for role, spec in container_specs.items():
print(f" {role}: {spec.height_px}px × {spec.width_px}px, topics={spec.topic_ids}")
_save(out_dir, "step1c_containers.json", {
role: {"height_px": s.height_px, "width_px": s.width_px, "topic_ids": s.topic_ids,
"max_height_cost": s.max_height_cost, "weight": s.weight}
for role, s in container_specs.items()
})
# ── Q-2: 블록 후보 필터링 (결정론적) ──
catalog = load_catalog()
used_blocks: set[str] = set()
candidates_per_topic: dict[int, list[dict]] = {}
budgets_per_topic: dict[int, dict[str, dict]] = {}
print(f"\n[{time.time()-t0:.1f}s] Q-2: 블록 후보 필터링")
for topic in topics:
tid = topic["id"]
spec = find_container_for_topic(tid, container_specs)
if not spec:
print(f" t{tid}: 컨테이너 없음!")
continue
candidates = select_block_candidates(topic, spec, used_blocks, catalog)
if not candidates:
candidates = select_fallback_candidates(spec, used_blocks, catalog)
print(f" t{tid}: fallback → {len(candidates)}")
candidates_per_topic[tid] = candidates
budgets_per_topic[tid] = calculate_budgets_for_candidates(candidates, spec)
per_topic_px = spec.height_px // max(1, len(spec.topic_ids))
print(f" t{tid} ({topic.get('relation_type', '?')}, {per_topic_px}px): "
f"{len(candidates)}개 → [{', '.join(c['id'] for c in candidates[:5])}]")
_save(out_dir, "step2_candidates.json", {
str(tid): [{"id": c["id"], "category": c.get("category")} for c in cs[:5]]
for tid, cs in candidates_per_topic.items()
})
# ── Q-4: Kei 블록 선택 (AI 1회) ──
print(f"\n[{time.time()-t0:.1f}s] Q-4: Kei 블록 선택 중... (AI 호출)")
from src.kei_client import select_block_for_topics
selections = None
for attempt in range(5):
selections = await select_block_for_topics(
topics, candidates_per_topic, budgets_per_topic,
container_specs, analysis
)
if selections:
break
print(f" 재시도 {attempt + 1}/5...")
await asyncio.sleep(10)
if not selections:
print(" ❌ Kei 블록 선택 실패")
return
print(f"[{time.time()-t0:.1f}s] 블록 선택 완료:")
selected_blocks: dict[int, dict] = {}
for topic in topics:
tid = topic["id"]
sel = selections.get(tid, {})
block_id = sel.get("block_id", "")
spec = find_container_for_topic(tid, container_specs)
if not block_id and candidates_per_topic.get(tid):
block_id = candidates_per_topic[tid][0]["id"]
used_blocks.add(block_id)
budget = budgets_per_topic.get(tid, {}).get(block_id, {})
variant = sel.get("variant", "default")
block = {
"type": block_id,
"_variant": variant,
"topic_id": tid,
"area": spec.zone if spec else "body",
"purpose": topic.get("purpose", ""),
"_char_budget": budget,
}
finalize_block_specs([block], container_specs)
selected_blocks[tid] = block
variant_label = f" [{variant}]" if variant != "default" else ""
print(f" t{tid}: {block_id}{variant_label} (예산: {budget.get('total_chars', '?')}자) — {sel.get('reason', '')[:50]}")
_save(out_dir, "step2_selection.json", {
str(tid): {"type": b["type"], "variant": b.get("_variant", "default"),
"area": b["area"], "budget": b.get("_char_budget", {}),
"reason": selections.get(tid, {}).get("reason", "")}
for tid, b in selected_blocks.items()
})
# ── layout_concept 조립 ──
final_blocks = []
# sidebar label
sidebar_tids = [tid for tid, b in selected_blocks.items() if b.get("area") == "sidebar"]
if sidebar_tids:
first_topic = next((t for t in topics if t["id"] == sidebar_tids[0]), {})
section_title = first_topic.get("section_title", "")
if not section_title:
purpose = first_topic.get("purpose", "")
section_title = {"용어정의": "용어 정의", "근거사례": "참고 자료"}.get(purpose, "")
if section_title:
final_blocks.append({
"area": "sidebar", "type": "divider-text",
"topic_id": None, "purpose": "_label",
"data": {"text": section_title}, "size": "compact",
})
role_order = ["배경", "본심", "첨부", "결론"]
for role in role_order:
spec = container_specs.get(role)
if not spec:
continue
for tid in spec.topic_ids:
block = selected_blocks.get(tid)
if block:
final_blocks.append(block)
layout_concept = {
"title": analysis.get("title", "슬라이드"),
"_container_specs": container_specs,
"pages": [{
"grid_areas": preset["grid_areas"],
"grid_columns": preset["grid_columns"],
"grid_rows": preset["grid_rows"],
"blocks": final_blocks,
}],
}
print(f"\n[{time.time()-t0:.1f}s] 레이아웃 조립: {len(final_blocks)}개 블록")
# ── Step 3: topic별 개별 호출 (Phase P fill_candidates 방식 복원) ──
print(f"[{time.time()-t0:.1f}s] Step 3: Kei 편집자 텍스트 채우기 중 (topic별 개별)...")
from src.content_editor import fill_candidates
for topic in topics:
tid = topic["id"]
block = selected_blocks.get(tid)
if not block:
continue
await fill_candidates(content, topic, [block], analysis)
has_data = bool(block.get("data"))
char_count = len(json.dumps(block.get("data", {}), ensure_ascii=False)) if has_data else 0
print(f" t{tid}: {block['type']}{'' if has_data else ''} ({char_count}자)")
blocks_with_data = [b for b in final_blocks if b.get("data") and b.get("topic_id") is not None]
blocks_without_data = [b for b in final_blocks if not b.get("data") and b.get("topic_id") is not None]
print(f"[{time.time()-t0:.1f}s] 텍스트 채우기 완료:")
print(f" 데이터 있음: {len(blocks_with_data)}개 — {[b['type'] for b in blocks_with_data]}")
if blocks_without_data:
print(f" 데이터 없음: {len(blocks_without_data)}개 — {[b['type'] for b in blocks_without_data]}")
_save(out_dir, "step3_fill_content.json", {
"filled": len(blocks_with_data),
"empty": len(blocks_without_data),
"blocks": [
{"type": b["type"], "topic_id": b.get("topic_id"),
"has_data": bool(b.get("data")),
"data_preview": str(b.get("data", {}))[:100]}
for b in final_blocks if b.get("topic_id") is not None
]
})
# ── Step 4: CSS 조정 + 렌더링 ──
print(f"\n[{time.time()-t0:.1f}s] Step 4: CSS 조정 + 렌더링...")
from src.pipeline import _adjust_design
layout_concept = await _adjust_design(layout_concept, analysis)
html = render_slide(layout_concept)
_save(out_dir, "step4_rendered.html", html)
print(f"[{time.time()-t0:.1f}s] HTML 생성: {len(html)}")
# ── 측정 ──
print(f"[{time.time()-t0:.1f}s] Selenium 측정 중...")
measurement = await asyncio.to_thread(measure_rendered_heights, html)
_save(out_dir, "step4_measurement.json", measurement)
has_overflow = False
for name, data in measurement.get("containers", {}).items():
status = "" if not data.get("overflowed") else ""
print(f" {name}: {data.get('scrollHeight', 0)}px / {data.get('allocatedHeight', 0)}px {status}")
if data.get("overflowed"):
has_overflow = True
slide_data = measurement.get("slide", {})
slide_status = "" if not slide_data.get("overflowed") else ""
print(f" slide: {slide_data.get('scrollHeight', 0)}px / 720px {slide_status}")
# ── 스크린샷 ──
screenshot_b64 = await asyncio.to_thread(capture_slide_screenshot, html)
if screenshot_b64:
import base64
png_path = out_dir / "screenshot.png"
png_path.write_bytes(base64.b64decode(screenshot_b64))
print(f"\n[{time.time()-t0:.1f}s] 스크린샷 저장: {png_path}")
# ── final.html 저장 ──
_save(out_dir, "final.html", html)
# ── 결과 요약 ──
total = time.time() - t0
print(f"\n{'='*50}")
print(f"Phase Q 테스트 완료: {total:.1f}")
print(f" 블록 다양성: {len(set(b['type'] for b in final_blocks))}종류")
print(f" 데이터 채움: {len(blocks_with_data)}/{len([b for b in final_blocks if b.get('topic_id') is not None])}")
print(f" overflow: {'없음 ✅' if not has_overflow else '있음 ❌'}")
print(f" 출력: {out_dir}")
print(f"{'='*50}")
def _save(out_dir: Path, filename: str, data):
path = out_dir / filename
if isinstance(data, str):
path.write_text(data, encoding="utf-8")
else:
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
if __name__ == "__main__":
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s %(message)s",
datefmt="%H:%M:%S",
)
# 너무 시끄러운 로거 조용히
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
run_id = sys.argv[1] if len(sys.argv) > 1 else "1774736083771"
asyncio.run(run_phase_q_test(run_id))

View File

@@ -0,0 +1,187 @@
"""Phase R' 테스트: 접근 C — 블록 CSS 참고 + AI 구조 결정.
기존 step1 결과를 재사용하여 html_generator로 HTML 직접 생성.
블록 선택(block_selector) 없음. 슬롯 채우기(fill_candidates) 없음.
AI가 콘텐츠에 맞는 HTML 구조를 직접 만든다.
사용법:
python scripts/test_phase_r_prime.py [run_id]
python scripts/test_phase_r_prime.py 1774736083771
"""
from __future__ import annotations
import asyncio
import json
import sys
import time
import datetime
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main(run_id: str):
from src.html_generator import generate_slide_html
from src.html_validator import validate_and_clean_html
from src.renderer import render_slide_from_html
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
from src.design_director import select_preset, LAYOUT_PRESETS
from src.space_allocator import calculate_container_specs
import base64
run_dir = ROOT / "data" / "runs" / run_id
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
out_dir = ROOT / "data" / "runs" / f"{run_id}_rprime_{timestamp}"
out_dir.mkdir(parents=True, exist_ok=True)
print(f"[Phase R' 테스트] run={run_id}")
print(f" 입력: {run_dir}")
print(f" 출력: {out_dir}")
print()
# ── Step 1 결과 로딩 (기존 것 재사용) ──
analysis = json.loads((run_dir / "step1_analysis.json").read_text(encoding="utf-8"))
concepts = json.loads((run_dir / "step1b_concepts.json").read_text(encoding="utf-8"))
concept_map = {c["id"]: c for c in concepts.get("concepts", [])}
for topic in analysis.get("topics", []):
tid = topic["id"]
if tid in concept_map:
topic["relation_type"] = concept_map[tid].get("relation_type", "none")
topic["expression_hint"] = concept_map[tid].get("expression_hint", "")
topic["source_data"] = concept_map[tid].get("source_data", "")
# 원본 콘텐츠
content = """# 건설산업 DX의 올바른 이해
## 용어의 혼용
건설산업에서 DX(Digital Transformation)와 BIM(Building Information Modeling)이 동일 개념으로 인식되고 있다.
실질적으로 DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 3차원 모델 기반의 정보 관리 도구로서 DX의 하위 기술에 해당한다.
## 혼용 대표 사례
1. 스마트 건설 활성화 방안(2022.07): 추진과제를 건설산업 디지털화로 명시하면서 실행과제는 BIM 전면 도입에 국한
2. 제7차 건설기술진흥 기본계획(2023.12): 추진방향을 디지털 전환으로 제시하면서 추진과제는 BIM 도입으로 한정
## DX와 핵심기술의 올바른 관계
DX는 BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능한 상위개념이다.
- GIS: 지리적 데이터를 공간 분석하여 시각적으로 표현
- BIM: 시설물 생애주기 정보를 3차원 모델로 통합 관리
- 디지털 트윈: 현실 객체를 디지털로 동일하게 구현
## 용어별 정의
- 건설산업: 광범위한 기술을 통합 융합하여 만드는 종합산업
- BIM: 3차원 모델 기반으로 통합 관리하는 정보 관리 도구
- DX: 업무방식과 가치 창출 구조를 전환하는 과정 및 결과
## 핵심 요약
BIM은 DX의 기초가 되는 일부분이다. 각 용어의 정의와 상호관계에 대한 체계적 정립이 필요하다.
"""
topics = analysis["topics"]
t0 = time.time()
# ── 컨테이너 계산 (유지) ──
preset_name = select_preset(analysis)
preset = LAYOUT_PRESETS[preset_name]
container_specs = calculate_container_specs(
analysis.get("page_structure", {}), topics, preset
)
print(f"[{time.time()-t0:.1f}s] 컨테이너 계산:")
for role, spec in container_specs.items():
print(f" {role}: {spec.height_px}px × {spec.width_px}px, topics={spec.topic_ids}")
_save(out_dir, "step1c_containers.json", {
role: {"height_px": s.height_px, "width_px": s.width_px, "topic_ids": s.topic_ids}
for role, s in container_specs.items()
})
# ══════════════════════════════════════
# ★ Phase R' 핵심: AI HTML 직접 생성
# block_selector 없음. fill_candidates 없음.
# ══════════════════════════════════════
print(f"\n[{time.time()-t0:.1f}s] ★ AI HTML 생성 중... (블록 선택 없음, AI가 구조 결정)")
generated = await generate_slide_html(
content=content,
analysis=analysis,
container_specs=container_specs,
preset=preset,
)
# HTML 정화 + 검증
generated = validate_and_clean_html(generated)
_save(out_dir, "step2_generated.json", {
"body_html_length": len(generated.get("body_html", "")),
"sidebar_html_length": len(generated.get("sidebar_html", "")),
"footer_html_length": len(generated.get("footer_html", "")),
"reasoning": generated.get("reasoning", ""),
})
print(f"[{time.time()-t0:.1f}s] HTML 생성 완료:")
print(f" body: {len(generated.get('body_html', ''))}")
print(f" sidebar: {len(generated.get('sidebar_html', ''))}")
print(f" footer: {len(generated.get('footer_html', ''))}")
print(f" 구조 결정 근거: {generated.get('reasoning', '')[:100]}")
# ── 렌더링 (AI HTML을 프레임에 삽입) ──
print(f"\n[{time.time()-t0:.1f}s] 렌더링...")
html = render_slide_from_html(generated, analysis, preset)
_save(out_dir, "step3_rendered.html", html)
_save(out_dir, "final.html", html)
# ── Selenium 측정 ──
print(f"[{time.time()-t0:.1f}s] Selenium 측정...")
measurement = await asyncio.to_thread(measure_rendered_heights, html)
_save(out_dir, "step4_measurement.json", measurement)
slide = measurement.get("slide", {})
print(f" slide: {slide.get('scrollHeight', 0)}px / 720px "
f"{'' if not slide.get('overflowed') else ''}")
for name, data in measurement.get("containers", {}).items():
status = "" if not data.get("overflowed") else f"❌ +{data.get('excess_px', 0)}px"
print(f" {name}: {data.get('scrollHeight', 0)}px / {data.get('allocatedHeight', 0)}px {status}")
# ── 스크린샷 ──
screenshot_b64 = await asyncio.to_thread(capture_slide_screenshot, html)
if screenshot_b64:
import base64 as b64
(out_dir / "screenshot.png").write_bytes(b64.b64decode(screenshot_b64))
print(f"\n[{time.time()-t0:.1f}s] 스크린샷: {out_dir / 'screenshot.png'}")
total = time.time() - t0
print(f"\n{'='*50}")
print(f"Phase R' 테스트 완료: {total:.1f}")
print(f" 블록 선택: 없음 (AI가 HTML 구조 직접 생성)")
print(f" 슬롯 채우기: 없음 (AI가 텍스트 직접 포함)")
print(f" 결과: {out_dir}")
print(f"{'='*50}")
def _save(out_dir, name, data):
path = out_dir / name
if isinstance(data, str):
path.write_text(data, encoding="utf-8")
else:
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
if __name__ == "__main__":
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s %(message)s",
datefmt="%H:%M:%S",
)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
run_id = sys.argv[1] if len(sys.argv) > 1 else "1774736083771"
asyncio.run(main(run_id))

248
scripts/verify_3issues.py Normal file
View File

@@ -0,0 +1,248 @@
"""Phase R' 검증: 3가지 문제를 각각 Kei API에 요청하여 가능 여부 확인.
검증 1: 배경 사례 2건이 박스 안에 온전히 들어가는 HTML
검증 2: DX/GIS/BIM/디지털트윈 상호 관계 시각화 HTML
검증 3: 용어 정의 풀 텍스트 + 출처 포함 HTML
각각 독립적으로 Kei API 호출 → 렌더링 → 스크린샷.
"""
from __future__ import annotations
import asyncio, json, sys, time, datetime, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.sse_utils import stream_sse_tokens
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
from src.config import settings
import httpx
out_dir = ROOT / "data" / "runs" / f"verify_3issues_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
print(f"출력: {out_dir}\n")
kei_url = getattr(settings, "kei_api_url", "http://localhost:8000")
t0 = time.time()
# ═══════════════════════════════════════
# 검증 1: 배경 — 문제 제기 + 사례 2건이 176px 안에 들어가는 HTML
# ═══════════════════════════════════════
print("=== 검증 1: 배경 사례 박스 ===")
prompt_1 = """다음 콘텐츠를 176px 높이 × 707px 너비의 다크 배경 박스 안에 HTML로 만들어라.
## 콘텐츠
- 제목: "현실 — 용어의 혼용"
- 본문: "건설산업에서 DX와 BIM이 동일 개념으로 인식되고 있다. 실질적으로 DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 3차원 모델 기반의 정보 관리 도구로서 DX의 하위 기술에 해당한다."
- 사례 1: "스마트 건설 활성화 방안(2022.07) — 추진과제: 건설산업 디지털화, 실행과제: BIM 전면 도입, BIM 전문인력 양성"
- 사례 2: "제7차 건설기술진흥 기본계획(2023.12) — 추진방향: 디지털 전환을 통한 스마트 건설 확산, 추진과제: BIM 도입으로 건설산업 디지털화"
## 요구사항
1. 다크 배경(#1e293b → #0f172a 그라데이션), 흰 텍스트
2. 제목: #93c5fd 색상
3. 사례 2건을 가로 나란히 카드로 배치 (border-left: 3px solid #60a5fa)
4. 사례 제목: #fbbf24 (노란색)
5. **176px 높이 안에 모든 내용이 들어가야 한다. 넘치면 안 된다.**
6. 본문과 사례의 텍스트를 축약하지 마라. 위에 제공한 텍스트 그대로 사용.
7. 폰트 크기를 줄여서라도 176px 안에 맞춰라 (최소 10px까지 허용)
## 출력
HTML + inline <style>만 반환. 설명 없이 코드만.
```html
(여기에 HTML)
```"""
html_1 = await _call_kei(kei_url, prompt_1)
if html_1:
wrapped_1 = _wrap_in_slide(html_1, 707, 176)
m_1 = await asyncio.to_thread(measure_rendered_heights, wrapped_1)
s_1 = await asyncio.to_thread(capture_slide_screenshot, wrapped_1)
_save(out_dir, "verify1_background.html", wrapped_1)
if s_1:
(out_dir / "verify1_background.png").write_bytes(base64.b64decode(s_1))
slide = m_1.get("slide", {})
print(f" [{time.time()-t0:.0f}s] 결과: {slide.get('scrollHeight', 0)}px / 720px")
print(f" HTML: {len(html_1)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 생성 실패")
# ═══════════════════════════════════════
# 검증 2: DX/GIS/BIM/디지털트윈 상호 관계 시각화
# ═══════════════════════════════════════
print("\n=== 검증 2: DX 관계 시각화 ===")
prompt_2 = """다음 관계를 시각화하는 HTML을 만들어라. 크기: 707px 너비 × 293px 높이.
## 관계 구조
- DX(디지털 전환)는 상위개념이다.
- DX 안에 GIS, BIM, 디지털 트윈이 포함된다.
- GIS, BIM, 디지털 트윈은 서로 연결/융합되어 DX를 실현한다.
- "BIM ≠ DX" — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다.
## 각 기술 설명 (원본 그대로 사용)
- DX: "BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능한 상위개념"
- GIS: "지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공"
- BIM: "시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구"
- 디지털 트윈: "현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술"
## 시각화 요구사항
1. DX를 큰 원 또는 큰 박스로, 그 안에 GIS/BIM/디지털트윈을 포함
2. GIS, BIM, 디지털 트윈은 서로 겹치거나 연결되어 융합을 표현 (벤 다이어그램, 겹치는 원, 또는 연결선)
3. 하단에 "BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다" 강조 박스
4. 색상: DX 파란(#2563eb), GIS/BIM/디지털트윈 각각 다른 색조의 파란
5. **293px 높이 안에 맞춰라**
6. SVG 또는 CSS로 시각화
## 출력
HTML + inline <style>만 반환. 설명 없이 코드만.
```html
(여기에 HTML)
```"""
html_2 = await _call_kei(kei_url, prompt_2)
if html_2:
wrapped_2 = _wrap_in_slide(html_2, 707, 293)
m_2 = await asyncio.to_thread(measure_rendered_heights, wrapped_2)
s_2 = await asyncio.to_thread(capture_slide_screenshot, wrapped_2)
_save(out_dir, "verify2_hierarchy.html", wrapped_2)
if s_2:
(out_dir / "verify2_hierarchy.png").write_bytes(base64.b64decode(s_2))
slide = m_2.get("slide", {})
print(f" [{time.time()-t0:.0f}s] 결과: {slide.get('scrollHeight', 0)}px / 720px")
print(f" HTML: {len(html_2)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 생성 실패")
# ═══════════════════════════════════════
# 검증 3: 용어 정의 풀 텍스트 + 출처
# ═══════════════════════════════════════
print("\n=== 검증 3: 용어 정의 (풀 텍스트 + 출처) ===")
prompt_3 = """다음 3개 용어의 정의를 sidebar 카드로 만들어라. 크기: 380px 너비 × 490px 높이.
## 용어 (원본 텍스트를 100% 그대로 사용. 한 글자도 바꾸지 마라.)
1. 건설산업
정의: "부동산 개발, 설계, 시공, 유지보수를 포괄하는 종합산업으로, 광범위한 기술을 통합·융합하여 인프라를 만드는 산업"
2. BIM (Building Information Modeling)
정의: "형상정보와 속성정보가 포함된 3D 모델로, 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구"
출처: "건설산업 BIM 기본지침, 국토교통부, 2020"
3. DX (Digital Transformation)
정의: "디지털 기술을 활용하여 업무방식과 가치 창출 구조를 전환하는 과정 및 결과. 단순한 기술 도입이 아닌, 산업의 새로운 방향을 정립"
출처: "IBM Institute for Business Value, 2011"
## 요구사항
1. 카드 스타일: 배경 #f8fafc, 테두리 1px solid #e2e8f0, border-radius 8px
2. 번호: 원형 #2563eb 배경 + 흰 숫자
3. 정의 텍스트를 축약하지 마라. 위에 제공한 텍스트를 한 글자도 빠짐없이 그대로 넣어라.
4. 출처가 있으면 이탤릭 작은 글씨(10px, #94a3b8)로 표시
5. 490px 높이 안에 여유 있게 배치 (공간이 충분함)
6. 상단에 "용어 정의" 구분선 라벨 (좌우 선 + 중앙 텍스트)
## 출력
HTML + inline <style>만 반환. 설명 없이 코드만.
```html
(여기에 HTML)
```"""
html_3 = await _call_kei(kei_url, prompt_3)
if html_3:
wrapped_3 = _wrap_in_slide(html_3, 380, 490)
m_3 = await asyncio.to_thread(measure_rendered_heights, wrapped_3)
s_3 = await asyncio.to_thread(capture_slide_screenshot, wrapped_3)
_save(out_dir, "verify3_definitions.html", wrapped_3)
if s_3:
(out_dir / "verify3_definitions.png").write_bytes(base64.b64decode(s_3))
slide = m_3.get("slide", {})
print(f" [{time.time()-t0:.0f}s] 결과: {slide.get('scrollHeight', 0)}px / 720px")
print(f" HTML: {len(html_3)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 생성 실패")
print(f"\n총 소요: {time.time()-t0:.0f}")
print(f"결과: {out_dir}")
async def _call_kei(kei_url: str, prompt: str) -> str | None:
"""Kei API 호출하여 HTML 코드 추출."""
import re
import httpx
try:
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream(
"POST", f"{kei_url}/api/message",
json={"message": prompt, "session_id": "verify-3issues", "mode_hint": "chat"},
timeout=None,
) as response:
if response.status_code != 200:
return None
from src.sse_utils import stream_sse_tokens
full_text = await stream_sse_tokens(response)
if not full_text:
return None
# ```html ... ``` 블록 추출
match = re.search(r"```html\s*(.*?)```", full_text, re.DOTALL)
if match:
return match.group(1).strip()
# <div나 <style로 시작하는 HTML 직접 추출
match = re.search(r"(<(?:div|style|section)[^>]*>.*)", full_text, re.DOTALL)
if match:
return match.group(1).strip()
return full_text.strip()
except Exception as e:
print(f" Kei API 오류: {e}")
return None
def _wrap_in_slide(inner_html: str, width: int, height: int) -> str:
"""HTML 조각을 측정 가능한 슬라이드 프레임으로 감싼다."""
return f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden;
background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
padding: 40px;
}}
.test-container {{
width: {width}px;
max-height: {height}px;
overflow: visible;
}}
</style>
</head><body>
<div class="slide">
<div class="test-container">
{inner_html}
</div>
</div>
</body></html>"""
def _save(out_dir, name, data):
(out_dir / name).write_text(data if isinstance(data, str) else json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,175 @@
"""검증 1, 2 재시도 — Claude API 직접 호출.
Kei는 콘텐츠 분석/판단. Claude가 HTML 코드 생성.
"""
from __future__ import annotations
import asyncio, json, sys, time, datetime, base64, re
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
from src.config import settings
import anthropic
out_dir = ROOT / "data" / "runs" / f"verify_claude_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
print(f"출력: {out_dir}\n")
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
t0 = time.time()
# ═══════════════════════════════════════
# 검증 1: 배경 사례 박스
# ═══════════════════════════════════════
print("=== 검증 1: 배경 사례 박스 (Claude) ===")
prompt_1 = """다음 콘텐츠를 다크 배경 박스 HTML로 만들어라.
## 크기
- width: 100%, height: 176px (고정, overflow 금지)
## 콘텐츠 (축약 금지, 그대로 사용)
- 제목: "현실 — 용어의 혼용"
- 본문: "건설산업에서 DX와 BIM이 동일 개념으로 인식되고 있다. 실질적으로 DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 3차원 모델 기반의 정보 관리 도구로서 DX의 하위 기술에 해당한다."
- 사례 1: "스마트 건설 활성화 방안(2022.07)" / "추진과제: 건설산업 디지털화 / 실행과제: BIM 전면 도입, BIM 전문인력 양성"
- 사례 2: "제7차 건설기술진흥 기본계획(2023.12)" / "추진방향: 디지털 전환을 통한 스마트 건설 확산 / 추진과제: BIM 도입으로 건설산업 디지털화"
## 디자인
- 배경: linear-gradient(135deg, #1e293b, #0f172a), border-radius: 8px
- width: 100%, height: 176px
- 제목: 13px bold #93c5fd
- 본문: 12px #e2e8f0, "DX와 BIM"을 <strong> 처리
- 사례 2개 가로 나란히 (flex/grid)
- 사례 카드: rgba(255,255,255,0.06), border-left: 3px solid #60a5fa
- 사례 제목: 11px bold #fbbf24
- 사례 내용: 10px #cbd5e1
HTML + inline <style>만 반환. 설명 없이 코드만."""
html_1 = await _call_claude(client, prompt_1)
if html_1:
wrapped = _wrap(html_1, 707)
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
_save(out_dir, "verify1.html", wrapped)
if s:
(out_dir / "verify1.png").write_bytes(base64.b64decode(s))
print(f" [{time.time()-t0:.0f}s] 완료. HTML {len(html_1)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 실패")
# ═══════════════════════════════════════
# 검증 2: DX 포함 관계 (카드 구조)
# ═══════════════════════════════════════
print("\n=== 검증 2: DX 포함 관계 (Claude) ===")
prompt_2 = """다음 포함 관계를 시각화하는 HTML을 만들어라.
## 크기
- width: 100%, max-height: 293px
## 구조 (정확히 이 구조를 따르라)
1. 제목: "DX와 핵심기술의 올바른 관계" (14px bold #2563eb 가운데)
2. DX 큰 박스:
- border: 3px solid #2563eb, border-radius: 14px
- background: linear-gradient(180deg, #eff6ff, #dbeafe)
- position: relative
- 라벨 배지 (absolute top:-11px left:50% transform:translateX(-50%)):
"DX — 디지털 전환 (상위개념)" background:#2563eb color:white font-size:12px font-weight:900 padding:3px 18px border-radius:10px
- 설명 (11px #1e40af 가운데):
"BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능"
- 카드 3개 가로 나란히 (gap:10px):
각 카드: background:white, border:2px solid #93c5fd, border-radius:8px, padding:10px, text-align:center
각 카드 상단 원형 아이콘: 36px, background:linear-gradient(135deg,#93c5fd,#2563eb), color:white, font-weight:900
- G | GIS | "지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공"
- B | BIM | "시설물 생애주기 정보를 3차원 모델 기반으로 통합·관리하는 도구"
- T | 디지털 트윈 | "현실 세계의 물리적 객체를 디지털 환경에 동일하게 구현"
카드 설명: 10px #64748b
3. 핵심 메시지 박스 (DX 박스 아래):
- background:#f0f9ff, border:2px solid #bae6fd, border-radius:8px, padding:10px, text-align:center
- "BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다"
- "BIM ≠ DX" 부분: color:#dc2626 font-weight:900
- 나머지: 13px bold #0c4a6e
HTML + inline <style>만 반환. 설명 없이 코드만."""
html_2 = await _call_claude(client, prompt_2)
if html_2:
wrapped = _wrap(html_2, 707)
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
_save(out_dir, "verify2.html", wrapped)
if s:
(out_dir / "verify2.png").write_bytes(base64.b64decode(s))
print(f" [{time.time()-t0:.0f}s] 완료. HTML {len(html_2)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 실패")
print(f"\n총 소요: {time.time()-t0:.0f}")
print(f"결과: {out_dir}")
async def _call_claude(client, prompt: str) -> str | None:
try:
response = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text if response.content else ""
if not text:
return None
# ```html ... ``` 추출
match = re.search(r"```html\s*(.*?)```", text, re.DOTALL)
if match:
return match.group(1).strip()
# HTML 직접 추출
match = re.search(r"(<(?:div|style)[^>]*>.*)", text, re.DOTALL)
if match:
return match.group(1).strip()
return text.strip()
except Exception as e:
print(f" Claude API 오류: {e}")
return None
def _wrap(inner_html: str, width: int) -> str:
return f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden;
background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
}}
.test-container {{ width: {width}px; }}
</style>
</head><body>
<div class="slide"><div class="test-container">
{inner_html}
</div></div>
</body></html>"""
def _save(d, n, data):
(d / n).write_text(data if isinstance(data, str) else json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,173 @@
"""본심 C 수정: 캡션 이미지에 가까이 + 두 번째 불릿 한 줄로."""
from __future__ import annotations
import asyncio, sys, datetime, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / f"core_c_fix_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
img_path = Path("D:/ad-hoc/cel/public/assets/images/dx1.png")
img_b64 = base64.b64encode(img_path.read_bytes()).decode()
img_src = f"data:image/png;base64,{img_b64}"
html = f"""<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
.core {{
width: 767px;
font-family: 'Pretendard Variable', sans-serif;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 18px;
overflow: hidden;
word-break: keep-all;
}}
.core-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}}
.core-label {{
background: #1e293b;
color: #ffffff;
font-size: 12px;
font-weight: 700;
padding: 3px 12px;
border-radius: 4px;
}}
.popup-link {{
font-size: 10px;
color: #2563eb;
font-weight: 700;
cursor: pointer;
text-decoration: underline;
}}
.fi {{
float: right;
margin: 60px 0 8px 12px;
width: 250px;
}}
.fi img {{ width: 100%; }}
.fi .cap {{
font-size: 9px;
color: #94a3b8;
text-align: center;
margin-top: 1px;
line-height: 1.2;
}}
.core-text {{
font-size: 12px;
color: #1e293b;
line-height: 1.75;
}}
.bp {{
padding-left: 14px;
text-indent: -14px;
margin-bottom: 5px;
}}
.bp::before {{
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #1e293b;
font-weight: 700;
}}
.sp {{
padding-left: 28px;
text-indent: -14px;
margin-bottom: 4px;
font-size: 11px;
color: #475569;
}}
.sp::before {{
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #64748b;
}}
.core-text b {{ font-weight: 700; color: #1e293b; }}
.key-msg {{
background: #f0f9ff;
border: 2px solid #bae6fd;
border-radius: 6px;
padding: 5px 12px;
text-align: center;
font-size: 11px;
font-weight: 700;
color: #0c4a6e;
margin-top: 8px;
clear: both;
}}
.key-msg em {{
color: #dc2626;
font-style: normal;
font-weight: 900;
}}
</style>
<div class="core">
<div class="core-header">
<div class="core-label">DX와 BIM의 관계</div>
<span class="popup-link">📊 DX와 BIM의 상세 비교</span>
</div>
<div class="core-text">
<div class="fi">
<img src="{img_src}">
<div class="cap">건설산업의 DX</div>
</div>
<div class="bp">DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 <b>프로세스를 혁신하는 상위개념</b></div>
<div class="bp">건설산업의 DX는 GIS(공간정보), BIM, 디지털 트윈(가상환경)의 <b>기술융합을 통해서만 실현 또는 구현 가능</b></div>
<div class="sp"><b>GIS의 역할</b> : 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</div>
<div class="sp"><b>BIM의 역할</b> : 형상정보와 내용정보가 포함된 3D모델로, 건설 정보 기반의 Process와 Product를 제공. 시설물의 생애주기동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="sp"><b>디지털 트윈</b> : 현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술</div>
<div class="bp">DX는 이들 기술을 통합하여 업무방식과 가치 창출 구조를 <b>근본적으로 전환하는 과정 및 결과</b></div>
</div>
<div class="key-msg">
<em>BIM ≠ DX</em> — BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
</div>"""
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin:0; padding:0; box-sizing:border-box; }}
.slide {{
width:1280px; height:720px; overflow:hidden; background:white;
font-family:'Pretendard Variable',sans-serif;
display:flex; align-items:center; justify-content:center;
}}
</style>
</head><body>
<div class="slide">
{html}
</div>
</body></html>"""
(out_dir / "core_c_fix.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "core_c_fix.png").write_bytes(base64.b64decode(s))
print(f"결과: {out_dir}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,227 @@
"""본심 최종 검증: 샘플 이미지 구조 정확히 반영.
구조: 왼쪽 텍스트(넓게) | 오른쪽 이미지(좁게) + 상단 팝업 링크
텍스트: 원본 MDX 거의 그대로, 축약 없음
"""
from __future__ import annotations
import asyncio, sys, datetime, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / f"core_final_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
# dx1.png base64
img_path = Path("D:/ad-hoc/cel/public/assets/images/dx1.png")
img_b64 = base64.b64encode(img_path.read_bytes()).decode()
img_src = f"data:image/png;base64,{img_b64}"
html = f"""<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
.core-section {{
width: 707px;
height: 293px;
font-family: 'Pretendard Variable', sans-serif;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 16px 20px;
display: flex;
flex-direction: column;
overflow: hidden;
}}
.core-header {{
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}}
.core-title {{
background: #1e293b;
color: #ffffff;
font-size: 13px;
font-weight: 700;
padding: 4px 14px;
border-radius: 4px;
}}
.core-detail-link {{
font-size: 10px;
color: #2563eb;
font-weight: 700;
cursor: pointer;
text-decoration: underline;
}}
.core-body {{
display: flex;
gap: 16px;
flex: 1;
}}
.core-text {{
flex: 62%;
font-size: 12px;
color: #1e293b;
line-height: 1.7;
}}
.core-text .main-point {{
margin-bottom: 8px;
}}
.core-text .main-point::before {{
content: '';
margin-right: 6px;
color: #1e293b;
font-weight: 700;
}}
.core-text .sub-point {{
padding-left: 16px;
font-size: 11px;
color: #475569;
margin-bottom: 4px;
}}
.core-text .sub-point::before {{
content: '';
margin-right: 6px;
color: #64748b;
}}
.core-text b {{
font-weight: 700;
color: #1e293b;
}}
.core-image {{
flex: 38%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}}
.core-image img {{
width: 100%;
border-radius: 6px;
border: 1px solid #e2e8f0;
object-fit: contain;
}}
.core-image .caption {{
font-size: 9px;
color: #94a3b8;
margin-top: 4px;
text-align: center;
}}
/* 팝업 테이블 */
.core-detail-link details {{
position: relative;
}}
.core-detail-link summary {{
font-size: 10px;
color: #2563eb;
font-weight: 700;
cursor: pointer;
list-style: none;
}}
.core-detail-link summary::-webkit-details-marker {{
display: none;
}}
.popup-table {{
position: absolute;
right: 0;
top: 20px;
background: white;
border: 1px solid #e2e8f0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 8px;
z-index: 10;
width: 500px;
}}
.popup-table table {{
width: 100%;
border-collapse: collapse;
font-size: 10px;
}}
.popup-table th {{
background: #1e293b;
color: white;
padding: 5px 8px;
text-align: left;
font-weight: 700;
}}
.popup-table td {{
padding: 4px 8px;
border-bottom: 1px solid #e2e8f0;
color: #334155;
}}
.popup-table tr:nth-child(even) {{
background: #f8fafc;
}}
</style>
<div class="core-section">
<div class="core-header">
<div class="core-title">DX와 BIM의 관계</div>
<div class="core-detail-link">
<details>
<summary>📊 DX와 BIM의 상세 비교</summary>
<div class="popup-table">
<table>
<tr><th>기준</th><th>DX</th><th>BIM</th></tr>
<tr><td>범위</td><td>BIM &lt;&lt; DX (Engineering + Management 통합)</td><td>Only 3D (형상 구현 중심)</td></tr>
<tr><td>프로세스</td><td>근본적 문제의식을 통한 개선</td><td>기존 2D 설계 방식 유지</td></tr>
<tr><td>성과품</td><td>공학 정보 및 콘텐츠 연계에 집중</td><td>3D 모델 중심</td></tr>
<tr><td>활용</td><td>설계/시공 생산성 혁신</td><td>3D 모델에 의한 일반적 이해 향상</td></tr>
<tr><td>확장성</td><td>전 생애주기 활용 시스템</td><td>(설계/시공/운영) 분야별 단절</td></tr>
<tr><td>주체</td><td>자체 수행 능력 — 지속가능성 확보</td><td>S/W 제작사 판매 정책에 의존</td></tr>
</table>
</div>
</details>
</div>
</div>
<div class="core-body">
<div class="core-text">
<div class="main-point">DX는 BIM과 같은 기술을 기반으로 산업 전반의 <b>프로세스를 혁신하는 상위개념</b></div>
<div class="main-point">건설산업의 DX는 GIS(공간정보), BIM, 디지털 트윈(가상환경)의 <b>기술융합을 통해서만 실현 또는 구현 가능</b></div>
<div class="sub-point"><b>GIS의 역할</b> : 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</div>
<div class="sub-point"><b>BIM의 역할</b> : 형상정보와 내용정보가 포함된 3D모델로, 건설 정보 기반의 <b>Process와 Product를 제공</b></div>
</div>
<div class="core-image">
<img src="{img_src}" alt="건설산업의 DX">
<div class="caption">건설산업의 DX</div>
</div>
</div>
</div>"""
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin:0; padding:0; box-sizing:border-box; }}
.slide {{
width:1280px; height:720px; overflow:hidden; background:white;
font-family:'Pretendard Variable',sans-serif;
display:flex; align-items:center; justify-content:center;
}}
</style>
</head><body>
<div class="slide">
{html}
</div>
</body></html>"""
(out_dir / "core_final.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "core_final.png").write_bytes(base64.b64decode(s))
print(f"결과: {out_dir}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,216 @@
"""본심 최종 v2: 원본 MDX 85-95% 보존 + 들여쓰기 + 여백 최소화."""
from __future__ import annotations
import asyncio, sys, datetime, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / f"core_final2_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
img_path = Path("D:/ad-hoc/cel/public/assets/images/dx1.png")
img_b64 = base64.b64encode(img_path.read_bytes()).decode()
img_src = f"data:image/png;base64,{img_b64}"
html = f"""<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
.core {{
width: 707px;
height: 293px;
font-family: 'Pretendard Variable', sans-serif;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 18px;
display: flex;
flex-direction: column;
overflow: hidden;
}}
.core-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}}
.core-label {{
background: #1e293b;
color: #ffffff;
font-size: 12px;
font-weight: 700;
padding: 3px 12px;
border-radius: 4px;
}}
.detail-link {{
font-size: 10px;
color: #2563eb;
font-weight: 700;
cursor: pointer;
text-decoration: underline;
}}
.detail-link details {{ position: relative; }}
.detail-link summary {{
font-size: 10px; color: #2563eb; font-weight: 700;
cursor: pointer; list-style: none;
}}
.detail-link summary::-webkit-details-marker {{ display: none; }}
.popup {{
position: absolute; right: 0; top: 18px;
background: white; border: 1px solid #e2e8f0;
border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 8px; z-index: 10; width: 480px;
}}
.popup table {{ width: 100%; border-collapse: collapse; font-size: 10px; }}
.popup th {{ background: #1e293b; color: white; padding: 4px 6px; text-align: left; }}
.popup td {{ padding: 3px 6px; border-bottom: 1px solid #e2e8f0; color: #334155; }}
.popup tr:nth-child(even) {{ background: #f8fafc; }}
.core-body {{
display: flex;
gap: 14px;
flex: 1;
}}
.text-area {{
flex: 60%;
font-size: 12px;
color: #1e293b;
line-height: 1.7;
word-break: keep-all;
}}
/* 불릿 들여쓰기: 점 다음 줄이 점 옆 글자 시작 위치에 맞춤 */
.bp {{
padding-left: 14px;
text-indent: -14px;
margin-bottom: 5px;
}}
.bp::before {{
content: '';
margin-right: 6px;
color: #1e293b;
font-weight: 700;
}}
.sp {{
padding-left: 28px;
text-indent: -14px;
margin-bottom: 3px;
font-size: 11px;
color: #475569;
}}
.sp::before {{
content: '';
margin-right: 6px;
color: #64748b;
}}
.text-area b {{ font-weight: 700; color: #1e293b; }}
.img-area {{
flex: 40%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}}
.img-area img {{
width: 100%;
border-radius: 6px;
border: 1px solid #e2e8f0;
object-fit: contain;
}}
.img-caption {{
font-size: 9px;
color: #94a3b8;
margin-top: 3px;
}}
.key-msg {{
background: #f0f9ff;
border: 2px solid #bae6fd;
border-radius: 6px;
padding: 5px 12px;
text-align: center;
font-size: 11px;
font-weight: 700;
color: #0c4a6e;
margin-top: 6px;
}}
.key-msg em {{
color: #dc2626;
font-style: normal;
font-weight: 900;
}}
</style>
<div class="core">
<div class="core-header">
<div class="core-label">DX와 BIM의 관계</div>
<div class="detail-link">
<details>
<summary>📊 DX와 BIM의 상세 비교</summary>
<div class="popup">
<table>
<tr><th>기준</th><th>DX</th><th>BIM</th></tr>
<tr><td>범위</td><td>BIM &lt;&lt; DX (Engineering + Management 통합)</td><td>Only 3D (형상 구현 중심)</td></tr>
<tr><td>프로세스</td><td>근본적 문제의식을 통한 개선</td><td>기존 2D 설계 방식 유지</td></tr>
<tr><td>성과품</td><td>공학 정보 및 콘텐츠 연계에 집중</td><td>3D 모델 중심</td></tr>
<tr><td>활용</td><td>설계/시공 생산성 혁신(개념의 재정립)</td><td>3D 모델에 의한 일반적 이해 향상</td></tr>
<tr><td>확장성</td><td>전 생애주기 활용 시스템</td><td>(설계/시공/운영) 분야별 단절</td></tr>
<tr><td>주체</td><td>적극적, 주체적인 기술 접목/융합<br>자체 수행 능력 — 지속가능성 확보</td><td>소극적, 상용 기술에 의존<br>S/W 제작사 판매 정책에 의존</td></tr>
</table>
</div>
</details>
</div>
</div>
<div class="core-body">
<div class="text-area">
<div class="bp">DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 <b>프로세스를 혁신하는 상위개념</b></div>
<div class="bp">건설산업의 DX는 GIS(공간정보), BIM, 디지털 트윈(가상환경)의 <b>기술융합을 통해서만 실현 또는 구현 가능</b></div>
<div class="sp"><b>GIS의 역할</b> : 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</div>
<div class="sp"><b>BIM의 역할</b> : 형상정보와 내용정보가 포함된 3D모델로, 건설 정보 기반의 Process와 Product를 제공. 시설물의 생애주기동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="sp"><b>디지털 트윈</b> : 현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술</div>
<div class="bp">DX는 이들 기술을 통합하여 업무방식과 가치 창출 구조를 <b>근본적으로 전환하는 과정 및 결과</b></div>
</div>
<div class="img-area">
<img src="{img_src}" alt="건설산업의 DX">
<div class="img-caption">건설산업의 DX</div>
</div>
</div>
<div class="key-msg">
<em>BIM ≠ DX</em> — BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
</div>"""
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin:0; padding:0; box-sizing:border-box; }}
.slide {{
width:1280px; height:720px; overflow:hidden; background:white;
font-family:'Pretendard Variable',sans-serif;
display:flex; align-items:center; justify-content:center;
}}
</style>
</head><body>
<div class="slide">
{html}
</div>
</body></html>"""
(out_dir / "core_final2.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "core_final2.png").write_bytes(base64.b64decode(s))
print(f"결과: {out_dir}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,211 @@
"""본심: 텍스트 감싸기(float) — 워드/HWP 스타일.
이미지를 오른쪽에 float, 텍스트가 이미지를 감싸며 흐름.
이미지 아래에도 텍스트가 이어짐. 빈 공간 없음.
"""
from __future__ import annotations
import asyncio, sys, datetime, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / f"core_float_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
img_path = Path("D:/ad-hoc/cel/public/assets/images/dx1.png")
img_b64 = base64.b64encode(img_path.read_bytes()).decode()
img_src = f"data:image/png;base64,{img_b64}"
html = f"""<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
.core {{
width: 767px;
font-family: 'Pretendard Variable', sans-serif;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 18px;
overflow: hidden;
word-break: keep-all;
}}
.core-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}}
.core-label {{
background: #1e293b;
color: #ffffff;
font-size: 12px;
font-weight: 700;
padding: 3px 12px;
border-radius: 4px;
}}
.detail-link details {{ position: relative; }}
.detail-link summary {{
font-size: 10px; color: #2563eb; font-weight: 700;
cursor: pointer; list-style: none;
}}
.detail-link summary::-webkit-details-marker {{ display: none; }}
.popup {{
position: absolute; right: 0; top: 18px;
background: white; border: 1px solid #e2e8f0;
border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.12);
padding: 8px; z-index: 10; width: 500px;
}}
.popup table {{ width: 100%; border-collapse: collapse; font-size: 10px; }}
.popup th {{ background: #1e293b; color: white; padding: 4px 6px; text-align: left; }}
.popup td {{ padding: 3px 6px; border-bottom: 1px solid #e2e8f0; color: #334155; }}
.popup tr:nth-child(even) {{ background: #f8fafc; }}
/* 이미지 float: 텍스트가 이미지를 감싸며 흐름 */
.float-img {{
float: right;
margin: 0 0 10px 14px;
width: 280px;
}}
.float-img img {{
width: 100%;
border-radius: 6px;
border: 1px solid #e2e8f0;
object-fit: contain;
}}
.float-img .caption {{
font-size: 9px;
color: #94a3b8;
text-align: center;
margin-top: 3px;
}}
.core-text {{
font-size: 12px;
color: #1e293b;
line-height: 1.75;
}}
/* 불릿 들여쓰기: 점 다음 줄은 점 옆 글자 시작 위치에 맞춤 */
.bp {{
padding-left: 14px;
text-indent: -14px;
margin-bottom: 5px;
}}
.bp::before {{
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #1e293b;
font-weight: 700;
}}
.sp {{
padding-left: 28px;
text-indent: -14px;
margin-bottom: 4px;
font-size: 11px;
color: #475569;
}}
.sp::before {{
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #64748b;
}}
.core-text b {{ font-weight: 700; color: #1e293b; }}
.key-msg {{
background: #f0f9ff;
border: 2px solid #bae6fd;
border-radius: 6px;
padding: 5px 12px;
text-align: center;
font-size: 11px;
font-weight: 700;
color: #0c4a6e;
margin-top: 8px;
clear: both;
}}
.key-msg em {{
color: #dc2626;
font-style: normal;
font-weight: 900;
}}
</style>
<div class="core">
<div class="core-header">
<div class="core-label">DX와 BIM의 관계</div>
<div class="detail-link">
<details>
<summary>📊 DX와 BIM의 상세 비교</summary>
<div class="popup">
<table>
<tr><th>기준</th><th>DX</th><th>BIM</th></tr>
<tr><td>범위</td><td>BIM &lt;&lt; DX (Engineering + Management 통합)</td><td>Only 3D (형상 구현 중심)</td></tr>
<tr><td>프로세스</td><td>근본적 문제의식을 통한 개선</td><td>기존 2D 설계 방식 유지</td></tr>
<tr><td>성과품</td><td>공학 정보 및 콘텐츠 연계에 집중</td><td>3D 모델 중심</td></tr>
<tr><td>활용</td><td>설계/시공 생산성 혁신(개념의 재정립)</td><td>3D 모델에 의한 일반적 이해 향상</td></tr>
<tr><td>확장성</td><td>전 생애주기 활용 시스템</td><td>(설계/시공/운영) 분야별 단절</td></tr>
<tr><td>주체</td><td>적극적, 주체적인 기술 접목/융합<br>자체 수행 능력 — 지속가능성 확보</td><td>소극적, 상용 기술에 의존<br>S/W 제작사 판매 정책에 의존</td></tr>
</table>
</div>
</details>
</div>
</div>
<div class="core-text">
<!-- 이미지를 오른쪽에 float -->
<div class="float-img">
<img src="{img_src}" alt="건설산업의 DX">
<div class="caption">건설산업의 DX</div>
</div>
<div class="bp">DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 <b>프로세스를 혁신하는 상위개념</b></div>
<div class="bp">건설산업의 DX는 GIS(공간정보), BIM, 디지털 트윈(가상환경)의 <b>기술융합을 통해서만 실현 또는 구현 가능</b></div>
<div class="sp"><b>GIS의 역할</b> : 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</div>
<div class="sp"><b>BIM의 역할</b> : 형상정보와 내용정보가 포함된 3D모델로, 건설 정보 기반의 Process와 Product를 제공. 시설물의 생애주기동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="sp"><b>디지털 트윈</b> : 현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술</div>
<div class="bp">DX는 이들 기술을 통합하여 업무방식과 가치 창출 구조를 <b>근본적으로 전환하는 과정 및 결과</b></div>
</div>
<div class="key-msg">
<em>BIM ≠ DX</em> — BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
</div>"""
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin:0; padding:0; box-sizing:border-box; }}
.slide {{
width:1280px; height:720px; overflow:hidden; background:white;
font-family:'Pretendard Variable',sans-serif;
display:flex; align-items:center; justify-content:center;
}}
</style>
</head><body>
<div class="slide">
{html}
</div>
</body></html>"""
(out_dir / "core_float.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "core_float.png").write_bytes(base64.b64decode(s))
print(f"결과: {out_dir}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,231 @@
"""본심 float v2: 이미지 아래 빈 공간에 팝업 배치."""
from __future__ import annotations
import asyncio, sys, datetime, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / f"core_float2_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
img_path = Path("D:/ad-hoc/cel/public/assets/images/dx1.png")
img_b64 = base64.b64encode(img_path.read_bytes()).decode()
img_src = f"data:image/png;base64,{img_b64}"
html = f"""<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
.core {{
width: 767px;
font-family: 'Pretendard Variable', sans-serif;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 18px;
overflow: hidden;
word-break: keep-all;
}}
.core-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}}
.core-label {{
background: #1e293b;
color: #ffffff;
font-size: 12px;
font-weight: 700;
padding: 3px 12px;
border-radius: 4px;
}}
/* 이미지 + 팝업을 하나의 float 블록으로 묶음 */
.float-block {{
float: right;
margin: 0 0 8px 14px;
width: 280px;
}}
.float-block img {{
width: 100%;
border-radius: 6px;
border: 1px solid #e2e8f0;
object-fit: contain;
}}
.float-block .caption {{
font-size: 9px;
color: #94a3b8;
text-align: center;
margin-top: 3px;
margin-bottom: 6px;
}}
/* 팝업이 이미지 바로 아래에 위치 */
.float-block .detail-trigger {{
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 6px 10px;
font-size: 10px;
color: #2563eb;
font-weight: 700;
cursor: pointer;
text-align: center;
}}
.float-block details {{ position: relative; }}
.float-block summary {{
font-size: 10px;
color: #2563eb;
font-weight: 700;
cursor: pointer;
list-style: none;
text-align: center;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
padding: 6px 10px;
}}
.float-block summary::-webkit-details-marker {{ display: none; }}
.popup {{
position: absolute;
right: 0;
top: 32px;
background: white;
border: 1px solid #e2e8f0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
padding: 8px;
z-index: 10;
width: 500px;
}}
.popup table {{ width: 100%; border-collapse: collapse; font-size: 10px; }}
.popup th {{ background: #1e293b; color: white; padding: 4px 6px; text-align: left; }}
.popup td {{ padding: 3px 6px; border-bottom: 1px solid #e2e8f0; color: #334155; }}
.popup tr:nth-child(even) {{ background: #f8fafc; }}
.core-text {{
font-size: 12px;
color: #1e293b;
line-height: 1.75;
}}
.bp {{
padding-left: 14px;
text-indent: -14px;
margin-bottom: 5px;
}}
.bp::before {{
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #1e293b;
font-weight: 700;
}}
.sp {{
padding-left: 28px;
text-indent: -14px;
margin-bottom: 4px;
font-size: 11px;
color: #475569;
}}
.sp::before {{
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #64748b;
}}
.core-text b {{ font-weight: 700; color: #1e293b; }}
.key-msg {{
background: #f0f9ff;
border: 2px solid #bae6fd;
border-radius: 6px;
padding: 5px 12px;
text-align: center;
font-size: 11px;
font-weight: 700;
color: #0c4a6e;
margin-top: 8px;
clear: both;
}}
.key-msg em {{
color: #dc2626;
font-style: normal;
font-weight: 900;
}}
</style>
<div class="core">
<div class="core-header">
<div class="core-label">DX와 BIM의 관계</div>
</div>
<div class="core-text">
<!-- 이미지 + 팝업을 하나의 float 블록으로 -->
<div class="float-block">
<img src="{img_src}" alt="건설산업의 DX">
<div class="caption">건설산업의 DX</div>
<details>
<summary>📊 DX와 BIM의 상세 비교</summary>
<div class="popup">
<table>
<tr><th>기준</th><th>DX</th><th>BIM</th></tr>
<tr><td>범위</td><td>BIM &lt;&lt; DX (Engineering + Management 통합)</td><td>Only 3D (형상 구현 중심)</td></tr>
<tr><td>프로세스</td><td>근본적 문제의식을 통한 개선</td><td>기존 2D 설계 방식 유지</td></tr>
<tr><td>성과품</td><td>공학 정보 및 콘텐츠 연계에 집중</td><td>3D 모델 중심</td></tr>
<tr><td>활용</td><td>설계/시공 생산성 혁신(개념의 재정립)</td><td>3D 모델에 의한 일반적 이해 향상</td></tr>
<tr><td>확장성</td><td>전 생애주기 활용 시스템</td><td>(설계/시공/운영) 분야별 단절</td></tr>
<tr><td>주체</td><td>적극적, 주체적인 기술 접목/융합<br>자체 수행 능력 — 지속가능성 확보</td><td>소극적, 상용 기술에 의존<br>S/W 제작사 판매 정책에 의존</td></tr>
</table>
</div>
</details>
</div>
<div class="bp">DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 <b>프로세스를 혁신하는 상위개념</b></div>
<div class="bp">건설산업의 DX는 GIS(공간정보), BIM, 디지털 트윈(가상환경)의 <b>기술융합을 통해서만 실현 또는 구현 가능</b></div>
<div class="sp"><b>GIS의 역할</b> : 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</div>
<div class="sp"><b>BIM의 역할</b> : 형상정보와 내용정보가 포함된 3D모델로, 건설 정보 기반의 Process와 Product를 제공. 시설물의 생애주기동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="sp"><b>디지털 트윈</b> : 현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술</div>
<div class="bp">DX는 이들 기술을 통합하여 업무방식과 가치 창출 구조를 <b>근본적으로 전환하는 과정 및 결과</b></div>
</div>
<div class="key-msg">
<em>BIM ≠ DX</em> — BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
</div>"""
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin:0; padding:0; box-sizing:border-box; }}
.slide {{
width:1280px; height:720px; overflow:hidden; background:white;
font-family:'Pretendard Variable',sans-serif;
display:flex; align-items:center; justify-content:center;
}}
</style>
</head><body>
<div class="slide">
{html}
</div>
</body></html>"""
(out_dir / "core_float2.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "core_float2.png").write_bytes(base64.b64decode(s))
print(f"결과: {out_dir}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,218 @@
"""본심 float v3: 이미지를 아래로 내려서 GIS 역할 줄과 상단 맞춤. 팝업은 상단 오른쪽."""
from __future__ import annotations
import asyncio, sys, datetime, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / f"core_float3_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
img_path = Path("D:/ad-hoc/cel/public/assets/images/dx1.png")
img_b64 = base64.b64encode(img_path.read_bytes()).decode()
img_src = f"data:image/png;base64,{img_b64}"
# 상단 불릿 2줄(메인 포인트)의 대략적 높이를 계산
# 줄 높이 12px * 1.75 = 21px, 불릿 2개 + margin = ~52px
# GIS 역할 줄이 시작하는 위치와 이미지 상단을 맞춤
# margin-top으로 이미지를 아래로 내림
html = f"""<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
.core {{
width: 767px;
font-family: 'Pretendard Variable', sans-serif;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 18px;
overflow: hidden;
word-break: keep-all;
}}
.core-header {{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}}
.core-label {{
background: #1e293b;
color: #ffffff;
font-size: 12px;
font-weight: 700;
padding: 3px 12px;
border-radius: 4px;
}}
.detail-link {{
font-size: 10px;
color: #2563eb;
font-weight: 700;
cursor: pointer;
text-decoration: underline;
}}
.detail-link details {{ position: relative; }}
.detail-link summary {{
font-size: 10px; color: #2563eb; font-weight: 700;
cursor: pointer; list-style: none;
}}
.detail-link summary::-webkit-details-marker {{ display: none; }}
.popup {{
position: absolute; right: 0; top: 18px;
background: white; border: 1px solid #e2e8f0;
border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.12);
padding: 8px; z-index: 10; width: 500px;
}}
.popup table {{ width: 100%; border-collapse: collapse; font-size: 10px; }}
.popup th {{ background: #1e293b; color: white; padding: 4px 6px; text-align: left; }}
.popup td {{ padding: 3px 6px; border-bottom: 1px solid #e2e8f0; color: #334155; }}
.popup tr:nth-child(even) {{ background: #f8fafc; }}
/* 이미지를 아래로 내림: 상단 불릿 2줄 후 GIS 역할과 상단 맞춤 */
.float-img {{
float: right;
margin: 50px 0 8px 14px;
width: 280px;
}}
.float-img img {{
width: 100%;
border-radius: 6px;
border: 1px solid #e2e8f0;
object-fit: contain;
}}
.float-img .caption {{
font-size: 9px;
color: #94a3b8;
text-align: center;
margin-top: 3px;
}}
.core-text {{
font-size: 12px;
color: #1e293b;
line-height: 1.75;
}}
.bp {{
padding-left: 14px;
text-indent: -14px;
margin-bottom: 5px;
}}
.bp::before {{
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #1e293b;
font-weight: 700;
}}
.sp {{
padding-left: 28px;
text-indent: -14px;
margin-bottom: 4px;
font-size: 11px;
color: #475569;
}}
.sp::before {{
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #64748b;
}}
.core-text b {{ font-weight: 700; color: #1e293b; }}
.key-msg {{
background: #f0f9ff;
border: 2px solid #bae6fd;
border-radius: 6px;
padding: 5px 12px;
text-align: center;
font-size: 11px;
font-weight: 700;
color: #0c4a6e;
margin-top: 8px;
clear: both;
}}
.key-msg em {{
color: #dc2626;
font-style: normal;
font-weight: 900;
}}
</style>
<div class="core">
<div class="core-header">
<div class="core-label">DX와 BIM의 관계</div>
<div class="detail-link">
<details>
<summary>📊 DX와 BIM의 상세 비교</summary>
<div class="popup">
<table>
<tr><th>기준</th><th>DX</th><th>BIM</th></tr>
<tr><td>범위</td><td>BIM &lt;&lt; DX (Engineering + Management 통합)</td><td>Only 3D (형상 구현 중심)</td></tr>
<tr><td>프로세스</td><td>근본적 문제의식을 통한 개선</td><td>기존 2D 설계 방식 유지</td></tr>
<tr><td>성과품</td><td>공학 정보 및 콘텐츠 연계에 집중</td><td>3D 모델 중심</td></tr>
<tr><td>활용</td><td>설계/시공 생산성 혁신(개념의 재정립)</td><td>3D 모델에 의한 일반적 이해 향상</td></tr>
<tr><td>확장성</td><td>전 생애주기 활용 시스템</td><td>(설계/시공/운영) 분야별 단절</td></tr>
<tr><td>주체</td><td>적극적, 주체적인 기술 접목/융합<br>자체 수행 능력 — 지속가능성 확보</td><td>소극적, 상용 기술에 의존<br>S/W 제작사 판매 정책에 의존</td></tr>
</table>
</div>
</details>
</div>
</div>
<div class="core-text">
<!-- 이미지: margin-top으로 아래로 내려서 GIS 역할 줄과 상단 맞춤 -->
<div class="float-img">
<img src="{img_src}" alt="건설산업의 DX">
<div class="caption">건설산업의 DX</div>
</div>
<div class="bp">DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 <b>프로세스를 혁신하는 상위개념</b></div>
<div class="bp">건설산업의 DX는 GIS(공간정보), BIM, 디지털 트윈(가상환경)의 <b>기술융합을 통해서만 실현 또는 구현 가능</b></div>
<div class="sp"><b>GIS의 역할</b> : 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</div>
<div class="sp"><b>BIM의 역할</b> : 형상정보와 내용정보가 포함된 3D모델로, 건설 정보 기반의 Process와 Product를 제공. 시설물의 생애주기동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="sp"><b>디지털 트윈</b> : 현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술</div>
<div class="bp">DX는 이들 기술을 통합하여 업무방식과 가치 창출 구조를 <b>근본적으로 전환하는 과정 및 결과</b></div>
</div>
<div class="key-msg">
<em>BIM ≠ DX</em> — BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
</div>"""
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin:0; padding:0; box-sizing:border-box; }}
.slide {{
width:1280px; height:720px; overflow:hidden; background:white;
font-family:'Pretendard Variable',sans-serif;
display:flex; align-items:center; justify-content:center;
}}
</style>
</head><body>
<div class="slide">
{html}
</div>
</body></html>"""
(out_dir / "core_float3.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "core_float3.png").write_bytes(base64.b64decode(s))
print(f"결과: {out_dir}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,220 @@
"""본심 4가지 샘플: 이미지와 텍스트가 어우러지는 방식."""
from __future__ import annotations
import asyncio, sys, datetime, base64
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
out_dir = ROOT / "data" / "runs" / f"core_samples_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
img_path = Path("D:/ad-hoc/cel/public/assets/images/dx1.png")
img_b64 = base64.b64encode(img_path.read_bytes()).decode()
img_src = f"data:image/png;base64,{img_b64}"
common_css = """
* { margin:0; padding:0; box-sizing:border-box; }
.core {
width: 767px;
font-family: 'Pretendard Variable', sans-serif;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 18px;
overflow: hidden;
word-break: keep-all;
}
.core-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.core-label {
background: #1e293b;
color: #ffffff;
font-size: 12px;
font-weight: 700;
padding: 3px 12px;
border-radius: 4px;
}
.popup-link {
font-size: 10px;
color: #2563eb;
font-weight: 700;
cursor: pointer;
text-decoration: underline;
}
.core-text {
font-size: 12px;
color: #1e293b;
line-height: 1.75;
}
.bp {
padding-left: 14px;
text-indent: -14px;
margin-bottom: 5px;
}
.bp::before {
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #1e293b;
font-weight: 700;
}
.sp {
padding-left: 28px;
text-indent: -14px;
margin-bottom: 4px;
font-size: 11px;
color: #475569;
}
.sp::before {
content: '';
display: inline-block;
width: 14px;
text-indent: 0;
color: #64748b;
}
.core-text b { font-weight: 700; color: #1e293b; }
.key-msg {
background: #f0f9ff;
border: 2px solid #bae6fd;
border-radius: 6px;
padding: 5px 12px;
text-align: center;
font-size: 11px;
font-weight: 700;
color: #0c4a6e;
margin-top: 8px;
clear: both;
}
.key-msg em {
color: #dc2626;
font-style: normal;
font-weight: 900;
}
"""
text_content = """
<div class="bp">DX는 BIM과 같은 디지털기술을 기반으로 산업 전반의 <b>프로세스를 혁신하는 상위개념</b></div>
<div class="bp">건설산업의 DX는 GIS(공간정보), BIM, 디지털 트윈(가상환경)의 <b>기술융합을 통해서만 실현 또는 구현 가능</b></div>
<div class="sp"><b>GIS의 역할</b> : 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공</div>
<div class="sp"><b>BIM의 역할</b> : 형상정보와 내용정보가 포함된 3D모델로, 건설 정보 기반의 Process와 Product를 제공. 시설물의 생애주기동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구</div>
<div class="sp"><b>디지털 트윈</b> : 현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술</div>
<div class="bp">DX는 이들 기술을 통합하여 업무방식과 가치 창출 구조를 <b>근본적으로 전환하는 과정 및 결과</b></div>
"""
key_msg = """
<div class="key-msg">
<em>BIM ≠ DX</em> — BIM은 건설산업의 디지털전환(DX)을 수행하는 과정에서 가장 기초가 되는 일부분이다
</div>
"""
header = """
<div class="core-header">
<div class="core-label">DX와 BIM의 관계</div>
<span class="popup-link">📊 DX와 BIM의 상세 비교</span>
</div>
"""
# 샘플 A: float right, 이미지 border/shadow 없이 자연스럽게
sample_a = f"""<style>{common_css}
.s-a .fi {{ float: right; margin: 45px 0 8px 12px; width: 260px; }}
.s-a .fi img {{ width: 100%; }}
.s-a .fi .cap {{ font-size: 9px; color: #94a3b8; text-align: center; margin-top: 2px; }}
</style>
<div class="core s-a">
{header}
<div class="core-text">
<div class="fi"><img src="{img_src}"><div class="cap">건설산업의 DX</div></div>
{text_content}
</div>
{key_msg}
</div>"""
# 샘플 B: float right, 살짝 큰 이미지, 연한 배경
sample_b = f"""<style>{common_css}
.s-b .fi {{ float: right; margin: 40px 0 8px 16px; width: 300px; background: #f8fafc; border-radius: 8px; padding: 8px; }}
.s-b .fi img {{ width: 100%; }}
.s-b .fi .cap {{ font-size: 9px; color: #94a3b8; text-align: center; margin-top: 3px; }}
</style>
<div class="core s-b">
{header}
<div class="core-text">
<div class="fi"><img src="{img_src}"><div class="cap">건설산업의 DX</div></div>
{text_content}
</div>
{key_msg}
</div>"""
# 샘플 C: float right, 이미지 더 아래로 (BIM 역할과 맞춤)
sample_c = f"""<style>{common_css}
.s-c .fi {{ float: right; margin: 65px 0 8px 12px; width: 250px; }}
.s-c .fi img {{ width: 100%; }}
.s-c .fi .cap {{ font-size: 9px; color: #94a3b8; text-align: center; margin-top: 2px; }}
</style>
<div class="core s-c">
{header}
<div class="core-text">
<div class="fi"><img src="{img_src}"><div class="cap">건설산업의 DX</div></div>
{text_content}
</div>
{key_msg}
</div>"""
# 샘플 D: float left (이미지가 왼쪽)
sample_d = f"""<style>{common_css}
.s-d .fi {{ float: left; margin: 45px 14px 8px 0; width: 260px; }}
.s-d .fi img {{ width: 100%; }}
.s-d .fi .cap {{ font-size: 9px; color: #94a3b8; text-align: center; margin-top: 2px; }}
</style>
<div class="core s-d">
{header}
<div class="core-text">
<div class="fi"><img src="{img_src}"><div class="cap">건설산업의 DX</div></div>
{text_content}
</div>
{key_msg}
</div>"""
samples = {"A_float_clean": sample_a, "B_float_bg": sample_b, "C_float_lower": sample_c, "D_float_left": sample_d}
for name, html in samples.items():
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin:0; padding:0; box-sizing:border-box; }}
.slide {{
width:1280px; height:720px; overflow:hidden; background:white;
font-family:'Pretendard Variable',sans-serif;
display:flex; align-items:center; justify-content:center;
}}
</style>
</head><body>
<div class="slide">
{html}
</div>
</body></html>"""
(out_dir / f"{name}.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / f"{name}.png").write_bytes(base64.b64decode(s))
print(f" {name} 완료")
print(f"\n결과: {out_dir}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

135
scripts/verify_core_v3.py Normal file
View File

@@ -0,0 +1,135 @@
"""검증 B 재시도: 본심 — 참고 이미지 구조 반영.
참고 이미지 구조:
- DX 박스(이미지+텍스트) | BIM 박스(이미지+텍스트) 좌우 나란히
- 각 박스 안에 관련 이미지 + 설명
- 비교표는 팝업(details)으로 오른쪽 상단
"""
from __future__ import annotations
import asyncio, sys, time, datetime, base64, re
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
from src.config import settings
import anthropic
out_dir = ROOT / "data" / "runs" / f"verify_core_v3_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
t0 = time.time()
prompt = """다음 콘텐츠를 본심 영역 HTML로 만들어라. 707px × 293px.
## 참고 레이아웃 (이 구조를 따르라)
실제 기획서 슬라이드의 본심 영역 레이아웃:
- 좌우 2단으로 DX 영역과 BIM 영역이 나란히 배치
- 각 영역 안에 관련 이미지/다이어그램 + 핵심 설명 텍스트
- 상단 오른쪽에 "📊 상세 비교표 보기" 팝업 링크
- 하단에 핵심 메시지 강조
## 구조
1. 상단 바: 좌측에 섹션 소제목, 우측에 팝업 링크
- 좌: 빈 공간 또는 소제목
- 우: <details><summary>📊 DX vs BIM 상세 비교표</summary>
표 내용:
| 기준 | DX | BIM |
| 범위 | BIM << DX (Engineering + Management 통합) | Only 3D (형상 구현 중심) |
| 프로세스 | 근본적 문제의식을 통한 개선 | 기존 2D 설계 방식 유지 |
| 활용 | 설계/시공 생산성 혁신 | 3D 모델에 의한 일반적 이해 향상 |
| 확장성 | 전 생애주기 활용 시스템 | (설계/시공/운영) 분야별 단절 |
| 주체 | 자체 수행 능력 | S/W 제작사 판매 정책에 의존 |
</details>
2. 본문: 좌우 2단 (각 50%)
왼쪽 — DX (디지털 전환):
- 상단: 이미지 <img src="/assets/images/dx1.png" style="width:100%; border-radius:6px;">
(이미지가 없으면 placeholder: 연한 파란 배경 + "DX 기술융합 관계도" 텍스트)
- 하단 텍스트:
"DX (Digital Transformation) : 상위개념"
• BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능
• Engineering + Management 통합
• 전 생애주기 활용 시스템
오른쪽 — BIM:
- 상단: placeholder 이미지 (연한 초록 배경 + "BIM 3D 모델 기반" 텍스트, border-radius:6px)
- 하단 텍스트:
"BIM (Building Information Modeling) : 하위기술"
• Only 3D (형상 구현 중심)
• 기존 2D 설계 방식 유지
• (설계/시공/운영) 분야별 단절
3. 하단: 핵심 메시지
- background: #f0f9ff, border: 2px solid #bae6fd, border-radius: 8px
- "BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다"
- "BIM ≠ DX": color: #dc2626, font-weight: 900
## 디자인
- DX 영역: border-left: 3px solid #2563eb
- BIM 영역: border-left: 3px solid #10b981
- 이미지 placeholder: height: 100px, border-radius: 6px, display:flex, align-items:center, justify-content:center
- DX placeholder: background: #eff6ff, color: #2563eb
- BIM placeholder: background: #f0fdf4, color: #10b981
- 제목: 12px bold
- 불릿: 11px #475569, line-height: 1.5
- <summary>: 11px bold #2563eb, cursor: pointer, float: right 또는 text-align: right
- 표: font-size: 10px, 헤더 #1e293b/white
- 전체 293px 안에 맞출 것
HTML + inline <style>만 반환. 설명 없이 코드만."""
print("=== 검증 B v3: 본심 (참고 이미지 구조) ===")
try:
response = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text if response.content else ""
match = re.search(r"```html\s*(.*?)```", text, re.DOTALL)
html = match.group(1).strip() if match else text.strip()
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden; background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
}}
.test-container {{ width: 707px; }}
</style>
</head><body>
<div class="slide"><div class="test-container">
{html}
</div></div>
</body></html>"""
(out_dir / "B_core_v3.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "B_core_v3.png").write_bytes(base64.b64decode(s))
print(f" [{time.time()-t0:.0f}s] 완료")
print(f" 결과: {out_dir}")
except Exception as e:
print(f" 오류: {e}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
asyncio.run(main())

116
scripts/verify_core_v4.py Normal file
View File

@@ -0,0 +1,116 @@
"""검증 B v4: dx1.png 중심 + 주변 텍스트 배치.
dx1.png가 DX/GIS/BIM/디지털트윈 전체 관계를 보여주는 중심 이미지.
이미지 주변에 원본 텍스트로 관계를 설명.
"""
from __future__ import annotations
import asyncio, sys, time, datetime, base64, re
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
from src.config import settings
import anthropic
out_dir = ROOT / "data" / "runs" / f"verify_core_v4_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
t0 = time.time()
prompt = """다음 콘텐츠를 본심 영역 HTML로 만들어라. 707px × 293px.
## 핵심: dx1.png 이미지가 중심
이 이미지는 Digital Transformation, GIS, BIM, Metaverse(Digital Twin)의 관계를 보여주는 다이어그램이다.
이 이미지 하나가 전체 관계를 시각적으로 보여주므로, 이미지를 중심에 크게 배치하고 주변에 텍스트로 보충한다.
## 구조
1. 이미지를 중앙 또는 좌측에 크게 배치:
<img src="D:/ad-hoc/cel/public/assets/images/dx1.png" style="max-width:320px; border-radius:8px; border:1px solid #e2e8f0;">
2. 이미지 오른쪽 또는 아래에 텍스트 배치 (원본 그대로 사용):
"DX는 BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능한 상위개념이다."
• GIS: 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공
• BIM: 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구
• 디지털 트윈: 현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술
3. 오른쪽 상단에 팝업:
<details><summary style="font-size:11px; color:#2563eb; cursor:pointer; font-weight:bold;">📊 DX vs BIM 상세 비교표</summary>
표:
| 기준 | DX | BIM |
| 범위 | BIM << DX (Engineering + Management 통합) | Only 3D (형상 구현 중심) |
| 프로세스 | 근본적 문제의식을 통한 개선 | 기존 2D 설계 방식 유지 |
| 활용 | 설계/시공 생산성 혁신 | 3D 모델에 의한 일반적 이해 향상 |
| 확장성 | 전 생애주기 활용 시스템 | (설계/시공/운영) 분야별 단절 |
| 주체 | 자체 수행 능력 — 지속가능성 확보 | S/W 제작사 판매 정책에 의존 |
</details>
4. 하단에 핵심 메시지:
background: #f0f9ff, border: 2px solid #bae6fd, border-radius: 8px, padding: 8px
"BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다"
"BIM ≠ DX": color: #dc2626, font-weight: 900
## 디자인
- 이미지+텍스트를 flex로 가로 배치 (이미지 왼쪽, 텍스트 오른쪽)
- 텍스트: 11px #475569, line-height: 1.6
- 각 기술명(GIS, BIM, 디지털 트윈): bold #1e293b
- 전체 293px 안에 맞출 것
- "상위개념", "하위기술" 같은 단어 사용 금지
HTML + inline <style>만 반환. 설명 없이 코드만."""
print("=== 검증 B v4: dx1.png 중심 ===")
try:
response = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text if response.content else ""
match = re.search(r"```html\s*(.*?)```", text, re.DOTALL)
html = match.group(1).strip() if match else text.strip()
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden; background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
}}
.test-container {{ width: 707px; }}
</style>
</head><body>
<div class="slide"><div class="test-container">
{html}
</div></div>
</body></html>"""
(out_dir / "B_core_v4.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "B_core_v4.png").write_bytes(base64.b64decode(s))
print(f" [{time.time()-t0:.0f}s] 완료")
print(f" 결과: {out_dir}")
except Exception as e:
print(f" 오류: {e}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
asyncio.run(main())

129
scripts/verify_core_v5.py Normal file
View File

@@ -0,0 +1,129 @@
"""검증 B v5: 텍스트 왼쪽 | dx1.png 이미지 오른쪽.
참고 이미지(스크린샷) 구조 정확히 반영.
dx1.png를 base64로 인라인 삽입하여 확실히 표시.
"""
from __future__ import annotations
import asyncio, sys, time, datetime, base64, re
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
from src.config import settings
import anthropic
out_dir = ROOT / "data" / "runs" / f"verify_core_v5_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
# dx1.png를 base64로 변환
dx1_path = Path("D:/ad-hoc/cel/public/assets/images/dx1.png")
dx1_b64 = ""
if dx1_path.exists():
dx1_b64 = base64.b64encode(dx1_path.read_bytes()).decode()
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
t0 = time.time()
prompt = f"""다음 콘텐츠를 본심 영역 HTML로 만들어라. 707px × 293px.
## 레이아웃 (정확히 이 구조를 따르라)
왼쪽(55%): 텍스트 | 오른쪽(45%): 이미지
텍스트가 왼쪽, 이미지가 오른쪽이다. 반대로 하지 마라.
## 왼쪽 영역 (텍스트)
원본 텍스트를 그대로 사용:
"DX는 BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능한 상위개념이다."
• GIS: 지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공
• BIM: 시설물의 생애주기 동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구
• 디지털 트윈: 현실 세계의 물리적 객체나 시스템을 디지털 환경에 동일하게 구현하는 기술
"DX는 이들 기술을 통합하여 업무방식과 가치 창출 구조를 근본적으로 전환하는 과정 및 결과이다."
## 오른쪽 영역 (이미지)
이미지를 아래 태그로 삽입 (base64 인라인):
<img src="data:image/png;base64,{dx1_b64}" style="width:100%; border-radius:8px; border:1px solid #e2e8f0;">
## 하단
오른쪽 상단에:
<details><summary style="font-size:11px; color:#2563eb; cursor:pointer; font-weight:bold; text-align:right;">📊 DX vs BIM 상세 비교표</summary>
표:
| 기준 | DX | BIM |
| 범위 | Engineering + Management 통합 | Only 3D (형상 구현 중심) |
| 프로세스 | 근본적 문제의식을 통한 개선 | 기존 2D 설계 방식 유지 |
| 활용 | 설계/시공 생산성 혁신 | 3D 모델에 의한 일반적 이해 향상 |
| 확장성 | 전 생애주기 활용 시스템 | (설계/시공/운영) 분야별 단절 |
</details>
맨 아래에 핵심 메시지:
background: #f0f9ff, border: 2px solid #bae6fd, border-radius: 8px, padding: 8px, text-align: center
"BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다"
"BIM ≠ DX": color: #dc2626, font-weight: 900
## 디자인
- flex로 가로 배치 (왼쪽 텍스트 55%, 오른쪽 이미지 45%)
- 왼쪽 텍스트: 12px #1e293b, 불릿 11px #475569
- 기술명(GIS, BIM, 디지털 트윈): bold
- 전체 293px 안에 맞출 것
- "상위개념", "하위기술" 단어 사용 금지
HTML + inline <style>만 반환. 설명 없이 코드만."""
print("=== 검증 B v5: 텍스트 왼쪽 | 이미지 오른쪽 ===")
try:
response = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=16384,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text if response.content else ""
match = re.search(r"```html\s*(.*?)```", text, re.DOTALL)
html = match.group(1).strip() if match else text.strip()
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden; background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
}}
.test-container {{ width: 707px; }}
</style>
</head><body>
<div class="slide"><div class="test-container">
{html}
</div></div>
</body></html>"""
(out_dir / "B_core_v5.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "B_core_v5.png").write_bytes(base64.b64decode(s))
print(f" [{time.time()-t0:.0f}s] 완료")
print(f" 결과: {out_dir}")
except Exception as e:
print(f" 오류: {e}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,175 @@
"""검증 A: 용어 정의 재검증 + 검증 B: 본심 (이미지+텍스트+팝업 표)
용어 정의: 참고 이미지 수준 — 부제 + 불릿 2개 + 원본 텍스트 거의 그대로
본심: dx1.png 이미지 + DX vs BIM 관계 텍스트 + 비교표는 details/summary 팝업
"""
from __future__ import annotations
import asyncio, sys, time, datetime, base64, re
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
from src.config import settings
import anthropic
out_dir = ROOT / "data" / "runs" / f"verify_v2_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
t0 = time.time()
# ═══════════════════════════════════════
# 검증 A: 용어 정의 (참고 이미지 수준)
# ═══════════════════════════════════════
print("=== 검증 A: 용어 정의 (참고 이미지 수준) ===")
prompt_a = """다음 3개 용어 정의를 sidebar 카드로 만들어라. 380px × 490px.
## 용어 (원본 텍스트를 한 글자도 바꾸지 말고 그대로 사용)
### BIM (Building Information Modeling) : 디지털 전환을 위한 핵심 기술
- 시설물의 생애주기동안 발생한 모든 정보를 3차원 모델 기반으로 통합·관리하는 정보 관리 도구
- 건설 정보와 절차를 표준화된 방식으로 연계하고 디지털 협업이 가능하도록 하는 핵심 인프라 기술
### 건설산업
- 다양한 시설물을 각 산업마다의 광범위한 기술을 통합 및 융합하여 만들어내는 종합산업
- 목적 시설물의 품질 욕구를 충족시키면서 최단기간 내에 최소 비용으로 편리하고 안전하며 우수한 성능의 시설물 완성을 목표로 함
### 디지털전환 (DX, Digital Transformation) : 산업 패러다임의 변화
- 디지털 기술을 기반으로 산업 전반의 업무 방식과 가치 창출 구조를 전환하는 과정 및 결과
- 단순한 기술 도입이 아닌, 고객 가치와 의사결정 방식의 근본적인 변화로 산업의 새로운 방향을 정립하는 것을 의미
## 디자인 요구사항
1. 상단에 "용어 정의" 구분선 라벨 (좌우 선 + 중앙 텍스트, 13px #64748b)
2. 각 용어를 카드로:
- 배경: #f8fafc, 테두리: 1px solid #e2e8f0, border-radius: 8px, padding: 14px
- 용어명: 14px bold #1e293b (예: "BIM (Building Information Modeling)")
- 부제: 12px #2563eb (예: ": 디지털 전환을 위한 핵심 기술")
- 불릿: 12px #475569, line-height: 1.6, 불릿 마커 ""
- 각 불릿은 원본 텍스트 그대로
3. 카드 간 간격 10px
4. 490px 안에 여유 있게 배치
HTML + inline <style>만 반환. 설명 없이 코드만."""
html_a = await _call(client, prompt_a)
if html_a:
wrapped = _wrap(html_a, 380)
(out_dir / "A_definitions.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "A_definitions.png").write_bytes(base64.b64decode(s))
print(f" [{time.time()-t0:.0f}s] 완료")
# ═══════════════════════════════════════
# 검증 B: 본심 (이미지 + 텍스트 + 팝업 표)
# ═══════════════════════════════════════
print("\n=== 검증 B: 본심 (이미지+텍스트+팝업표) ===")
prompt_b = """다음 콘텐츠를 본심 영역 HTML로 만들어라. 707px × 293px.
## 구조 (정확히 이 구조를 따르라)
1. 제목: "DX와 핵심기술의 올바른 관계" (14px bold #2563eb 가운데)
2. 좌우 2단 레이아웃:
- 왼쪽 (50%): 이미지
<img src="/assets/images/dx1.png" style="width:100%; border-radius:8px; border:1px solid #e2e8f0;">
- 오른쪽 (50%): DX vs BIM 핵심 차이 텍스트
DX (상위개념):
• 기술융합을 통해서만 실현 가능한 상위개념
• Engineering + Management 통합
• 전 생애주기 활용 시스템
• 자체 수행 능력 — 지속가능성 확보
BIM (하위기술):
• Only 3D (형상 구현 중심)
• 기존 2D 설계 방식 유지
• (설계/시공/운영) 분야별 단절
• S/W 제작사 판매 정책에 의존
3. 이미지+텍스트 아래에 <details>/<summary> 팝업:
<summary>📊 DX vs BIM 상세 비교표 보기</summary>
펼치면 표가 보임:
| 기준 | DX | BIM |
| 범위 | BIM << DX (Engineering + Management 통합) | Only 3D (형상 구현 중심) |
| 프로세스 | 근본적 문제의식을 통한 개선 | 기존 2D 설계 방식 유지 |
| 활용 | 설계/시공 생산성 혁신 | 3D 모델에 의한 일반적 이해 향상 |
| 확장성 | 전 생애주기 활용 시스템 | (설계/시공/운영) 분야별 단절 |
| 주체 | 자체 수행 능력 — 지속가능성 확보 | S/W 제작사 판매 정책에 의존 |
4. 맨 아래에 핵심 메시지:
background: #f0f9ff, border: 2px solid #bae6fd, border-radius: 8px, padding: 8px, text-align: center
"BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다"
"BIM ≠ DX" 부분: color: #dc2626, font-weight: 900
## 디자인
- DX 항목 제목: 13px bold #2563eb
- BIM 항목 제목: 13px bold #64748b
- 불릿: 11px #475569
- 표 헤더: background: #1e293b, color: white
- 표 셀: 10px, border-bottom: 1px solid #e2e8f0
- <summary>: cursor: pointer, 12px bold #2563eb
- 이미지가 안 보이면 placeholder 박스(회색 배경 + "DX 관계도" 텍스트)로 대체
HTML + inline <style>만 반환. 설명 없이 코드만."""
html_b = await _call(client, prompt_b)
if html_b:
wrapped = _wrap(html_b, 707)
(out_dir / "B_core.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "B_core.png").write_bytes(base64.b64decode(s))
print(f" [{time.time()-t0:.0f}s] 완료")
print(f"\n총 소요: {time.time()-t0:.0f}")
print(f"결과: {out_dir}")
async def _call(client, prompt):
try:
response = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text if response.content else ""
match = re.search(r"```html\s*(.*?)```", text, re.DOTALL)
return match.group(1).strip() if match else text.strip()
except Exception as e:
print(f" 오류: {e}")
return None
def _wrap(inner, width):
return f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden; background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
}}
.test-container {{ width: {width}px; }}
</style>
</head><body>
<div class="slide"><div class="test-container">
{inner}
</div></div>
</body></html>"""
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
asyncio.run(main())

View File

@@ -0,0 +1,129 @@
"""DX 포함 관계를 3가지 다른 시각화로 비교.
A: 벤 다이어그램 (원 안에 이름만, 설명은 하단 별도)
B: 동심원 (DX 큰 원 > 기술융합 중간 원 > GIS/BIM/DT 작은 원)
C: 계층 박스 (DX 박스 안에 3개 기술 + 겹치는 영역 표시)
"""
from __future__ import annotations
import asyncio, sys, time, datetime, base64, re
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
COMMON_INFO = """
## 관계 (반드시 반영)
- DX는 상위개념. GIS, BIM, 디지털 트윈을 포함.
- 3개 기술은 서로 융합되어 DX를 실현.
- "BIM ≠ DX"
## 텍스트
- DX: 상위개념 (디지털 전환)
- GIS: 공간 정보
- BIM: 3차원 모델
- 디지털 트윈: 디지털 구현
- 핵심 메시지: "BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다"
## 공통 규칙
- 크기: 707px × 280px
- 원 안에는 이름만 (설명 텍스트를 원 안에 넣지 마라)
- 각 기술의 설명은 원 아래에 작은 텍스트로 별도 배치하거나 생략
- "BIM ≠ DX" 강조 박스는 하단에 배치
- 색상: GIS=#3b82f6, BIM=#10b981, 디지털트윈=#f59e0b, DX=#2563eb
- 폰트: Pretendard Variable
HTML + inline <style> 반환. 설명 없이 코드만.
"""
async def main():
from src.slide_measurer import capture_slide_screenshot
from src.config import settings
import anthropic
out_dir = ROOT / "data" / "runs" / f"hierarchy_3ways_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
t0 = time.time()
prompts = {
"A_venn": f"""DX 포함 관계를 **벤 다이어그램**으로 시각화하라.
- SVG로 3개 원을 서로 30% 겹치게 배치
- 각 원 안에 이름과 아이콘 글자(G, B, T)만 표시 (설명 넣지 마라)
- DX 큰 둥근 박스가 3개 원을 감싼다
- 3개가 겹치는 중심에 "융합" 텍스트
- 원 아래에 각 기술명 + 한 줄 설명을 가로로 나열
{COMMON_INFO}""",
"B_concentric": f"""DX 포함 관계를 **동심원 구조**로 시각화하라.
- 가장 큰 원: DX (연한 파란 배경)
- 중간 원: "기술 융합" (약간 진한 파란)
- 안쪽에 GIS, BIM, 디지털트윈 3개 작은 원이 삼각형으로 배치
- 각 원 안에 이름만 (G, B, T 아이콘 + 이름)
- 아래에 각 기술 한 줄 설명
{COMMON_INFO}""",
"C_nested_boxes": f"""DX 포함 관계를 **중첩 박스**로 시각화하라.
- DX 큰 박스 (border: 3px solid #2563eb, 둥근 모서리)
- 안에 3개 기술 카드가 가로로 배치
- 카드 사이에 겹치는 영역을 그라데이션 또는 점선으로 표시 (융합을 시각적으로)
- 각 카드: 원형 아이콘(G/B/T) + 이름 + 한 줄 설명
- DX 박스 상단에 라벨: "DX — 디지털 전환 (상위개념)"
- 카드들 아래에 "3개 기술이 융합되어 DX를 실현" 텍스트
{COMMON_INFO}""",
}
for name, prompt in prompts.items():
print(f"\n=== {name} ===")
try:
response = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text if response.content else ""
match = re.search(r"```html\s*(.*?)```", text, re.DOTALL)
html = match.group(1).strip() if match else text.strip()
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden; background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
}}
.test-container {{ width: 707px; }}
</style>
</head><body>
<div class="slide"><div class="test-container">
{html}
</div></div>
</body></html>"""
(out_dir / f"{name}.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / f"{name}.png").write_bytes(base64.b64decode(s))
print(f" [{time.time()-t0:.0f}s] 완료")
except Exception as e:
print(f" 오류: {e}")
print(f"\n총 소요: {time.time()-t0:.0f}")
print(f"결과: {out_dir}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
asyncio.run(main())

146
scripts/verify_layout_3.py Normal file

File diff suppressed because one or more lines are too long

198
scripts/verify_retry_1_2.py Normal file
View File

@@ -0,0 +1,198 @@
"""검증 1, 2 재시도 — 프롬프트 개선.
검증 1: 배경 박스가 영역을 꽉 채우도록
검증 2: 벤 다이어그램이 아니라 포함 관계 박스 구조 (C_reference 방식)
"""
from __future__ import annotations
import asyncio, json, sys, time, datetime, base64, re
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.sse_utils import stream_sse_tokens
from src.slide_measurer import measure_rendered_heights, capture_slide_screenshot
from src.config import settings
import httpx
out_dir = ROOT / "data" / "runs" / f"verify_retry_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
print(f"출력: {out_dir}\n")
kei_url = getattr(settings, "kei_api_url", "http://localhost:8000")
t0 = time.time()
# ═══════════════════════════════════════
# 검증 1 재시도: 배경 박스가 영역을 꽉 채움
# ═══════════════════════════════════════
print("=== 검증 1 재시도: 배경 사례 박스 ===")
prompt_1 = """다음 콘텐츠를 다크 배경 박스 HTML로 만들어라.
## 크기 제약
- 너비: 707px을 꽉 채운다 (width: 100%)
- 높이: 176px을 꽉 채운다 (height: 176px)
- overflow 금지 — 176px 안에 모든 내용이 보여야 한다
## 콘텐츠 (이 텍스트를 그대로 사용, 축약 금지)
- 제목: "현실 — 용어의 혼용"
- 본문: "건설산업에서 DX와 BIM이 동일 개념으로 인식되고 있다. 실질적으로 DX는 산업 전반의 프로세스를 혁신하는 상위개념이며, BIM은 3차원 모델 기반의 정보 관리 도구로서 DX의 하위 기술에 해당한다."
- 사례 1: 제목 "스마트 건설 활성화 방안(2022.07)" / 내용 "추진과제: 건설산업 디지털화 / 실행과제: BIM 전면 도입, BIM 전문인력 양성"
- 사례 2: 제목 "제7차 건설기술진흥 기본계획(2023.12)" / 내용 "추진방향: 디지털 전환을 통한 스마트 건설 확산 / 추진과제: BIM 도입으로 건설산업 디지털화"
## 디자인
- 배경: linear-gradient(135deg, #1e293b, #0f172a)
- border-radius: 8px
- width: 100%, height: 176px (고정)
- 제목: 13px bold, color: #93c5fd
- 본문: 12px, color: #e2e8f0
- 사례 카드 2개를 가로 나란히 (flex 또는 grid)
- 사례 카드: background: rgba(255,255,255,0.06), border-left: 3px solid #60a5fa, padding: 8px 12px
- 사례 제목: 11px bold, color: #fbbf24
- 사례 내용: 10px, color: #cbd5e1
- DX와 BIM을 strong 태그로 강조
## 출력
HTML + inline <style>만 반환. 설명 없이.
```html
(여기)
```"""
html_1 = await _call_kei(kei_url, prompt_1)
if html_1:
wrapped_1 = _wrap_in_container(html_1, 707, 200)
m_1 = await asyncio.to_thread(measure_rendered_heights, wrapped_1)
s_1 = await asyncio.to_thread(capture_slide_screenshot, wrapped_1)
_save(out_dir, "verify1_retry.html", wrapped_1)
if s_1:
(out_dir / "verify1_retry.png").write_bytes(base64.b64decode(s_1))
print(f" [{time.time()-t0:.0f}s] 완료. HTML {len(html_1)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 실패")
# ═══════════════════════════════════════
# 검증 2 재시도: 포함 관계 박스 구조 (벤 다이어그램 아님)
# ═══════════════════════════════════════
print("\n=== 검증 2 재시도: DX 포함 관계 ===")
prompt_2 = """다음 포함 관계를 시각화하는 HTML을 만들어라.
## 크기 제약
- 너비: 707px을 꽉 채운다
- 높이: 293px 안에 맞춘다
## 관계 구조
DX는 상위개념이다. DX 안에 GIS, BIM, 디지털 트윈이 포함된다.
이 3개 기술이 융합되어야 DX가 실현된다.
## 시각화 구조 (이 구조를 정확히 따르라)
1. "DX와 핵심기술의 올바른 관계" 제목 (14px bold, #2563eb, 가운데 정렬)
2. DX 큰 박스:
- border: 3px solid #2563eb, border-radius: 14px
- background: linear-gradient(180deg, #eff6ff, #dbeafe)
- 상단에 라벨 배지: "DX — 디지털 전환 (상위개념)" (absolute, top: -11px, background: #2563eb, color: white, border-radius: 10px)
- 배지 아래에 설명: "BIM, GIS, 디지털 트윈 등 핵심기술의 융합을 통해서만 실현 가능" (11px, #1e40af, 가운데)
- 내부에 카드 3개를 가로 나란히:
- 각 카드: background: white, border: 2px solid #93c5fd, border-radius: 8px, padding: 10px
- 각 카드 상단: 원형 아이콘 (36px, gradient #93c5fd→#2563eb, 흰 글자)
- GIS 카드: 아이콘 "G", 이름 "GIS", 설명 "지리적 데이터를 공간 분석하여 시각적으로 표현, 위치기반 정보 제공"
- BIM 카드: 아이콘 "B", 이름 "BIM", 설명 "시설물 생애주기 정보를 3차원 모델 기반으로 통합·관리하는 도구"
- 디지털트윈 카드: 아이콘 "T", 이름 "디지털 트윈", 설명 "현실 세계의 물리적 객체를 디지털 환경에 동일하게 구현"
3. DX 박스 아래에 핵심 메시지 박스:
- background: #f0f9ff, border: 2px solid #bae6fd, border-radius: 8px
- 텍스트: "BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다" (13px bold, #0c4a6e)
- "BIM ≠ DX" 부분만 color: #dc2626, font-weight: 900
## 출력
HTML + inline <style>만 반환. 설명 없이.
```html
(여기)
```"""
html_2 = await _call_kei(kei_url, prompt_2)
if html_2:
wrapped_2 = _wrap_in_container(html_2, 707, 310)
m_2 = await asyncio.to_thread(measure_rendered_heights, wrapped_2)
s_2 = await asyncio.to_thread(capture_slide_screenshot, wrapped_2)
_save(out_dir, "verify2_retry.html", wrapped_2)
if s_2:
(out_dir / "verify2_retry.png").write_bytes(base64.b64decode(s_2))
print(f" [{time.time()-t0:.0f}s] 완료. HTML {len(html_2)}")
else:
print(f" [{time.time()-t0:.0f}s] ❌ 실패")
print(f"\n총 소요: {time.time()-t0:.0f}")
print(f"결과: {out_dir}")
async def _call_kei(kei_url: str, prompt: str) -> str | None:
import httpx
try:
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream(
"POST", f"{kei_url}/api/message",
json={"message": prompt, "session_id": "verify-retry", "mode_hint": "chat"},
timeout=None,
) as response:
if response.status_code != 200:
return None
from src.sse_utils import stream_sse_tokens
full_text = await stream_sse_tokens(response)
if not full_text:
return None
match = re.search(r"```html\s*(.*?)```", full_text, re.DOTALL)
if match:
return match.group(1).strip()
match = re.search(r"(<(?:div|style|section)[^>]*>.*)", full_text, re.DOTALL)
if match:
return match.group(1).strip()
return full_text.strip()
except Exception as e:
print(f" Kei API 오류: {e}")
return None
def _wrap_in_container(inner_html: str, width: int, height: int) -> str:
return f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ background: white; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden;
background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
}}
.test-container {{
width: {width}px;
}}
</style>
</head><body>
<div class="slide">
<div class="test-container">
{inner_html}
</div>
</div>
</body></html>"""
def _save(out_dir, name, data):
(out_dir / name).write_text(data if isinstance(data, str) else json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
asyncio.run(main())

108
scripts/verify_venn.py Normal file
View File

@@ -0,0 +1,108 @@
"""검증: DX 포함 관계를 겹치는 원(벤 다이어그램)으로 시각화.
3개 기술이 서로 겹쳐서 융합을 표현하고, DX가 전체를 감싸는 구조.
"""
from __future__ import annotations
import asyncio, json, sys, time, datetime, base64, re
from pathlib import Path
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
async def main():
from src.slide_measurer import capture_slide_screenshot
from src.config import settings
import anthropic
out_dir = ROOT / "data" / "runs" / f"verify_venn_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
out_dir.mkdir(parents=True, exist_ok=True)
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
t0 = time.time()
prompt = """다음 포함 관계를 SVG 벤 다이어그램으로 시각화하는 HTML을 만들어라.
## 관계 구조
- DX(디지털 전환)는 상위개념이다 — 전체를 감싸는 가장 큰 원 또는 박스.
- DX 안에 GIS, BIM, 디지털 트윈 3개 기술이 있다.
- 이 3개 기술은 **서로 겹쳐서 융합**된다 — 벤 다이어그램처럼 원이 겹치는 부분이 있어야 한다.
- 3개가 겹치는 중심 영역 = "기술 융합" 또는 "DX 실현"
## 시각화 요구사항 (SVG 사용)
1. 전체 크기: 707px × 250px
2. DX 큰 원 또는 둥근 박스가 전체를 감싼다:
- fill: rgba(37,99,235,0.08), stroke: #2563eb, stroke-width: 2
- 상단 라벨: "DX (상위개념)"
3. 내부에 3개 원이 서로 겹쳐서 배치:
- GIS 원: cx=250, cy=120, r=80, fill: rgba(59,130,246,0.2), stroke: #3b82f6
- BIM 원: cx=350, cy=120, r=80, fill: rgba(16,185,129,0.2), stroke: #10b981
- 디지털트윈 원: cx=450, cy=120, r=80, fill: rgba(245,158,11,0.2), stroke: #f59e0b
- 각 원이 약 30-40px씩 겹쳐야 한다 (완전 분리 아님)
4. 각 원 안에 텍스트:
- 이름 (14px bold)
- 한 줄 설명 (10px)
5. 3개가 겹치는 중심 영역에 "융합" 또는 "DX 실현" 텍스트 (작게)
6. 아래에 핵심 메시지: "BIM ≠ DX — BIM은 DX를 실현하기 위한 핵심 기술 중 하나일 뿐이다"
- background: #f0f9ff, border: 2px solid #bae6fd, border-radius: 8px
- "BIM ≠ DX" 부분: color: #dc2626, font-weight: 900
## 텍스트 (원본 그대로)
- GIS: "지리적 데이터를 공간 분석하여 시각적으로 표현"
- BIM: "시설물 생애주기 정보를 3차원 모델로 통합·관리"
- 디지털 트윈: "현실 객체를 디지털로 동일하게 구현"
HTML + inline <style> + <svg>를 포함하여 반환. 설명 없이 코드만."""
print("=== 벤 다이어그램 검증 (Claude Sonnet) ===")
try:
response = await client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text if response.content else ""
match = re.search(r"```html\s*(.*?)```", text, re.DOTALL)
html = match.group(1).strip() if match else text.strip()
wrapped = f"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8">
<style>
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css');
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
.slide {{
width: 1280px; height: 720px; overflow: hidden;
background: white;
font-family: 'Pretendard Variable', sans-serif;
display: flex; align-items: center; justify-content: center;
}}
.test-container {{ width: 707px; }}
</style>
</head><body>
<div class="slide"><div class="test-container">
{html}
</div></div>
</body></html>"""
(out_dir / "venn.html").write_text(wrapped, encoding="utf-8")
s = await asyncio.to_thread(capture_slide_screenshot, wrapped)
if s:
(out_dir / "venn.png").write_bytes(base64.b64decode(s))
print(f" [{time.time()-t0:.0f}s] 완료. HTML {len(html)}")
print(f" 결과: {out_dir}")
except Exception as e:
print(f" 오류: {e}")
if __name__ == "__main__":
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("selenium").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
asyncio.run(main())