"""MDX04 F16 override slide-fit preview — slide_fit_preview, NOT a Phase Z final. 배경: - V4 top1 = F26. 사용자 semantic review 로 F16 채택 - 사유: MDX04 04-2.* 는 4-issue diagnostic 구조 → F16 quadrant pattern 적합 - F26 figma 1:1 변환 부재 (별도 작업 보류) - anchor 보정 / detect_mdx 수정 / v4_full32_result.yaml 변경 모두 없음 매핑 (사용자 결정 — B 수정판, 그대로 유지): 04-2.1 (4) + 04-2.2 (4) = 8 항목을 4 원인군으로 그룹핑. 04-2.2 보존. 레이아웃 전환 (composition_preview → slide_fit_preview): 이전: 1280×1230 비표준 (composition preview) 현재: 1280×720 표준 슬라이드 (slide_fit_preview) ├ title bar (1280×56) ├ body (1200×590) │ ├ zone-left (340×590) = 04-1 compact 5-card stack │ └ zone-right (840×590) = F16 quadrant zone-fit (4분면 + center quote) └ footer pill (1280×48) F16 native dim (1280×1015) 폐기. zone (840×590) 에 맞게 좌표 재계산. 폰트 축소. """ import json import re import sys from datetime import datetime from html import escape from pathlib import Path import yaml from jinja2 import Environment, FileSystemLoader, select_autoescape ROOT = Path(__file__).resolve().parents[2] MDX_PATH = ROOT / "samples" / "mdx_batch" / "04.mdx" V4_RESULT = ROOT / "tests" / "matching" / "v4_full32_result.yaml" RUN_DIR = ROOT / "data" / "runs" / "mdx04_f16_override" TEMPLATES_DIR = RUN_DIR / "templates" # ─── 그룹핑 정의 (사용자 예시 그대로) ────────────────────────── GROUPING_RULE = { 'description': '04-2.1 (4 정책 항목) + 04-2.2 (4 조직 항목) = 8 항목을 4 원인군으로 그룹핑. F16 4 분면 ribbon = 그룹명.', 'reason': 'user wants 04-2.2 보존 + F16 4 분면 디자인 활용. 1:1 짝짓기 강제 회피.', 'groups': [ { 'quadrant': 'q1', 'name': '정책 집행 / 제도 운용 문제', 'items': [ {'source': '04-2.1', 'index': 0}, # 실질적 기술 경쟁을 저해하는 정책 집행 {'source': '04-2.1', 'index': 1}, # 적용 효과가 있는 사례도 없이 방침부터 도입 ], }, { 'quadrant': 'q2', 'name': '개념 이해 부족', 'items': [ {'source': '04-2.1', 'index': 2}, # 엔지니어링 S/W에 대한 개념 부재 {'source': '04-2.2', 'index': 0}, # 공학적 개념 정립 부재 {'source': '04-2.2', 'index': 2}, # DX/BIM의 근본 취지와 목표의 이해 부족 ], }, { 'quadrant': 'q3', 'name': '기술 투자 / 본업 기술력 부족', 'items': [ {'source': '04-2.1', 'index': 3}, # 기술투자(R&D) 없는 성과 창출 기대 {'source': '04-2.2', 'index': 1}, # '본업 기술력 확보' 우선의 개념 부재 ], }, { 'quadrant': 'q4', 'name': '조직 / 수행 역량 문제', 'items': [ {'source': '04-2.2', 'index': 3}, # 과거의 타성에 머무르고 있는 기술자 집단 ], }, ], } # ─── MDX 04 파싱 ──────────────────────────────────────────────── RE_SUBSECTION_HEAD = re.compile(r'^###\s+(\d+\.\d+)\s+(.+)$', re.MULTILINE) RE_TOP_BULLET = re.compile(r'^-\s+\*\*([^*]+)\*\*\s*$') def extract_subsection_items(text, num_label): lines = text.split('\n') start = None for i, ln in enumerate(lines): m = RE_SUBSECTION_HEAD.match(ln.strip()) if m and m.group(1) == num_label: start = i break if start is None: return None, [] end = len(lines) for j in range(start + 1, len(lines)): s = lines[j].strip() if RE_SUBSECTION_HEAD.match(s) or s == '---': end = j break section_title = lines[start].lstrip('# ').strip() body_lines = lines[start + 1:end] items = [] cur = None for ln in body_lines: stripped = ln.strip() m = RE_TOP_BULLET.match(stripped) if m: if cur is not None: items.append(cur) cur = {'headline': m.group(1).strip(), 'subs': []} continue m2 = re.match(r'^-\s+(.+)$', stripped) if m2 and cur is not None and not stripped.startswith('- **'): cur['subs'].append(m2.group(1).strip()) if cur is not None: items.append(cur) return section_title, items def extract_section_04_1_cards(text): m = re.search(r'## 1\. DX에 대한 인식(.*?)(?=^## 2\.)', text, re.DOTALL | re.MULTILINE) if not m: return None, [] body = m.group(1) cards = [] h3_iter = list(re.finditer(r']*>([^<]+)', body)) for idx, h3m in enumerate(h3_iter): label = h3m.group(1).strip() section_end = h3_iter[idx + 1].start() if idx + 1 < len(h3_iter) else len(body) section_text = body[h3m.end():section_end] # 인용 (첫

의 따옴표 텍스트) quote_m = re.search(r']*>(?:["“])(.+?)(?:["”])

', section_text, re.DOTALL) if not quote_m: quote_m = re.search(r']*>([^<]+)

', section_text, re.DOTALL) quote = quote_m.group(1).strip() if quote_m else '' bullets = [b.strip() for b in re.findall(r']*>([^<]+)', section_text)] cards.append({'label': label, 'quote': quote, 'bullets': bullets}) return '1. DX에 대한 인식', cards # ─── F16 grouped mapper ──────────────────────────────────────── def map_to_f16_grouped(items_2_1, items_2_2, slide_title): """8 items (2.1 4 + 2.2 4) → 4 quadrant groups (사용자 그룹핑 룰 적용).""" source_map = {'04-2.1': items_2_1, '04-2.2': items_2_2} payload = {'center_quote': slide_title} for group in GROUPING_RULE['groups']: q = group['quadrant'] items_for_q = [] for ref in group['items']: src_items = source_map[ref['source']] idx = ref['index'] if idx < len(src_items): src_item = src_items[idx] items_for_q.append({ 'source': '[' + ref['source'].replace('04-', '') + ']', 'headline': src_item['headline'], 'subs': src_item['subs'], }) payload[f'{q}_label'] = group['name'] payload[f'{q}_items'] = items_for_q return payload def map_to_5card_compact_slots(cards, section_title): return {'section_title': section_title, 'cards': cards} # ─── V4 metadata lookup ─────────────────────────────────────── def get_top1(v4, sid): sec = v4.get('mdx_sections', {}).get(sid) if not sec: return None j = sec.get('judgments_full32', []) return j[0] if j else None def get_frame_judgment(v4, sid, frame_number): sec = v4.get('mdx_sections', {}).get(sid) if not sec: return None for e in sec.get('judgments_full32', []): if e['frame_number'] == frame_number: return e return None # ─── 메인 ────────────────────────────────────────────────────── def main(): if not MDX_PATH.exists(): print(f"ERROR: MDX 04 not found at {MDX_PATH}", file=sys.stderr) sys.exit(1) if not V4_RESULT.exists(): print(f"ERROR: V4 result not found at {V4_RESULT}", file=sys.stderr) sys.exit(1) mdx_text = MDX_PATH.read_text(encoding='utf-8') v4 = yaml.safe_load(V4_RESULT.read_text(encoding='utf-8')) title_2_1, items_2_1 = extract_subsection_items(mdx_text, '2.1') title_2_2, items_2_2 = extract_subsection_items(mdx_text, '2.2') title_1, cards_1 = extract_section_04_1_cards(mdx_text) env = Environment( loader=FileSystemLoader(str(TEMPLATES_DIR)), autoescape=select_autoescape(['html', 'xml']), ) f16_zonefit_tpl = env.get_template("bim_issues_quadrant_four_zonefit.html.j2") cards5_left_tpl = env.get_template("cards_5_left_zone.html.j2") slide_fit_tpl = env.get_template("slide_fit_base.html.j2") # 04-2 통합 (그룹핑) → F16 zone-fit payload_f16 = map_to_f16_grouped(items_2_1, items_2_2, slide_title='DX 지연
요인') html_f16_zonefit = f16_zonefit_tpl.render(slot_payload=payload_f16) # 04-1 → 5-card left zone payload_cards = map_to_5card_compact_slots(cards_1, section_title=title_1) html_cards_left = cards5_left_tpl.render(slot_payload=payload_cards) # slide_fit base 조립 (1280×720) slide_fit_html = slide_fit_tpl.render( slide_title='4. DX 지연 요인', slide_meta='F16 user_semantic_override · slide_fit_preview', zone_left=html_cards_left, zone_right=html_f16_zonefit, slide_footer='검증 없는 정책의 일방적 추진과 조직의 회피, 이해 부족이 DX 지연을 반복시킨다', ) # 통합 1 슬라이드 페이지 (banner + slide_fit + metadata) timestamp = datetime.now().isoformat(timespec='seconds') page_html = f''' MDX04 1280×720 slide_fit · F16 user_semantic_override
MDX04 slide_fit_preview · 1280×720 표준 (NOT a Phase Z final)
composition_preview (1280×1230) → slide_fit_preview (1280×720) 전환. 같은 grouping rule 유지 (04-2.* 8 항목 → 4 원인군). 04-2.2 보존. F16 native dim 폐기, zone-fit 적용.
  • layout = title (56) + body (1200×590, left 340 + right 840) + footer pill (48)
  • zone-left = 04-1 compact 5-card stack
  • zone-right = F16 quadrant zone-fit (4분면 + center quote)
  • q1 = 정책 집행 / 제도 운용 (2.1×2) · q2 = 개념 이해 부족 (2.1×1 + 2.2×2)
  • q3 = 기술 투자 / 본업 기술력 부족 (2.1×1 + 2.2×1) · q4 = 조직 / 수행 역량 (2.2×1)
{slide_fit_html}
''' (RUN_DIR / "index.html").write_text(page_html, encoding='utf-8') # 단독 slide_fit (banner 없이 슬라이드 자체만) standalone_slide = f''' MDX04 1280×720 slide_fit (standalone) {slide_fit_html} ''' (RUN_DIR / "slide_1280x720.html").write_text(standalone_slide, encoding='utf-8') # debug.json top1_2_1 = get_top1(v4, '04-2.1') top1_2_2 = get_top1(v4, '04-2.2') top1_1 = get_top1(v4, '04-1') f16_2_1 = get_frame_judgment(v4, '04-2.1', 16) f16_2_2 = get_frame_judgment(v4, '04-2.2', 16) # grouping coverage 검증 (모든 8 항목 사용됐는지) used = set() for g in GROUPING_RULE['groups']: for ref in g['items']: used.add((ref['source'], ref['index'])) expected = set([('04-2.1', i) for i in range(len(items_2_1))] + [('04-2.2', i) for i in range(len(items_2_2))]) missing = sorted(expected - used) extra = sorted(used - expected) debug = { 'kind': 'mdx04_f16_override_slide_fit', 'preview_stage': 'slide_fit_preview', 'transition_from': 'composition_preview (1280×1230 비표준)', 'transition_to': 'slide_fit_preview (1280×720 표준)', 'transition_note': '같은 grouping rule 유지. F16 native height (1015px) 폐기. zone-fit 좌표 재계산. 폰트 축소.', 'is_phase_z_final': False, 'is_diagnostic': True, 'is_preview_or_result_candidate': True, 'generated_at': timestamp, 'v4_source': str(V4_RESULT.relative_to(ROOT)), 'mdx_source': str(MDX_PATH.relative_to(ROOT)), 'integrated_slide': True, 'layout': { 'slide_dimensions': '1280×720', 'title_bar_height': 56, 'body': {'width': 1200, 'height': 590, 'left_zone': 340, 'right_zone': 840, 'gap': 20}, 'footer_pill_height': 48, 'zone_left': '04-1 compact 5-card stack (frame library gap)', 'zone_right': '04-2 통합 F16 quadrant zone-fit (grouped)', 'mdx_one_slide_principle': True, 'standard_16_9': True, }, 'override_decision': { 'selected_frame_source': 'user_semantic_override', 'selected_frame': 'F16', 'selected_template_id': 'bim_issues_quadrant_four', 'reason': 'F16 quadrant pattern semantically/visually appropriate for MDX04 04-2.* ' '(four-issue diagnostic structure). V4 top1 F26 figma 변환 부재 + semantic ' 'review 에서 F16 가 더 적합 판단.', }, 'grouping_rule': GROUPING_RULE, 'grouping_coverage': { 'total_items': len(items_2_1) + len(items_2_2), 'mapped_items': len(used), 'missing': [{'source': s, 'index': i} for s, i in missing], 'extra': [{'source': s, 'index': i} for s, i in extra], 'all_items_preserved': not missing, }, 'sections': { '04-2.1': { 'mdx_title': title_2_1, 'item_count': len(items_2_1), 'v4_top1': { 'frame_number': top1_2_1['frame_number'], 'template_id': top1_2_1['template_id'], 'label': top1_2_1['label'], 'confidence': top1_2_1['confidence'], }, 'selected_frame': 16, 'original_label': f16_2_1['label'] if f16_2_1 else None, 'original_confidence': f16_2_1['confidence'] if f16_2_1 else None, }, '04-2.2': { 'mdx_title': title_2_2, 'item_count': len(items_2_2), 'v4_top1': { 'frame_number': top1_2_2['frame_number'], 'template_id': top1_2_2['template_id'], 'label': top1_2_2['label'], 'confidence': top1_2_2['confidence'], }, 'selected_frame': 16, 'original_label': f16_2_2['label'] if f16_2_2 else None, 'original_confidence': f16_2_2['confidence'] if f16_2_2 else None, 'preserved_in_grouping': True, }, '04-1': { 'mdx_title': title_1, 'card_count': len(cards_1), 'v4_top1': { 'frame_number': top1_1['frame_number'], 'template_id': top1_1['template_id'], 'label': top1_1['label'], 'confidence': top1_1['confidence'], } if top1_1 else None, 'selected_frame': None, 'override_note': '5-card library gap (32 frame DB 에 cardinality.ideal=5 frame 부재). ' 'compact 5-column grid 로 통합 슬라이드 상단에 배치.', }, }, 'caveats': [ '정식 Phase Z final 아님 — V4 lookup 우회', 'preview_stage = slide_fit_preview (1280×720 표준). 이전 composition_preview (1280×1230) 에서 전환', 'F16 partial template = preview 전용 (data/runs/mdx04_f16_override/templates/) — design_agent/templates/phase_z2 미수정', 'anchor 보정 / detect_mdx 수정 / v4_full32_result.yaml 변경 없음', '04-2.1 의 F16 original_label = reject (anchor=0). override 로 진행', '04-2.2 의 F16 original_label = restructure (사용 가능 라벨)', '04-2.2 보존 — 그룹핑으로 8 항목 모두 분면에 매핑', '04-1 = 5-card library gap. zone-left 에 compact stack 으로 배치', '그룹핑 룰은 사용자 semantic 결정 (yaml/dict 로 명시). 자동 생성 아님', 'F16 native dim (1280×1015) 폐기 — zone (840×590) 에 맞춰 좌표 재계산. 폰트 14px(ribbon)/11.5px(headline)/9.5px(sub)', 'slide-fit 으로 폰트 작아짐 → 가독성 trade-off. composition_preview 와 비교 필요', ], } (RUN_DIR / "debug.json").write_text( json.dumps(debug, ensure_ascii=False, indent=2), encoding='utf-8', ) # 이전 composition_preview 산출물 정리 — slide_fit_preview 로 대체 for old in ["slide_04-2.1.html", "slide_04-2.2.html", "slide_04-1.html", "slide_04-2_grouped.html", "slide_04-1_compact.html"]: p = RUN_DIR / old if p.exists(): p.unlink() print(f"[mdx04_f16_override_slide_fit] generated:") print(f" index : {RUN_DIR / 'index.html'}") print(f" slide 1280×720 : {RUN_DIR / 'slide_1280x720.html'}") print(f" debug : {RUN_DIR / 'debug.json'}") print() print(f"Coverage: {len(used)}/{len(expected)} items mapped, missing={list(missing)}") print(f"Stage: composition_preview → slide_fit_preview") if __name__ == "__main__": main()