"""MDX 02 최종 4안 — 처음부터 독립, CSS font-size override 금지. 글씨 크기: 블록 CSS 원래 값 그대로. 절대 변경 금지. zone 크기: 글씨 크기에 맞게 조정 (글씨가 기준, 레이아웃이 맞춤). 안 1: 내 판단 + new/ only 상단: cards-3col-persona 하단좌: stacked-arrow-list 하단우: cards-3col-persona(3주체) 안 2: 내 판단 + new/ + 기존 상단: card-compare-3col 하단좌: card-numbered 하단우: table-simple-striped 안 3: Kei 판단 + new/ only (Kei 응답: 상단1 하단좌4 하단우2 결론6) 상단: cards-3col-persona 하단좌: split-panel-numbered 하단우: compare-vs-rows 안 4: Kei 판단 + new/ + 기존 (Kei 응답: 상단A 하단좌7 하단우H 결론6) 상단: card-compare-3col 하단좌: issues-paired-rows 하단우: compare-3col-badge """ import base64, re from pathlib import Path from jinja2 import Environment, FileSystemLoader BLOCKS = Path("templates/blocks") SVG = BLOCKS / "svg" OUT = Path("data/runs/mdx02_4plans_final") OUT.mkdir(parents=True, exist_ok=True) env = Environment(loader=FileSystemLoader(str(BLOCKS)), autoescape=False) def b64(f): p = SVG / f if not p.exists(): return "" ext = "svg+xml" if f.endswith(".svg") else "png" return f"data:image/{ext};base64," + base64.b64encode(p.read_bytes()).decode() def clean(h): return re.sub(r'', '', h, flags=re.DOTALL).strip() img_uri = open("data/runs/mdx02_v4_test/slide_image.txt").read().strip() title = "DX의 시행 목표 및 기대효과" ft = '고품질의 성과품, 비용 절감, 시간 단축, 의사소통에 도움이 안 되면 DX가 아니다' # slide-body: 1200×590px. 블록 글씨 크기에 맞춰 zone 크기 계산. # 컨테이너 계산 → 글자수 예산 → overflow 방지 IW = 500 # 이미지 GAP = 10 # zone 높이: 블록 필요 높이 기반 계산 # new/ 블록(15~22px): 상단 244px, 하단 336px # 기존 블록(13~15px): 상단 204px, 하단 310px TH_NEW = 244 BH_NEW = 336 TH_OLD = 204 BH_OLD = 310 # 원문 텍스트 TOP = [ ("안전과 품질", [ "시설물의 요구 성능을 설계-시공-운영 전 과정에서 디지털로 검증하여 안전성 확보", "Copy & Paste로 하향 평준화된 성과물의 하자 최소화로 고품질 성과물 제공"]), ("생산성 향상", [ "Analogue 기반 업무를 Digital 기반 프로세스로 전환하여 업무 속도·정확성·일관성 향상", "건설 비용 및 유지관리비 절감, 건설 기간 단축, 인력투입 최소화를 통해 부가가치 제고"]), ("소통과 신뢰", [ "성과품과 Solution을 통한 협업 강화로 의사소통 효율 및 운영·유지관리의 편리성 증진", "3D 모델 및 데이터 기반 검증을 통한 오류 최소화 및 Claim 예방으로 신뢰성 확보"]), ] BL = [ ("생산 방식", "수작업 의존의 반복 업무에서 벗어나, SW를 활용한 체계화된 방식으로 전환"), ("인지·검토", "2D 도면 해석 중심에서 3D 모델 기반의 직관적 인지·검토 체계로 전환"), ("협업 구조", "개별 문서 중심 협업에서 데이터 통합 기반의 정보 공유·관리 협업 환경으로 전환"), ("검증·대응", "사후 대응 중심의 문제 처리에서 사전 검증 중심의 예방적 업무 방식으로 전환"), ] BR = { "headers": ["구분", "발주자", "시공자", "설계자"], "rows": [ ["필요 역량", "실행 의지와 합리적 판단", "기술 투자와 운영 역량", "SW개발 투자 역량"], ["SW 기반 체계화", "행정 자동화로 생산성 향상", "체계적 공정관리로 신뢰성 확보", "설계 프로세스 체계화"], ["3D 기반 전환", "직관적 시각화로 품질 향상", "안전성 제고 및 관리 편의", "3D 검증으로 설계 오류 방지"], ["데이터 통합 협업", "원활한 의사소통으로 오류 감소", "협업 효율 및 서류 감소", "설계 신뢰도로 상호신뢰 증진"], ["사전 검증 관리", "민원·소송 등 사전 예방", "설계·시공 오류 예방", "설계 책임 리스크 감소"], ], } def wrap_slide(body_html, fname): sb = clean((BLOCKS / "slide-base.html").read_text(encoding="utf-8")) r = sb.replace('{{ title|default("슬라이드") }}', title) r = r.replace('{{ title|default("슬라이드 제목") }}', title) r = r.replace('{% block body %}{% endblock %}', body_html) pill = b64("pill_scroll.png") r = r.replace('{% if footer_text %}', '').replace('{% if footer_pill_bg %}', '') r = r.replace('{{ footer_pill_bg }}', pill).replace('{% else %}', '') r = r.replace('
', '') li = r.rfind('{% endif %}') if li > 0: r = r[:li] + r[li + len('{% endif %}'):] r = r.replace('{% endif %}', '').replace('{{ footer_text|safe }}', ft) r = r.replace('src="svg/bg_slide_texture.png"', f'src="{b64("bg_slide_texture.png")}"') r = r.replace('src="svg/line_divider.svg"', f'src="{b64("line_divider.svg")}"') out = OUT / fname out.write_text(r, encoding="utf-8") return out def make_body(top_html, bl_html, br_html, th, bh): """slide-body 안에 배치. zone 높이는 블록 글씨 크기 기반 계산값.""" return f""" """ # Jinja2 .items() 충돌 방지용 클래스 class _Cat: def __init__(self, name, color, items): self.name=name; self.color=color; self.items=items # ══════════════════════════════════════════ # 안 1: 내 판단 + new/ only — 글씨 크기 그대로 # ══════════════════════════════════════════ print("=== 안 1 ===") t1 = clean(env.get_template("new/cards-3col-persona.html").render(personas=[ {"overlay_color":c,"label_line1":t,"label_color":"#1a365d","bullets":[{"text":b} for b in bs]} for (t,bs),c in zip(TOP,["#dce8d4","#d4dce8","#e8dcd4"]) ])) # 뱃지/사진만 숨김 — 글씨 크기는 원래 값(15px body, 20px label) t1 += "" b1l = clean(env.get_template("new/stacked-arrow-list.html").render(items=[ {"text":f"{k}: {v}","border_color":c} for (k,v),c in zip(BL,["#fb5915","#e79000","#919f00","#0d6361"]) ])) # 헤더/장식만 숨김 — 글씨 크기(22px)는 원래 값 b1l += "" b1r = clean(env.get_template("new/cards-3col-persona.html").render(personas=[ {"overlay_color":c,"label_line1":h,"label_color":"#1a365d", "bullets":[{"text":BR["rows"][ri][ci+1]} for ri in range(5)]} for ci,(h,c) in enumerate([("발주자","#c8d8e8"),("시공자","#d8e8c8"),("설계자","#e8d8c8")]) ])) b1r = b1r.replace('block-c3p','block-c3p2').replace('c3p-','c3p2-') b1r += "" p1 = wrap_slide(make_body(t1, b1l, b1r, TH_NEW, BH_NEW), "plan1_new_only.html") print(f" {p1.stat().st_size:,} bytes") # ══════════════════════════════════════════ # 안 2: 내 판단 + mixed — 글씨 크기 그대로 # ══════════════════════════════════════════ print("=== 안 2 ===") t2 = clean(env.get_template("cards/card-compare-3col.html").render(cards=[ {"title":t,"color":c,"bullets":bs} for (t,bs),c in zip(TOP,["#1a365d","#15803d","#b45309"]) ])) b2l = clean(env.get_template("cards/card-numbered.html").render(items=[ {"title":k,"description":v,"color":c} for (k,v),c in zip(BL,["#2563eb","#16a34a","#d97706","#7c3aed"]) ])) b2r = clean(env.get_template("tables/table-simple-striped.html").render(**BR)) p2 = wrap_slide(make_body(t2, b2l, b2r, TH_OLD, BH_OLD), "plan2_mixed.html") print(f" {p2.stat().st_size:,} bytes") # ══════════════════════════════════════════ # 안 3: Kei 판단 + new/ only (1,4,2,6) # ══════════════════════════════════════════ print("=== 안 3 (Kei: 1,4,2,6) ===") # 상단: 1. cards-3col-persona (안 1과 동일 블록, 동일 데이터) t3 = clean(env.get_template("new/cards-3col-persona.html").render(personas=[ {"overlay_color":c,"label_line1":t,"label_color":"#1a365d","bullets":[{"text":b} for b in bs]} for (t,bs),c in zip(TOP,["#dce8d4","#d4dce8","#e8dcd4"]) ])) t3 += "" # 하단좌: 4. split-panel-numbered (Kei 선택) b3l = clean(env.get_template("new/split-panel-numbered.html").render( categories=[_Cat(k,c,[v]) for (k,v),c in zip(BL,["#417d38","#008e52","#008970","#2563eb"])], right_items=[], )) b3l += "" # 하단우: 2. compare-vs-rows (Kei 선택) — 3주체를 좌/우로 b3r = clean(env.get_template("new/compare-vs-rows.html").render( main_labels={"left":"발주자","center":"구분","right":"시공자·설계자"}, rows=[{"category":r[0],"left_text":r[1],"right_text":f"{r[2]} / {r[3]}"} for r in BR["rows"]], )) b3r += "" p3 = wrap_slide(make_body(t3, b3l, b3r, TH_NEW, BH_NEW), "plan3_kei_new.html") print(f" {p3.stat().st_size:,} bytes") # ══════════════════════════════════════════ # 안 4: Kei 판단 + mixed (A,7,H,6) # ══════════════════════════════════════════ print("=== 안 4 (Kei: A,7,H,6) ===") # 상단: A. card-compare-3col (안 2와 동일 블록) t4 = clean(env.get_template("cards/card-compare-3col.html").render(cards=[ {"title":t,"color":c,"bullets":bs} for (t,bs),c in zip(TOP,["#1a365d","#15803d","#b45309"]) ])) # 하단좌: 7. issues-paired-rows (Kei 선택) b4l = clean(env.get_template("new/issues-paired-rows.html").render( pill_bg=b64("pill_scroll.png"), rows=[ {"left":{"label":BL[0][0],"text":BL[0][1]},"right":{"label":BL[1][0],"text":BL[1][1]}}, {"left":{"label":BL[2][0],"text":BL[2][1]},"right":{"label":BL[3][0],"text":BL[3][1]},"pills_bottom":True}, ], )) b4l += "" # 하단우: H. compare-3col-badge (Kei 선택) b4r = clean(env.get_template("tables/compare-3col-badge.html").render(**BR)) p4 = wrap_slide(make_body(t4, b4l, b4r, TH_OLD, BH_OLD), "plan4_kei_mixed.html") print(f" {p4.stat().st_size:,} bytes") # ══════════════════════════════════════════ # Selenium 검증 # ══════════════════════════════════════════ print("\n=== Selenium 검증 ===") from selenium import webdriver from selenium.webdriver.chrome.options import Options import time options = Options() options.add_argument('--headless') options.add_argument('--window-size=1400,900') options.add_argument('--force-device-scale-factor=2') driver = webdriver.Chrome(options=options) for fname in ["plan1_new_only.html","plan2_mixed.html","plan3_kei_new.html","plan4_kei_mixed.html"]: path = OUT / fname driver.get(f"file:///{path.resolve()}") time.sleep(2) driver.save_screenshot(str(path).replace(".html",".png")) html = path.read_text(encoding="utf-8") c = re.sub(r'data:image/[^;]+;base64,[A-Za-z0-9+/=]+','I',html) # font-size override 확인 — !important가 있으면 위반 overrides = re.findall(r'font-size.*?!important', c) j = re.findall(r'\{[%{].*?[%}]\}',c) ts = ['안전','품질','생산','향상','소통','신뢰','생산 방식','인지','협업','검증', '발주자','시공자','설계자','안 되면 DX가 아니다','DX의 궁극적 목표','Process 혁신','업무 수행'] m = [t for t in ts if t not in c] print(f" [{fname}]") print(f" Jinja:{len(j)} 텍스트:{len(ts)-len(m)}/{len(ts)} font-size override:{len(overrides)}") if m: print(f" MISSING: {m}") if overrides: print(f" OVERRIDE 위반: {overrides[:3]}") driver.quit() print("\n완료!")