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:
617
scripts/test_3approaches.py
Normal file
617
scripts/test_3approaches.py
Normal 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())
|
||||
474
scripts/test_3approaches_dx2.py
Normal file
474
scripts/test_3approaches_dx2.py
Normal 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
325
scripts/test_3directions.py
Normal 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
369
scripts/test_hybrid.py
Normal 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())
|
||||
201
scripts/test_ideal_layout.py
Normal file
201
scripts/test_ideal_layout.py
Normal 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
426
scripts/test_ideal_v2.py
Normal 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
346
scripts/test_phase_q.py
Normal 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))
|
||||
187
scripts/test_phase_r_prime.py
Normal file
187
scripts/test_phase_r_prime.py
Normal 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
248
scripts/verify_3issues.py
Normal 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())
|
||||
175
scripts/verify_claude_1_2.py
Normal file
175
scripts/verify_claude_1_2.py
Normal 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())
|
||||
173
scripts/verify_core_c_fix.py
Normal file
173
scripts/verify_core_c_fix.py
Normal 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())
|
||||
227
scripts/verify_core_final.py
Normal file
227
scripts/verify_core_final.py
Normal 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 << 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())
|
||||
216
scripts/verify_core_final2.py
Normal file
216
scripts/verify_core_final2.py
Normal 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 << 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())
|
||||
211
scripts/verify_core_float.py
Normal file
211
scripts/verify_core_float.py
Normal 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 << 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())
|
||||
231
scripts/verify_core_float2.py
Normal file
231
scripts/verify_core_float2.py
Normal 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 << 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())
|
||||
218
scripts/verify_core_float3.py
Normal file
218
scripts/verify_core_float3.py
Normal 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 << 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())
|
||||
220
scripts/verify_core_samples.py
Normal file
220
scripts/verify_core_samples.py
Normal 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
135
scripts/verify_core_v3.py
Normal 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
116
scripts/verify_core_v4.py
Normal 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
129
scripts/verify_core_v5.py
Normal 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())
|
||||
175
scripts/verify_definitions_v2.py
Normal file
175
scripts/verify_definitions_v2.py
Normal 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())
|
||||
129
scripts/verify_hierarchy_3ways.py
Normal file
129
scripts/verify_hierarchy_3ways.py
Normal 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
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
198
scripts/verify_retry_1_2.py
Normal 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
108
scripts/verify_venn.py
Normal 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())
|
||||
Reference in New Issue
Block a user