"""Pipeline Step 16 — 4 페이지 deck 생성 (A4 1 페이지씩). Page 1: TARGET (01-2) × V1~V4 Top-3 이미지 + 점수 Page 2: Holdout (02-2.1) × V1~V4 Top-3 이미지 + 점수 Page 3: V1~V4 각각이 무엇인가 + 어떻게 매칭되는가 (방법 설명) Page 4: V4 가 Figma DB 를 어떻게 정리하는가 (Frame 12 예시) """ import base64 import sys from collections import Counter from pathlib import Path import yaml HERE = Path(__file__).parent PNG_DIR = (HERE / '..' / '..' / 'data' / 'figma_previews').resolve() DECK_01 = HERE / 'DECK_01_TARGET_01_2.html' DECK_02 = HERE / 'DECK_02_HOLDOUT_02_2_1.html' DECK_03 = HERE / 'DECK_03_METHODS.html' DECK_04 = HERE / 'DECK_04_DB_STRUCTURE.html' DECK_05 = HERE / 'DECK_05_STRUCTURE_KEYWORD.html' DECK_06 = HERE / 'DECK_06_FIGMA_TO_DB.html' DECK_07 = HERE / 'DECK_07_KEYWORD_PIPELINE.html' _IMG_CACHE = {} def img_uri(fn): if fn in _IMG_CACHE: return _IMG_CACHE[fn] p = PNG_DIR / f'{fn:02d}.png' if not p.exists(): return '' uri = 'data:image/png;base64,' + base64.b64encode(p.read_bytes()).decode('ascii') _IMG_CACHE[fn] = uri return uri def build_frame_descriptions(auto): out = {} for fid, info in auto['frame_stats'].items(): fnum = info['frame_number'] for set_id, sinfo in auto['source_text_sets'].items(): if sinfo['frame_id'] == fid and sinfo['source_text_index'] == 1: out[fnum] = sinfo['source_text_raw'] break if fnum not in out: out[fnum] = f'Frame {fnum}' return out CSS_BASE = """ @page { size: A4 portrait; margin: 12mm; } * { box-sizing: border-box; } body { font-family: -apple-system, "Segoe UI", Pretendard, "Malgun Gothic", sans-serif; background: white; color: #1e293b; margin: 0; font-size: 9.5pt; line-height: 1.4; } @media screen { body { max-width: 210mm; margin: 10mm auto; box-shadow: 0 4px 24px rgba(0,0,0,0.12); padding: 12mm; background: #fff; } } h1 { font-size: 13pt; border-bottom: 2pt solid #2563eb; padding-bottom: 3pt; margin: 0 0 4pt 0; color: #0f172a; } h2 { font-size: 10pt; color: #1e293b; margin: 6pt 0 3pt; padding: 2pt 5pt; background: #e0e7ff; border-left: 3pt solid #0a6; border-radius: 2pt; } .meta { color: #64748b; font-size: 8pt; font-style: italic; margin-bottom: 5pt; } .mdx-card { background: #f8fafc; border: 0.5pt solid #cbd5e1; border-radius: 3pt; padding: 5pt 8pt; margin: 3pt 0 5pt; } .mdx-card .title { font-weight: 700; color: #0f172a; font-size: 9.5pt; margin-bottom: 2pt; } .popup-tag { display: inline-block; padding: 1pt 5pt; background: #eef2ff; color: #3730a3; border-radius: 8pt; font-size: 7.5pt; font-weight: 700; margin-left: 4pt; vertical-align: middle; } .mdx-card ul { margin: 2pt 0 0 0; padding-left: 12pt; font-size: 8pt; color: #475569; } table { border-collapse: collapse; width: 100%; font-size: 9pt; } th, td { border: 0.5pt solid #cbd5e1; padding: 3pt 5pt; text-align: left; vertical-align: top; } th { background: #1e293b; color: white; font-weight: 700; font-size: 8.5pt; text-align: center; } code { background: #f1f5f9; padding: 0 3pt; border-radius: 2pt; font-family: ui-monospace, Menlo, monospace; font-size: 8.5pt; color: #0f172a; } .formula-box { background: #eff6ff; border: 0.7pt solid #93c5fd; border-radius: 3pt; padding: 5pt 8pt; margin: 3pt 0; font-size: 9pt; color: #1e3a8a; } .label-use_as_is { background: #d1fae5; color: #065f46; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; } .label-light_edit { background: #dbeafe; color: #1e40af; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; } .label-restructure { background: #fef3c7; color: #92400e; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; } .label-reject { background: #fee2e2; color: #991b1b; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 700; } """ CSS_DECK_12 = CSS_BASE + """ /* DECK 1, 2: V1~V4 × Top-3 매트릭스 */ table.cascade { table-layout: fixed; margin-top: 4pt; } table.cascade th.method-col { width: 13%; background: #1e293b; color: white; } table.cascade td.method-cell { background: #f1f5f9; text-align: center; vertical-align: middle; font-weight: 700; color: #1e293b; font-size: 9pt; padding: 3pt; } table.cascade td.method-cell strong { color: #2563eb; font-size: 10pt; display: block; } table.cascade td.method-cell small { font-size: 7pt; color: #64748b; display: block; margin-top: 2pt; line-height: 1.2; } table.cascade td.frame-cell { width: 29%; text-align: center; vertical-align: top; padding: 3pt; } table.cascade td.frame-cell img { max-width: 100%; max-height: 28mm; border: 0.5pt solid #94a3b8; border-radius: 2pt; display: block; margin: 0 auto; } table.cascade td.frame-cell .frame-label { font-size: 8pt; font-weight: 600; color: #2563eb; margin-top: 2pt; } table.cascade td.frame-cell .frame-desc { font-size: 7pt; color: #64748b; margin-top: 1pt; line-height: 1.2; min-height: 2em; } table.cascade td.frame-cell .frame-score { font-size: 8pt; color: #059669; font-family: ui-monospace, monospace; margin-top: 2pt; font-weight: 600; } table.cascade td.frame-cell.answer { background: #fffbeb; } table.cascade td.frame-cell.answer .frame-label { color: #dc2626; } table.cascade td.empty-cell { background: #f8fafc; text-align: center; color: #94a3b8; font-size: 11pt; vertical-align: middle; } """ CSS_DECK_3 = CSS_BASE + """ /* DECK 3: 방법 설명 */ .method-block { border: 0.7pt solid #cbd5e1; border-radius: 4pt; padding: 8pt 10pt; margin: 5pt 0; background: #fafafa; } .method-block .method-name { font-size: 12pt; font-weight: 700; color: #2563eb; margin-bottom: 2pt; } .method-block .method-name .badge { display: inline-block; padding: 2pt 7pt; background: #2563eb; color: white; border-radius: 3pt; font-size: 9pt; margin-right: 6pt; } .method-block .method-tagline { font-size: 9pt; color: #475569; margin-bottom: 5pt; font-style: italic; } .method-block .method-formula { background: white; border: 0.5pt solid #cbd5e1; padding: 4pt 7pt; border-radius: 2pt; font-family: ui-monospace, Menlo, monospace; font-size: 8.5pt; color: #0f172a; margin: 3pt 0; } .method-block .method-detail { font-size: 9pt; line-height: 1.5; color: #1e293b; } .method-block .method-detail b { color: #0f172a; } .cascade-arrow { text-align: center; font-size: 12pt; color: #64748b; margin: 2pt 0; } """ CSS_DECK_4 = CSS_BASE + """ /* DECK 4: MDX 1 vs Top-3 프레임 비교 표 */ @page { size: A4 portrait; margin: 10mm; } table.cmp { width: 100%; border-collapse: collapse; font-size: 7.5pt; line-height: 1.32; table-layout: fixed; margin-top: 4pt; } table.cmp col.lbl { width: 17%; } table.cmp col.mdx { width: 19%; } table.cmp col.frm { width: 21.33%; } table.cmp th, table.cmp td { border: 0.4pt solid #cbd5e1; padding: 3pt 4pt; vertical-align: top; word-break: keep-all; overflow-wrap: anywhere; } table.cmp td.lbl-col { background: #f1f5f9; font-weight: 700; color: #1e293b; } table.cmp td.lbl-col .def { display: block; color: #64748b; font-size: 7pt; font-weight: 400; margin-top: 1pt; line-height: 1.3; } table.cmp td.mdx-col { background: #fefce8; } table.cmp td.frm-col { background: #f0f9ff; } table.cmp tr.answer td.frm-col.is-ans { background: #fffbeb; box-shadow: inset 0 0 0 0.7pt #f59e0b; } table.cmp tr.section-head td { background: #1e293b; color: white; font-weight: 700; padding: 4pt 6pt; font-size: 8.5pt; letter-spacing: 0.5pt; } table.cmp tr.formula-row td { background: #eff6ff; font-family: ui-monospace, Menlo, monospace; font-size: 7.5pt; color: #1e3a8a; padding: 4pt 6pt; } table.cmp tr.score-row td { background: #ecfdf5; font-family: ui-monospace, monospace; font-weight: 700; color: #059669; text-align: center; font-size: 8.5pt; } table.cmp tr.score-row td.lbl-col, table.cmp tr.score-row td.mdx-col { background: #f1f5f9; color: #475569; text-align: left; font-weight: 600; font-family: -apple-system, sans-serif; } table.cmp tr.img-row td { text-align: center; padding: 5pt 3pt; vertical-align: middle; } table.cmp tr.img-row img { max-width: 100%; max-height: 30mm; border: 0.5pt solid #94a3b8; border-radius: 2pt; display: block; margin: 0 auto; } table.cmp tr.img-row .name { font-size: 7.5pt; font-weight: 700; color: #2563eb; margin-top: 2pt; line-height: 1.2; } table.cmp tr.img-row .name.ans { color: #dc2626; } table.cmp .mdx-head { padding: 5pt 6pt; background: #fefce8; } table.cmp .mdx-head .title { font-weight: 700; font-size: 8.5pt; color: #ca8a04; margin-bottom: 2pt; } table.cmp .mdx-head .meta { font-size: 7pt; color: #64748b; margin-bottom: 2pt; line-height: 1.3; } table.cmp .mdx-head ul { margin: 1pt 0; padding-left: 10pt; font-size: 7pt; line-height: 1.3; color: #1e293b; } table.cmp tr.label-row td { text-align: center; font-size: 8pt; padding: 4pt; } table.cmp tr.label-row td.lbl-col, table.cmp tr.label-row td.mdx-col { background: #f1f5f9; text-align: left; font-weight: 600; color: #475569; } .routing-table { width: 100%; font-size: 7.5pt; border-collapse: collapse; margin-top: 5pt; } .routing-table th { background: #1e293b; color: white; padding: 3pt 5pt; font-size: 8pt; } .routing-table td { border: 0.4pt solid #cbd5e1; padding: 3pt 6pt; vertical-align: top; } .routing-table td.center { text-align: center; } .routing-table td.num { text-align: center; font-family: ui-monospace, monospace; font-weight: 700; } table.cmp tr.method-row td { background: #fffbeb; padding: 5pt 8pt; font-size: 7.5pt; line-height: 1.45; color: #78350f; border-top: 0.7pt solid #f59e0b; } table.cmp tr.method-row .head { font-weight: 700; color: #92400e; margin-bottom: 3pt; font-size: 8pt; display: block; } table.cmp tr.method-row .item { margin: 2pt 0; } table.cmp tr.method-row .item b { color: #78350f; } table.cmp tr.method-row code { background: #fef3c7; padding: 0 3pt; border-radius: 2pt; font-size: 7pt; } table.cmp tr.method-row .limit { color: #991b1b; font-weight: 600; } """ def html_wrap(title, css, body): return f""" {title} {body}""" # ============================================================ # DECK 1, 2 공통 — V1~V4 Top-3 매트릭스 # ============================================================ def render_cascade_deck(sid, mdx_title, popup_label, mdx_lines, v1, v2, v3_r5, v4_r2, descriptions, is_target, page_subtitle): v1_sec = v1['mdx_sections'][sid] answer = v1_sec.get('answer_frame_number') if is_target else None body = [] body.append(f'

{page_subtitle}

') body.append('
매칭점수 = ' '0.414×핵심 + 0.320×세트 + 0.265×연관 · ' 'V2 ko-sroberta cosine · V3 v2 schema rerank · V4 template-fit-v1 판정') body.append('
') # MDX body.append('
') body.append(f'
MDX {sid} · {mdx_title}' + (f'{popup_label}' if popup_label else '') + '
') if mdx_lines: body.append('') body.append('
') # V1~V4 cascade table — V4 는 32 전체 평가 결과 사용 (v4_full32) v1_top3 = v1_sec['rank_by_matching_score'][:3] v2_top3 = sorted(v2['mdx_sections'][sid]['v2_rerank'], key=lambda x: x['v2_rank'])[:3] v3_top3 = sorted(v3_r5['mdx_sections'][sid]['v3_r5_rerank'], key=lambda x: x['v3_r5_rank'])[:3] # V4: 32 전체 평가에서 reject 제외, confidence 내림차순 Top-3 v4_full_sec = v4_r2['mdx_sections'].get(sid, {}) # 여기서 v4_r2 는 v4_full32 로 교체됨 v4_all = v4_full_sec.get('judgments_full32', []) v4_usable = [j for j in v4_all if j['label'] != 'reject'][:3] v4_top3 = v4_usable body.append('') body.append('' '') def render_row(method_label, sub_label, top3, score_fmt, extra_fmt=None, empty_message=None): cells = [f''] # 후보 없는 경우 — 전체 colspan 으로 메시지 if not top3 and empty_message: cells.append(f'') return '' + ''.join(cells) + '' # 후보 채우기 (3 미만이면 빈 셀로) for i in range(3): if i < len(top3): r = top3[i] fn = r['frame_number'] is_ans = answer is not None and fn == answer cls = 'frame-cell answer' if is_ans else 'frame-cell' score_html = score_fmt(r) extra_html = extra_fmt(r) if extra_fmt else '' badge = ' 🎯' if is_ans else '' cells.append( f'' ) else: cells.append('') return '' + ''.join(cells) + '' body.append(render_row('V1', '키워드', v1_top3, lambda r: f"score {r['matching_score']:.3f}")) body.append(render_row('V2', '+ 의미', v2_top3, lambda r: f"sem {r['semantic_score']:.3f}")) body.append(render_row('V3', '+ 구조', v3_top3, lambda r: f"struct {r['v3_r5_total']:.3f}")) # V4 — reject 제외, 사용 가능 후보만. 0 개면 "재구성 필요" 메시지. rejected_count = sum(1 for j in v4_all if j['label'] == 'reject') empty_msg = ( f'
' f'사용 가능한 프레임 없음
' f'' f'(32 프레임 전체 평가에서 모두 reject — 기존 프레임에 없는 신규 콘텐츠 → 재구성 필요)' f'
' ) body.append(render_row( 'V4', '+ 판정', v4_top3, lambda r: f"conf {r['confidence']:.3f}", lambda r: f'{r["label"]}', empty_message=empty_msg if not v4_top3 else None, )) body.append('
방식1순위2순위3순위
{method_label}{sub_label}{empty_message}
' f'{fn:02d}' f'
Frame {fn}{badge}
' f'
{descriptions.get(fn, "")[:30]}
' f'
{score_html}
' + (f'
{extra_html}
' if extra_html else '') + '
') # V4 후보 개수 안내 (32 전체 기준) total_usable = v4_full_sec.get('usable_count', 0) total_reject = v4_full_sec.get('reject_count', 0) body.append( f'
' f'※ V4 는 32 프레임 전체 평가 결과에서 reject 제외 후 confidence 상위. ' f'사용 가능 {total_usable}개 / reject {total_reject}개 (총 32)' f'
' ) return ''.join(body) # ============================================================ # DECK 1: TARGET 01-2 # ============================================================ def build_deck1(v1, v2, v3_r5, v4_r2, descriptions): return html_wrap( '01. TARGET 01-2 매칭', CSS_DECK_12, render_cascade_deck( sid='01-2', mdx_title='DX와 BIM의 구분', popup_label='01.mdx §2 내부
팝업', mdx_lines=[ 'BIM ≪ DX (Engineering + Management 통합) | 범위 | Only 3D', '제작·운영 (상용+전용 40~80개) | S/W | 모델 제작용 상용 SW (Revit, Civil 3D, Navisworks, Autocad)', '근본적 문제의식을 통한 개선 | 프로세스 | 기존 2D 설계 방식 유지', '공학 정보·콘텐츠 연계 | 성과품 | 3D 모델 중심', ], v1=v1, v2=v2, v3_r5=v3_r5, v4_r2=v4_r2, descriptions=descriptions, is_target=True, page_subtitle='01. TARGET (정답 있음) — MDX 01-2 × V1~V4 Top-3', ) ) # ============================================================ # DECK 2: Holdout 02-2.1 # ============================================================ def build_deck2(v1, v2, v3_r5, v4_r2, descriptions): return html_wrap( '02. Holdout 02-2.1 매칭', CSS_DECK_12, render_cascade_deck( sid='02-2.1', mdx_title='업무 수행 과정(Process)의 변화', popup_label='02.mdx §2.1 (정답 미지정 · 블라인드)', mdx_lines=[ '생산 방식: 수작업 의존의 반복 업무 → SW 활용한 체계화된 방식으로 전환', '인지·검토: 2D 도면 해석 → 3D 모델 기반의 직관적 인지·검토 체계로 전환', '협업 구조: 개별 문서 중심 → 데이터 통합 기반의 정보 공유·관리 환경으로 전환', '검증·대응: 사후 대응 중심 → 사전 검증 중심의 예방적 업무 방식으로 전환', ], v1=v1, v2=v2, v3_r5=v3_r5, v4_r2=v4_r2, descriptions=descriptions, is_target=False, page_subtitle='02. Holdout (정답 미지정) — MDX 02-2.1 × V1~V4 Top-3', ) ) # ============================================================ # DECK 3: 방법 설명 # ============================================================ def build_deck3(): body = [] body.append('

03. V1~V4 매칭 방법 — 무엇이고 어떻게 적용되는가

') body.append('
키워드 → 의미 → 구조 → 적용 가능성으로 신호를 누적해 캐스케이드 매칭.
') # V1 body.append('
') body.append('
V1키워드 매칭 (Baseline)
') body.append('
"이 단어들이 같이 나오나?" — 빠른 후보 필터
') body.append('
' 'matching_score = 0.414 × 핵심(단독) + 0.320 × 세트(co-occurrence) + 0.265 × 연관' '
') body.append('
' '· 프레임 텍스트에서 핵심 / 세트 / 연관 3계층 키워드 추출
' '· 가중치는 Logistic Regression 학습 (TARGET 4 × 32, LOOCV 4/4)
' '· 32 프레임 중 1위 = 후보 프레임. TARGET 4/4 정답' '
') body.append('
') body.append('
↓ V1 Top-K 후보를 다음 축으로 재정렬
') # V2 body.append('
') body.append('
V2의미 매칭 (semantic rerank)
') body.append('
"단어가 달라도 의미가 같은가?"
') body.append('
' 'similarity = cosine(MDX summary embedding, frame.content embedding) · ' 'model = jhgan/ko-sroberta-multitask' '
') body.append('
' '· V1 Top-K 후보 안에서 의미 유사도로 재정렬
' '· 키워드는 다르지만 같은 주제를 다룰 때 잡아냄' '
') body.append('
') body.append('
') # V3 body.append('
') body.append('
V3구조 매칭 (structure rerank)
') body.append('
"이 콘텐츠를 담을 수 있는 레이아웃인가?"
') body.append('
' 'score = 0.40 × layout_compat + 0.35 × content_affinity + 0.25 × structure_intent' '
') body.append('
' '· content_affinity 12 enum (concept_definition / goal_axes / persona_benefit / ...)
' '· structure_intent 9 enum (binary_compare / multi_parallel / state_transition / ...)
' '· alternative_patterns — 대안 layout 관계 (cycle ↔ 3col-parallel 등)' '
') body.append('
') body.append('
') # V4 body.append('
') body.append('
V4적용 가능성 판정 (template-fit-v1)
') body.append('
"실제로 쓸 수 있나? 어느 정도 수정해야 하나?"
') body.append('
' 'multi-axis: anchor + cardinality + relation + slot + content - penalty
' '→ 라벨: ' 'use_as_is / ' 'light_edit / ' 'restructure / ' 'reject' '
') body.append('
' '· 점수 1위 ≠ 실사용 가능. 판정 라벨로 결정
' '· reject 면 기존 프레임에 없는 신규 콘텐츠 → 재구성 신호' '
') body.append('
') return html_wrap('03. V1~V4 방법 설명', CSS_DECK_3, ''.join(body)) # ============================================================ # DECK 4: V4 → DB 구조 (Frame 12 예시) # ============================================================ def build_deck4(ontology_r2, auto, v1, v3_r5, v4_full, descriptions): """DECK 4 — MDX → Figma 매칭 프로세스 예시 (MDX 01-2 ↔ Frame 18) 구성: [상단] 좌:MDX 01-2 텍스트 | 우:Frame 18 이미지 (어떤 콘텐츠 ↔ 어떤 프레임) [매칭] ① 키워드 매칭 (V1) — 매칭 결과 + 산식 + 점수 ② 구조 매칭 (V3) — 4 항목 좌우 비교 + 산식 + 점수 ③ 종합 판정 (V4) — confidence + 임계값 라우팅 """ SHORT_ID = '18' FRAME_NUM = 18 templates = ontology_r2['templates_v2'] fid = next(fid for fid, e in templates.items() if e.get('short_id') == SHORT_ID) e = templates[fid] vp = e['visual_pattern'] aff = e['content_affinity'] intent = e['structure_intent_v2'] alts = e.get('alternative_patterns', []) slots = e.get('slots', []) card = vp.get('cardinality', {}) # ─── 키워드 데이터 ─── detail = v1['mdx_sections']['01-2']['per_frame_detail'][fid] standalone_kws = sorted(detail['standalone']['total']) all_tokens = set() for sid, s in auto['source_text_sets'].items(): if s['frame_id'] != fid: continue for t in s.get('terms', []): all_tokens.add(t['token']) related_kws = sorted(all_tokens - set(standalone_kws)) sets_for_frame = [] for sid, s in auto['source_text_sets'].items(): if s['frame_id'] != fid: continue sets_for_frame.append(s['source_text_raw']) # ─── 한글 라벨 (영문 enum 은 보고용에서 숨김) ─── aff_ko = { 'comparative_matrix': '행렬형 비교 — 여러 항목을 행/열로 체계적 대조', 'capability_requirements': '역량 / 요건 명세', 'interrelation': '항목 간 상호관계', 'before_after_change': '변화 / 전환 (AS-IS → TO-BE)', 'concept_definition': '개념 정의', 'concept_comparison': '개념 비교', 'goal_axes': '목표 축', 'persona_benefit': '대상별 효익', 'process_steps': '단계 흐름', 'policy_requirements': '정책 / 요구사항', 'stakeholder_roles': '이해관계자 역할', } intent_ko = { 'matrix_coverage': '모든 비교축을 한눈에 보여주는 매트릭스', 'binary_compare': '2 항목 직접 비교', 'cycle_interrelation': '순환 / 상호 관계', 'state_transition': '상태 전환', 'sequence': '순차 단계', 'hierarchy': '위계', 'enumeration': '항목 나열', 'spotlight': '단일 강조', } slot_ko = { 'title': '제목', 'col_a_label': 'A 컬럼 라벨', 'col_b_label': 'B 컬럼 라벨', 'rows': '비교 행 데이터', 'banner_left': '좌측 배너', 'banner_right': '우측 배너', 'col_a_title': 'A 컬럼 제목', 'col_a_as_is': 'A AS-IS', 'col_a_to_be': 'A TO-BE', 'col_b_title': 'B 컬럼 제목', 'col_b_as_is': 'B AS-IS', 'col_b_to_be': 'B TO-BE', } layout_ko = { 'table-2col': '2단 표', 'table-3col': '3단 표', 'paired-rows': '쌍을 이룬 행', 'compare-2col': '2단 좌우 비교', 'compare-rows': '행 단위 비교 매트릭스', 'compare-2banner-top-2col-bottom': '상단 배너 + 하단 2단', '2col-paired': '2단 짝맞춤', '2-boxes': '2개 박스 비교', 'central-split': '가운데 분할', } def ko_aff(k): return aff_ko.get(k, k) def ko_intent(k): return intent_ko.get(k, k) def ko_slot(k): return slot_ko.get(k, k) def ko_layout(k): return layout_ko.get(k, k) # ─── Top-3 프레임 (V4 reject 제외 confidence 순) ─── sys.path.insert(0, str(HERE)) from pipeline_08_v3_r4_structure_rerank import detect_mdx_v2_profile_r4, v3_r4_score mdx_profile = detect_mdx_v2_profile_r4('01-2') sec_v4 = v4_full['mdx_sections']['01-2'] usable = [j for j in sec_v4['judgments_full32'] if j['label'] != 'reject'][:3] templates = ontology_r2['templates_v2'] answer_fn = v1['mdx_sections']['01-2'].get('answer_frame_number') # 각 프레임 데이터 수집 cols = [] for j in usable: fid = j['frame_id'] fn = j['frame_number'] tpl = templates[fid] d = v1['mdx_sections']['01-2']['per_frame_detail'][fid] v3s = v3_r4_score(mdx_profile, tpl) # Frame 핵심 키워드 매칭 sk_total = int(d['standalone'].get('total_count', 0)) sk_hit = sorted(d['standalone'].get('hit', [])) sk_hit_count = int(d['standalone'].get('hit_count', 0)) # Frame 세트 매칭 — coverage > 0 인 세트들 groups = d['keyword_group'].get('groups', []) g_total = int(d['keyword_group'].get('total_count', len(groups))) hit_groups = [g for g in groups if g.get('coverage', 0) > 0] # 가장 잘 매칭된 세트 (coverage 높은 순) hit_groups.sort(key=lambda x: -x.get('coverage', 0)) # Frame 연관 키워드 매칭 rel_total = int(d['related'].get('total_count', 0)) rel_hit = sorted(d['related'].get('hit', [])) rel_hit_count = int(d['related'].get('hit_count', 0)) s_score = float(d['standalone']['score']) g_score = float(d['keyword_group'].get('score_avg', 0)) r_score = float(d['related']['score']) kw_score = 0.414 * s_score + 0.320 * g_score + 0.265 * r_score cols.append({ 'fid': fid, 'fn': fn, 'tpl': tpl, 'title': tpl['source']['title'], 'is_ans': fn == answer_fn, # 키워드 매칭 정보 (MDX ↔ Frame) 'sk_total': sk_total, 'sk_hit': sk_hit, 'sk_hit_count': sk_hit_count, 'g_total': g_total, 'hit_groups': hit_groups, 'rel_total': rel_total, 'rel_hit': rel_hit, 'rel_hit_count': rel_hit_count, 's_score': s_score, 'g_score': g_score, 'r_score': r_score, 'kw_score': kw_score, 'v3_total': v3s['total'], 'v3_layout': v3s['layout_compat'], 'v3_aff': v3s['content_affinity'], 'v3_intent': v3s['structure_intent'], 'v4_conf': j['confidence'], 'v4_label': j['label'], 'v4_axes': j['axes'], 'aff': tpl['content_affinity']['primary'], 'intent': tpl['structure_intent_v2']['primary'], 'card': tpl['visual_pattern'].get('cardinality', {}), 'rel': tpl['visual_pattern'].get('relation_type'), 'slots': [s['id'] for s in tpl.get('slots', [])], }) # ─── 한글 라벨 ─── aff_ko = { 'comparative_matrix': '행렬형 비교', 'concept_definition': '개념 정의', 'concept_comparison': '개념 비교', 'capability_requirements': '역량/요건 명세', 'interrelation': '상호관계', 'before_after_change': '변화/전환', 'goal_axes': '목표 축', 'persona_benefit': '대상별 효익', 'process_steps': '단계 흐름', 'policy_requirements': '정책/요구사항', 'stakeholder_roles': '이해관계자 역할', } intent_ko = { 'matrix_coverage': '매트릭스 (모든 축을 한눈에)', 'binary_compare': '2 항목 직접 비교', 'cycle_interrelation': '순환/상호 관계', 'state_transition': '상태 전환', 'sequence': '순차 단계', 'hierarchy': '위계', 'enumeration': '항목 나열', 'spotlight': '단일 강조', } label_ko = { 'use_as_is': ('그대로 사용', '#065f46', '#d1fae5'), 'light_edit': ('가벼운 편집', '#1e40af', '#dbeafe'), 'restructure': ('구조 재배치', '#92400e', '#fef3c7'), 'reject': ('사용 불가', '#991b1b', '#fee2e2'), } body = [] body.append('

04. MDX 01-2 ↔ Top-3 프레임 매칭 비교 — DX 와 BIM 의 구분

') body.append('
DECK 01 의 정답 프레임 + 차순위 2개. ' '각 프레임이 어떤 키워드/구조로 MDX 와 매칭되었는지 항목별 비교.
') body.append('') body.append('' '' '' '') # ─── 헤더 행: MDX 정보 + Frame 이미지들 ─── body.append('') body.append('') body.append('') for c in cols: ans_cls = 'ans' if c['is_ans'] else '' ans_mark = ' 🎯' if c['is_ans'] else '' body.append(f'') body.append('') # ─── < 키워드 매칭 > 섹션 ─── body.append('') # MDX 본문 단어 예시 (헤더에서 추출 — 본문 그대로의 단어들) mdx_core_ex = 'BIM, DX, Autocad, Navisworks, Civil 3D 등' mdx_set_ex = ('· Only 3D · 모델 제작용 상용 SW · ' '2D 설계방식 유지 등') mdx_rel_ex = '2D, 3D, S/W, 모델, 제작, 상용, 운영, 설계, 방식 등' # 핵심 키워드 — MDX 본문에서 매칭된 단어 vs 각 프레임의 매칭 결과 body.append('') body.append('') body.append(f'') for c in cols: if c['sk_hit_count'] == 0: body.append(f'') else: kw_show = ', '.join(c['sk_hit'][:6]) rest = f', … {c["sk_hit_count"]}/{c["sk_total"]} 일치' \ if c['sk_hit_count'] > 6 \ else f' ({c["sk_hit_count"]}/{c["sk_total"]} 일치)' body.append(f'') body.append('') # 세트 키워드 — 매칭된 세트 raw text + coverage body.append('') body.append('') body.append(f'') for c in cols: hit_g = c['hit_groups'] if not hit_g: body.append(f'') else: shows = [] for g in hit_g[:2]: raw = g.get('source_text_raw', '') if len(raw) > 26: raw = raw[:25] + '…' cov = g.get('coverage', 0) shows.append(f'· {raw} ({cov:.0%})') hit_count = len(hit_g) body.append(f'') body.append('') # 연관 키워드 body.append('') body.append('') body.append(f'') for c in cols: if c['rel_hit_count'] == 0: body.append(f'') else: kw_show = ', '.join(c['rel_hit'][:6]) body.append(f'') body.append('') # V1 점수 행 body.append('') body.append('') body.append('') for c in cols: body.append(f'') body.append('') # 키워드 산식 body.append('') # ─── < 구조 매칭 > 섹션 ─── body.append('') # 콘텐츠 성격 mdx_aff = mdx_profile.get('content_affinity', {}).get('primary', '—') body.append('') body.append('') body.append(f'') for c in cols: body.append(f'') body.append('') # 시각 의도 mdx_intent = mdx_profile.get('structure_intent', {}).get('primary', '—') body.append('') body.append('') body.append(f'') for c in cols: body.append(f'') body.append('') # 편집 슬롯 body.append('') body.append('') body.append('') for c in cols: slot_s = float(c['v4_axes'].get('slot', 0)) # 분해: within(0.5) + labels(0.3) + bodies(0.2) # MDX 01-2 의 detected item_count = 2 mdx_n = 2 within = c['card'].get('min', 0) <= mdx_n <= c['card'].get('max', 99) within_part = '0.5 (항목수 일치)' if within else '0.0 (항목수 불일치)' body.append(f'') body.append('') # 항목수 / 관계 mdx_card = mdx_profile.get('cardinality', {}).get('ideal', '—') mdx_rel = mdx_profile.get('relation_type', '—') body.append('') body.append('') body.append(f'') for c in cols: cv = c['card'].get('ideal', '—') body.append(f'') body.append('') # V3 점수 body.append('') body.append('') body.append('') for c in cols: body.append(f'') body.append('') # 구조 산식 body.append('') # ─── < 종합 판정 > 섹션 ─── body.append('') # confidence 행 body.append('') body.append('') body.append('') for c in cols: body.append(f'') body.append('') # 라벨 행 body.append('') body.append('') body.append('') for c in cols: ko, fg, bg = label_ko.get(c['v4_label'], (c['v4_label'], '#000', '#eee')) body.append(f'') body.append('') # 종합 산식 body.append('') body.append('
항목' '
MDX 01-2 · DX 와 BIM 의 구분
' '
4개 비교축 × 2개 항목(BIM ↔ DX) 행렬 비교
' '
' f'Frame {c[' f'
Frame {c["fn"]}{ans_mark}
' f'{c["title"]}' f'
< 키워드 매칭 > ' '— 프레임 키워드와 MDX 본문의 일치도
핵심 키워드' '해당 프레임에만 등장하는 고유 키워드MDX 본문 핵심 단어
{mdx_core_ex}
일치 없음 ' f'(0/{c["sk_total"]}){kw_show}{rest}
세트 키워드' '함께 등장하는 키워드 묶음MDX 본문 문장 단위
{mdx_set_ex}
일치 없음 ' f'(0/{c["g_total"]}){"
".join(shows)}' f'
등 {hit_count}/{c["g_total"]} 세트 일치
연관 키워드' '여러 프레임에 공통 등장하는 보조 키워드MDX 본문 일반어
{mdx_rel_ex}
일치 없음{kw_show}, … ' f'{c["rel_hit_count"]}/{c["rel_total"]} 일치
키워드 점수V1 매칭 점수— (정답 텍스트){c["kw_score"]:.3f}
' '키워드 점수 = 0.414 × 핵심 + 0.320 × 세트 + 0.265 × 연관 ' '(가중치 = Logistic Regression 학습, TARGET 4/4)' '
< 구조 매칭 > ' '— MDX 글 구조 분석 결과와 프레임 4 항목 대조
콘텐츠 성격' '콘텐츠 종류{aff_ko.get(mdx_aff, mdx_aff)}{aff_ko.get(c["aff"], c["aff"])}
시각 의도' '레이아웃이 전달하는 시각적 표현 방식{intent_ko.get(mdx_intent, mdx_intent)}{intent_ko.get(c["intent"], c["intent"])}
편집 슬롯' '텍스트 교체 가능한 영역' '표에서 컬럼 2개 추출 — 각 컬럼의 헤더=라벨, 첫 행=본문
' '' '[1] label="DX", body="BIM << DX (Engineering + Management 통합)"
' '[2] label="BIM", body="Only 3D (형상 구현 중심)"' '

' '' '※ 표면적 추출 — 첫 행만, 나머지 행 (S/W·프로세스·성과품) 무시됨' '
' f'{slot_s:.2f} = {within_part} + 0.3 (라벨) + 0.2 (본문)
' f'' f'프레임 슬롯 정의: ' + ', '.join(c["slots"][:4]) + '
항목 수 / 관계' '비교 단위 개수와 관계 유형(비교/병렬/순서){mdx_card}개 / {mdx_rel}{cv}개 / {c["rel"]}
구조 점수V3 구조 매칭 점수— (정답 텍스트){c["v3_total"]:.3f}
' '구조 점수 = 0.40 × 레이아웃 일치 + 0.35 × 콘텐츠 성격 일치 + 0.25 × 시각 의도 일치 ' '(0~1)' '
< 종합 판정 > ' '— 키워드 + 구조 + 의미 신호 가중 합산
confidenceV4 종합 점수 (0~1)— (정답 텍스트){c["v4_conf"]:.3f}
판정 라벨후속 작업 분기— (정답 텍스트)' f'{c["v4_label"]}
' f'{ko}
' 'confidence = 0.25 × 핵심 키워드 + 0.20 × 항목 수 + 0.20 × 관계 유형 ' '+ 0.15 × 편집 슬롯 + 0.20 × 의미 임베딩 − 페널티' '
') # ─── 라벨 / 후속 작업 비용 표 ─── body.append('' '' '' '' '' '' '') body.append('' '' '' '') body.append('' '' '' '') body.append('' '' '' '') body.append('' '' '' '') body.append('
판정 라벨confidence후속 작업비용
use_as_is≥ 0.90슬롯에 텍스트만 매핑자동, AI 호출 0회
light_edit≥ 0.75AI 가 슬롯 텍스트 다듬기AI 1회 호출
restructure≥ 0.60항목 수 조정 또는 대안 layout 으로 이동사람 + AI 여러 호출
reject< 0.60새로 디자인사람 디자인
') return html_wrap('04. MDX ↔ Top-3 프레임 비교', CSS_DECK_4, ''.join(body)) # ============================================================ # DECK 5: 구조 매칭 4 항목 — MDX 분석 방법 상세 # ============================================================ CSS_DECK_5 = CSS_BASE + """ @page { size: A4 portrait; margin: 12mm; } .sk-block { background: #fafafa; border: 0.7pt solid #cbd5e1; border-radius: 4pt; padding: 6pt 9pt; margin: 5pt 0; } .sk-block .sk-title { font-size: 11pt; font-weight: 700; color: #2563eb; margin-bottom: 3pt; } .sk-block .sk-title .num { display: inline-block; background: #2563eb; color: white; width: 18pt; height: 18pt; line-height: 18pt; text-align: center; border-radius: 50%; font-size: 9pt; margin-right: 6pt; } .sk-block .sk-title .accuracy { float: right; font-size: 8pt; padding: 2pt 7pt; border-radius: 8pt; font-weight: 700; } .sk-block .accuracy.good { background: #d1fae5; color: #065f46; } .sk-block .accuracy.weak { background: #fee2e2; color: #991b1b; } .sk-block .what { font-size: 9pt; color: #475569; margin: 3pt 0; } .sk-block .what b { color: #0f172a; } .sk-block .how { background: white; border-left: 2.5pt solid #2563eb; padding: 4pt 8pt; margin: 4pt 0; font-size: 8.5pt; line-height: 1.5; } .sk-block .how b { color: #1e3a8a; } .sk-block table.dict { width: 100%; font-size: 8pt; border-collapse: collapse; margin-top: 3pt; } .sk-block table.dict th { background: #1e293b; color: white; padding: 2pt 5pt; font-size: 7.5pt; text-align: left; } .sk-block table.dict td { border: 0.4pt solid #cbd5e1; padding: 2pt 5pt; vertical-align: top; } .sk-block table.dict td.cat { font-weight: 700; color: #1e3a8a; width: 30%; background: #eff6ff; font-family: ui-monospace, monospace; font-size: 7.5pt; } .sk-block table.dict td.kws { color: #475569; font-size: 7.5pt; } .sk-block .example { background: #fffbeb; border: 0.5pt solid #f59e0b; border-radius: 2pt; padding: 4pt 7pt; margin-top: 4pt; font-size: 8pt; color: #78350f; } .sk-block .example b { color: #92400e; } .sk-block .limit { background: #fee2e2; color: #991b1b; padding: 1pt 5pt; border-radius: 2pt; font-size: 7.5pt; font-weight: 600; } """ def build_deck5_structure_keyword(): """DECK 5 — 구조 매칭 4 항목 각각이 MDX 본문을 어떻게 분석하나""" body = [] body.append('

05. 구조 매칭 4 항목 — MDX 본문을 어떻게 분석하나

') body.append('
DECK 04 의 「구조 매칭」 4 항목이 코드에서 실제로 어떻게 동작하는지 상세 설명. ' '각 항목별 정의 + 매칭 방법 + 사용하는 키워드 사전.
') # ─── 1) 콘텐츠 성격 ─── body.append('
') body.append('
1콘텐츠 성격 (content_affinity)' '정확도 약함
') body.append('
무엇인가 : MDX 콘텐츠가 어떤 종류의 정보를 담는가 ' '(개념 정의 / 비교 / 정책·요구사항 / 단계 흐름 / 변화 / 역할 등 12 카테고리)
') body.append('
매칭 방법 : 사전 정의 12 카테고리의 키워드 사전을 가지고 ' 'MDX 본문에서 각 키워드 등장 횟수를 카운트 → 가장 많이 매칭된 카테고리 = primary. ' '(2위·3위 = secondary)
') body.append('') aff_dict = [ ('concept_definition', '개념 정의', '정의, 이란, 분류, 구분'), ('concept_comparison', '개념 비교', '비교, 대조, 차이, vs, VS, 상호관계'), ('goal_axes', '목표 축', '목표, 궁극적, 비전, 목적, 지향'), ('persona_benefit', '대상별 효익', '발주자, 설계자, 시공자, 기대효과, 혜택, 이익'), ('process_steps', '단계 흐름', '단계, 순서, Step, 1단계, 2단계, 흐름'), ('before_after_change', '변화 / 전환', 'AS-IS, TO-BE, 전환, 혁신, 변화, 이중 변환, 과정 (STRONG 패턴 필수)'), ('capability_requirements', '역량 / 요건', '필수, 요건, 필요, 역량, S/W, 도구'), ('comparative_matrix', '행렬형 비교', '다면, 다축, 관점별, 여러 관점, 축'), ('stakeholder_roles', '이해관계자 역할', '역할, 책임, 주도'), ('policy_requirements', '정책 / 요구사항', '정책, 제도, 도입, 거버넌스, 전면 도입, 국외, 국내, 선진, 현황, 사례'), ('tool_ecosystem', '도구 생태계', 'Revit, Navisworks, SketchUp, 소프트웨어, S/W 생태'), ('interrelation', '상호관계', '상호관계, 순환, 조화, 교차, 수렴, 3원, 순환도, 관계도'), ] for cat, ko, kws in aff_dict: body.append(f'') body.append('
카테고리 (영문 enum / 한글)매칭 키워드
{cat}
{ko}
{kws}
') body.append('
예시 — MDX 01-2 (DX 와 BIM 의 구분)
' '본문에 「국내, 도입, 사례, 현황」 다수 등장 → policy_requirements (정책/요구사항) 1위로 잘못 잡힘. ' '한계 : 단순 카운트라 부정확 ' '(사람이 보면 「행렬형 비교」가 맞음)
') body.append('
') # ─── 2) 시각 의도 ─── body.append('
') body.append('
2시각 의도 (structure_intent)' '정확도 약함
') body.append('
무엇인가 : 콘텐츠를 어떤 방식으로 시각적으로 표현하려는지 ' '(2 항목 비교 / 매트릭스 / 순환 / 단계 / 위계 / 강조 등 9 카테고리)
') body.append('
매칭 방법 : 사전 9 카테고리 키워드 매칭 (콘텐츠 성격과 동일 방식) ' '+ 일부 카테고리는 STRONG 구절 패턴 추가 검증 필수 ' '(예: state_transition 은 「AS-IS / TO-BE」 같은 강한 구절 1개 이상)
') body.append('') int_dict = [ ('binary_compare', '2 항목 직접 비교', '2개 비교, 2개 개념, 대조, 양분'), ('state_transition', '상태 전환', 'AS-IS, TO-BE, 전환, 혁신, 이중 Transformation, 과정 (STRONG 필수)'), ('cycle_interrelation', '순환 / 상호 관계', '상호관계, 순환, 조화, 교차, 3원'), ('multi_parallel', '다항목 병렬', '3개 병렬, 4개 병렬, 카드 3열, 3관점'), ('hierarchy', '위계', '단일 키워드 매칭 금지 — STRONG 구절 패턴만 인정'), ('sequence', '순차 단계', '단계, 순서, 흐름, step'), ('matrix_coverage', '매트릭스 (모든 축 한눈에)', '다면, 관점별, 여러 관점, 축별'), ('persona_mapping', '주체별 매핑', '주체별, 발주자/설계자/시공자, 역할별'), ('singleton_emphasis', '단일 강조', '강조, 문제, 진단, 약점'), ] for cat, ko, kws in int_dict: body.append(f'') body.append('
카테고리 (영문 enum / 한글)매칭 키워드
{cat}
{ko}
{kws}
') body.append('
예시 — MDX 01-2
' '제목·본문에 「상호관계」 단어가 있어 cycle_interrelation (순환/상호 관계) 1위로 잡힘. ' '한계 : 도메인 키워드 사전이라 일반 표현은 못 잡음
') body.append('
') # ─── 3) 편집 슬롯 ─── body.append('
') body.append('
3편집 슬롯 (slot)' '표면적 검사만
') body.append('
무엇인가 : 프레임에서 텍스트 교체 가능한 영역 ' '(예: 제목 / A 컬럼 라벨 / B 컬럼 라벨 / 행 데이터). ' 'MDX 콘텐츠가 이 슬롯들을 채울 수 있는가가 핵심.
') body.append('
매칭 방법 : MDX 본문에서 슬롯 후보를 자동 추출 → 후보의 라벨/본문 유무로 점수.
' '  · 슬롯 후보 추출 우선순위 :
' '    ① subsections (`##` 헤딩) 있으면 → 각 헤딩이 슬롯. label = 헤딩 텍스트, body = 헤딩 아래 본문
' '    ② 표 있으면 → 각 컬럼이 슬롯. label = 컬럼 헤더, body = 첫 행 데이터
' '    ③ 불릿 (`*`, `-`) 있으면 → 각 불릿이 슬롯. label = 불릿 첫 단어, body = 불릿 나머지
' '  · 점수 계산 (휴리스틱) :
' '    ① MDX 항목 수가 frame cardinality [min, max] 범위 안 → +0.5
' '    ② 모든 후보에 라벨이 비어있지 않음 → +0.3
' '    ③ 모든 후보에 본문이 비어있지 않음 → +0.2 / 최대 1.0
') body.append('
예시 — MDX 01-2 ↔ Frame 18
' 'MDX 본문에 표 발견 → 컬럼 2개 추출 :
' '  [1] label="DX", body="BIM << DX (Engineering + Management 통합)"
' '  [2] label="BIM", body="Only 3D (형상 구현 중심)"
' '→ Frame 18 cardinality 2/2/2 범위 내 (+0.5) + 라벨 둘 다 있음 (+0.3) + 본문 둘 다 있음 (+0.2) = 1.00
' '한계 1 : 표 첫 행만 추출, 나머지 행 (S/W·프로세스·성과품) 무시
' '한계 2 : "BIM → col_a_label, DX → col_b_label" 같은 frame 슬롯과의 의미 매핑 없음 ' '(후속 단계 텍스트 편집자 책임)
') body.append('
') # ─── 4) 항목 수 / 관계 ─── body.append('
') body.append('
4항목 수 / 관계 (cardinality + relation_type)' '비교적 정확
') body.append('
무엇인가 : MDX 콘텐츠의 비교 단위 개수와 관계 유형. ' '관계 유형 = compare(비교) / parallel(병렬) / sequence(순서) 중 하나.
') body.append('
매칭 방법 : detect_mdx_layout_v2 함수가 MDX 본문에서 ' '구조 패턴을 자동 감지 → MDX_LAYOUT_STRUCTURE 사전에서 해당 layout 의 ' 'family / relation_type / cardinality_ideal 가져옴.
' '  · 표 형태 (`|`, `|---|`) 패턴 → table family
' '  · `1.`, `2.`, `Step 1`, `→` 등 → sequence
' '  · 카드 나열 / `*` 불릿 N개 → parallel + cardinality
') body.append('
예시 — MDX 01-2
' '본문이 4 행 표 (범위/SW/프로세스/성과품 × BIM/DX) → compare-rows layout 감지 ' '→ family=table, relation_type=compare, cardinality=2. ' '정확 (구조적 신호라 안정적)
') body.append('
') return html_wrap('05. 구조 매칭 4 항목 상세', CSS_DECK_5, ''.join(body)) # ============================================================ # DECK 6: Frame 18 → DB 저장 샘플 # ============================================================ CSS_DECK_6 = CSS_BASE + """ @page { size: A4 portrait; margin: 12mm; } .db-header { display: table; width: 100%; margin-bottom: 6pt; } .db-header > div { display: table-cell; vertical-align: middle; } .db-header .img-col { width: 30%; text-align: center; padding-right: 10pt; } .db-header .img-col img { max-width: 100%; max-height: 35mm; border: 0.5pt solid #94a3b8; border-radius: 3pt; } .db-header .info-col .name { font-weight: 700; color: #2563eb; font-size: 11pt; } .db-header .info-col .desc { font-size: 8.5pt; color: #475569; margin-top: 3pt; line-height: 1.5; } .db-section { background: #f8fafc; border: 0.7pt solid #cbd5e1; border-radius: 4pt; padding: 6pt 10pt; margin: 5pt 0; } .db-section .sec-title { font-size: 10pt; font-weight: 700; color: #2563eb; margin-bottom: 4pt; padding-bottom: 2pt; border-bottom: 1pt solid #cbd5e1; } .db-section .sec-desc { font-size: 8pt; color: #64748b; margin-bottom: 4pt; } pre.yaml { background: #1e293b; color: #e2e8f0; border-radius: 3pt; padding: 6pt 10pt; font-family: ui-monospace, "Menlo", monospace; font-size: 7.5pt; line-height: 1.4; margin: 3pt 0; overflow-x: auto; white-space: pre-wrap; word-break: break-all; } pre.yaml .key { color: #7dd3fc; } pre.yaml .str { color: #fde68a; } pre.yaml .num { color: #86efac; } pre.yaml .com { color: #94a3b8; font-style: italic; } """ def build_deck6_figma_to_db(ontology_r2, auto, v1): """DECK 6 — Frame 18 (BIM과 DX의 이해) 의 DB 저장 형태 샘플""" SHORT_ID = '18' FRAME_NUM = 18 templates = ontology_r2['templates_v2'] fid = next(fid for fid, e in templates.items() if e.get('short_id') == SHORT_ID) tpl = templates[fid] # 키워드 데이터 detail = v1['mdx_sections']['01-2']['per_frame_detail'][fid] standalone_kws = sorted(detail['standalone'].get('total', [])) sets_for_frame = [] for sid_a, s in auto['source_text_sets'].items(): if s['frame_id'] != fid: continue sets_for_frame.append({ 'raw': s['source_text_raw'], 'tokens': [t['token'] for t in s.get('terms', [])], }) all_tokens = set() for s in auto['source_text_sets'].values(): if s['frame_id'] != fid: continue for t in s.get('terms', []): all_tokens.add(t['token']) related_kws = sorted(all_tokens - set(standalone_kws)) # 구조 데이터 (templates_v2 에서) vp = tpl['visual_pattern'] aff = tpl['content_affinity'] intent = tpl['structure_intent_v2'] alts = tpl.get('alternative_patterns', []) slots = tpl.get('slots', []) body = [] body.append('

06. Figma 디자인이 들어오면 DB 에 어떻게 저장되나 — Frame 18 샘플

') body.append('
새 Figma 프레임이 입력되면 「키워드 추출」 + 「구조 분석」 두 갈래로 ' 'DB 에 저장. 매칭 시 이 DB 가 후보 풀이 됨. Frame 18 (BIM과 DX의 이해) 실제 저장 형태.
') # ─── 헤더 ─── body.append('
') body.append('
') body.append(f'Frame {FRAME_NUM}') body.append('
') body.append('
') body.append(f'
Frame {FRAME_NUM} · {tpl["source"]["title"]}
') body.append(f'
레이아웃 : {vp["layout"]} ' f'(2 항목 × N 행 행렬형 비교)
' f'정답 매칭 사례 : MDX 01-2 (DX 와 BIM 의 구분) → use_as_is (confidence 0.946)
') body.append('
') # ─── 키워드 DB ─── body.append('
') body.append('
키워드 DB — 프레임 텍스트에서 추출
') body.append('
Figma 프레임의 모든 텍스트 노드를 토큰화 → ' '식별력(unique) / 구조성(co-occur) / 보조성(common) 3 계층으로 분류해 저장.
') # YAML 형식으로 표시 yaml_kw = [] yaml_kw.append('# 핵심 키워드 (이 프레임에만 등장 — 식별력 강함)') yaml_kw.append(f'standalone:') yaml_kw.append(f' total_count: {len(standalone_kws)}') yaml_kw.append(f' terms:') for k in standalone_kws[:8]: yaml_kw.append(f' - {k}') if len(standalone_kws) > 8: yaml_kw.append(f' # … 외 {len(standalone_kws)-8}개') yaml_kw.append('') yaml_kw.append('# 키워드 세트 (같은 줄에 함께 등장하는 묶음 — 구조 단위)') yaml_kw.append(f'source_text_sets:') yaml_kw.append(f' total_count: {len(sets_for_frame)}') yaml_kw.append(f' sets:') for s in sets_for_frame[:3]: raw = s["raw"][:50] + ("…" if len(s["raw"]) > 50 else "") toks = s["tokens"][:6] toks_str = ', '.join(f'{t}' for t in toks) if len(s["tokens"]) > 6: toks_str += f' # … 외 {len(s["tokens"])-6}개' yaml_kw.append(f' - raw: "{raw}"') yaml_kw.append(f' tokens: [{toks_str}]') if len(sets_for_frame) > 3: yaml_kw.append(f' # … 외 {len(sets_for_frame)-3}개 세트') yaml_kw.append('') yaml_kw.append('# 연관 키워드 (여러 프레임 공통 — 보조 신호)') yaml_kw.append(f'related:') yaml_kw.append(f' total_count: {len(related_kws)}') yaml_kw.append(f' terms: [' + ', '.join(f'{k}' for k in related_kws[:8]) + f' # … 외 {len(related_kws)-8}개]') body.append('
' + '\n'.join(yaml_kw) + '
') body.append('
') # ─── 구조 DB ─── body.append('
') body.append('
구조 DB — 프레임 분석 결과
') body.append('
Figma 프레임의 레이아웃·항목수·관계를 분석 + ' 'AI / 사람이 콘텐츠 성격·시각 의도·슬롯·대안 레이아웃 라벨링 → 저장.
') yaml_st = [] yaml_st.append('# 시각 패턴 (자동 감지)') yaml_st.append('visual_pattern:') yaml_st.append(f' layout: {vp["layout"]}' f' # 표/카드 등 레이아웃 ID') yaml_st.append(f' family: {vp.get("family")}' f' # table / cards / visual / emphasis') yaml_st.append(f' relation_type: {vp.get("relation_type")}' f' # compare / parallel / sequence') card = vp.get('cardinality', {}) yaml_st.append(f' cardinality:') yaml_st.append(f' ideal: {card.get("ideal")}' f' min: {card.get("min")}' f' max: {card.get("max")}') yaml_st.append('') yaml_st.append('# 콘텐츠 성격 (12 카테고리)') yaml_st.append('content_affinity:') yaml_st.append(f' primary: {aff["primary"]}' f' # 행렬형 비교') if aff.get('secondary'): yaml_st.append(f' secondary: [' + ', '.join(f'{s}' for s in aff['secondary']) + ']') yaml_st.append('') yaml_st.append('# 시각 의도 (9 카테고리)') yaml_st.append('structure_intent:') yaml_st.append(f' primary: {intent["primary"]}' f' # 매트릭스') if intent.get('secondary'): yaml_st.append(f' secondary: [' + ', '.join(f'{s}' for s in intent['secondary']) + ']') yaml_st.append('') yaml_st.append('# 편집 슬롯 (텍스트 교체 가능 영역)') yaml_st.append('slots:') for s in slots: yaml_st.append(f' - {s["id"]}') yaml_st.append('') yaml_st.append('# 대안 레이아웃 (이 프레임으로 못 담을 때 대체 가능)') yaml_st.append('alternative_patterns:') for a in alts[:4]: yaml_st.append(f' - pattern: {a["pattern"]}' f' confidence: {a["confidence"]:.2f}') if len(alts) > 4: yaml_st.append(f' # … 외 {len(alts)-4}개') body.append('
' + '\n'.join(yaml_st) + '
') body.append('
') # ─── 매칭 시 사용 ─── body.append('
') body.append('
' '저장된 DB 가 매칭 시 어떻게 쓰이나
') body.append('
') body.append('· 키워드 매칭 (V1) : standalone / source_text_sets / ' 'related 가 MDX 본문 단어와 대조 → 키워드 점수
') body.append('· 구조 매칭 (V3) : visual_pattern.layout / content_affinity / ' 'structure_intent 가 MDX 의 자동 분석 결과와 대조 → 구조 점수
') body.append('· 종합 판정 (V4) : cardinality / relation_type / ' 'slots 가 V4 5축의 cardinality/relation/slot 점수에 사용 → confidence
') body.append('· 적용 불가 시 : alternative_patterns 의 후보로 fallback') body.append('
') return html_wrap('06. Frame → DB 저장 샘플', CSS_DECK_6, ''.join(body)) # ============================================================ # DECK 7: 키워드집 정리 과정 + Frame 18 샘플 # ============================================================ CSS_DECK_7 = CSS_BASE + """ @page { size: A4 portrait; margin: 12mm; } .kp-section { margin: 5pt 0 8pt; } .kp-section .kp-title { font-size: 11pt; font-weight: 700; color: #2563eb; padding: 3pt 7pt; background: #e0e7ff; border-left: 3pt solid #2563eb; border-radius: 2pt; margin-bottom: 4pt; } .kp-section .kp-title .num { display: inline-block; background: #2563eb; color: white; width: 18pt; height: 18pt; line-height: 18pt; text-align: center; border-radius: 50%; font-size: 9pt; margin-right: 6pt; } table.kp { width: 100%; font-size: 9pt; border-collapse: collapse; margin: 3pt 0; } table.kp th { background: #1e293b; color: white; padding: 4pt 7pt; font-size: 8.5pt; text-align: left; } table.kp td { border: 0.5pt solid #cbd5e1; padding: 4pt 7pt; vertical-align: top; } table.kp td.num { text-align: right; font-family: ui-monospace, monospace; font-weight: 700; color: #0f172a; } table.kp tr.total td { background: #fef3c7; font-weight: 700; color: #78350f; } .kp-rules { background: #fafafa; border: 0.5pt solid #cbd5e1; border-radius: 3pt; padding: 5pt 9pt; font-size: 8.5pt; line-height: 1.55; margin: 3pt 0; } .kp-rules b { color: #0f172a; } .kp-rules ul { margin: 2pt 0; padding-left: 14pt; } .kp-rules code { background: #f1f5f9; padding: 0 4pt; border-radius: 2pt; font-family: ui-monospace, monospace; font-size: 8pt; } .kp-result { background: #ecfdf5; border: 0.7pt solid #059669; border-radius: 3pt; padding: 5pt 9pt; font-size: 9pt; color: #065f46; margin: 4pt 0; } .kp-result b { color: #064e3b; font-size: 11pt; } .kp-sample { background: #fffbeb; border: 0.7pt solid #f59e0b; border-radius: 3pt; padding: 6pt 10pt; margin: 4pt 0; } .kp-sample .sample-head { font-size: 9.5pt; font-weight: 700; color: #92400e; margin-bottom: 4pt; } .kp-sample .row { font-size: 8.5pt; line-height: 1.6; margin: 3pt 0; } .kp-sample .row .label { display: inline-block; min-width: 22mm; font-weight: 700; color: #78350f; } .kp-sample .row .count { color: #b45309; font-weight: 700; } .kp-sample .row .terms { color: #1e293b; } """ def build_deck7_keyword_pipeline(auto, v1): """DECK 7 — 키워드집 생성 과정 (수집 → 전처리 → 분류) + Frame 18 샘플""" # Frame 18 샘플 데이터 fid_18 = next((fid for fid, info in auto['frame_stats'].items() if info.get('frame_number') == 18), None) detail_18 = v1['mdx_sections']['01-2']['per_frame_detail'][fid_18] sk_18 = sorted(detail_18['standalone'].get('total', [])) sets_18 = [s['source_text_raw'] for s in auto['source_text_sets'].values() if s['frame_id'] == fid_18] all_tokens_18 = set() for s in auto['source_text_sets'].values(): if s['frame_id'] != fid_18: continue for t in s.get('terms', []): all_tokens_18.add(t['token']) related_18 = sorted(all_tokens_18 - set(sk_18)) body = [] body.append('

07. 키워드집 정리 과정 — 어떻게 1,713개로 정리되었나

') body.append('
Figma + BEPS + MDX 문장을 수집 → 형태소 분석 + 표기 통합 → ' '등장 범위별 분류 → 최종 1,713개 키워드 확정.
') # ─── 1. 수집 ─── body.append('
') body.append('
1수집 — 3 출처에서 문장 수집
') body.append('' '' '') body.append('') body.append('') body.append('') body.append('') body.append('
출처입력 문서수집된 문장
Figma 프레임 텍스트32 개586
BEPS 마스터 텍스트1 개1,410
MDX 검증 구간4 개129
합계37 개2,125
') body.append('
') # ─── 2. 전처리 ─── body.append('
') body.append('
2전처리 — 형태소 분석 + 표기 통합
') body.append('' '' '') body.append('') body.append('') body.append('
단계전체 등장 수고유 단어 수
형태소 분석 결과 (원시)13,9131,738
전처리 후 (표기 통합 + 보존 규칙 적용)14,1611,713
') body.append('
') body.append('적용 규칙') body.append('
    ') body.append('
  • 중요 표기 보존 : DX, BIM, S/W, H/W, ' '2D, 3D, AS-IS, TO-BE 등 13개 — 깨지면 안 되는 표기
  • ') body.append('
  • 표기 통합 : "디지털 전환", "디지털전환" → 대표 표기 DX 로 합침
  • ') body.append('
  • 제외 : 1글자 단어, 순수 숫자, 공백/기호만 있는 토큰
  • ') body.append('
  • 품사 사용 : 일반명사 / 고유명사 / 외국어 표기 / 숫자 표기만 (조사·동사 등 제외)
  • ') body.append('
') body.append('
') # ─── 3. 분류 ─── body.append('
') body.append('
3분류 — 등장 범위별 3 계층 (1,713 = 451 + 1,238 + 24)
') body.append('' '' '' '') body.append('' '' '') body.append('' '' '') body.append('' '' '') body.append('') body.append('
계층개수등장 범위의미 / 용도
핵심 키워드4511 프레임에만특정 프레임에만 등장 → 식별력 강함. V1 매칭 가중치 0.414
연관 키워드1,2382~9 프레임여러 프레임에 보조 등장 → 맥락 보강. V1 매칭 가중치 0.265
광범위 공통어2410+ 프레임너무 흔해 식별력 낮음 → 매칭에서 가중 작음
합계1,713전체 고유 키워드
') body.append('
' '※ 위에 더해 키워드 세트 524개 별도 — 같은 프레임 내 함께 등장하는 키워드 묶음 ' '(frame-level 시그니처). V1 매칭 가중치 0.320' '
') body.append('
') # ─── 결과 ─── body.append('
' '결과   2,125 문장 → 형태소 분석 13,913 등장 (고유 1,738) → ' '전처리 후 최종 1,713 고유 키워드 + 524 키워드 세트.
' '키워드는 등장 범위로 3 계층 (451 / 1,238 / 24) — V1 매칭의 핵심·연관·광범위 신호로 사용.' '
') # ─── 4. 샘플 — Frame 18 ─── body.append('
') body.append('
샘플 — Frame 18 (BIM과 DX의 이해) 의 매칭 키워드
') body.append(f'
핵심 키워드' f'{len(sk_18)} 개 · ' f'{", ".join(sk_18[:12])}' f'{f", … 외 {len(sk_18)-12}개" if len(sk_18)>12 else ""}
') sets_short = [] for s in sets_18[:4]: sets_short.append(s[:30] + ('…' if len(s) > 30 else '')) body.append(f'
키워드 세트' f'{len(sets_18)} 개 · ' f'{", ".join(sets_short)}' f'{f", … 외 {len(sets_18)-4}개" if len(sets_18)>4 else ""}
') body.append(f'
연관 키워드' f'{len(related_18)} 개 · ' f'{", ".join(related_18[:12])}' f'{f", … 외 {len(related_18)-12}개" if len(related_18)>12 else ""}
') body.append('
' f'→ Frame 18 은 핵심 {len(sk_18)} + 세트 {len(sets_18)} + 연관 {len(related_18)} 개 키워드를 가지고 ' 'MDX 본문과 매칭. MDX 01-2 (DX와 BIM의 구분) 와는 핵심 25/{0} 일치, 세트 38/{1} 일치, 연관 75/{2} 일치 ' '→ 키워드 점수 0.930 → V4 confidence 0.946 → use_as_is (그대로 사용)' .format(len(sk_18), len(sets_18), len(related_18)) + '
') body.append('
') return html_wrap('07. 키워드집 정리 과정', CSS_DECK_7, ''.join(body)) # ============================================================ # 메인 # ============================================================ def main(): v1 = yaml.safe_load((HERE / 'mdx_matching_result.yaml').read_text(encoding='utf-8')) v2 = yaml.safe_load((HERE / 'v2_semantic_rerank_result.yaml').read_text(encoding='utf-8')) v3_r5 = yaml.safe_load((HERE / 'v3_structure_rerank_r5_result.yaml').read_text(encoding='utf-8')) # V4 = 32 전체 평가 (v4_full32_result.yaml) 사용 v4_r2 = yaml.safe_load((HERE / 'v4_full32_result.yaml').read_text(encoding='utf-8')) auto = yaml.safe_load((HERE / 'auto_anchor_candidates.yaml').read_text(encoding='utf-8')) ontology_r2 = yaml.safe_load((HERE / 'structure_ontology_v2_final_r2.yaml').read_text(encoding='utf-8')) descriptions = build_frame_descriptions(auto) DECK_01.write_text(build_deck1(v1, v2, v3_r5, v4_r2, descriptions), encoding='utf-8') DECK_02.write_text(build_deck2(v1, v2, v3_r5, v4_r2, descriptions), encoding='utf-8') DECK_03.write_text(build_deck3(), encoding='utf-8') DECK_04.write_text(build_deck4(ontology_r2, auto, v1, v3_r5, v4_r2, descriptions), encoding='utf-8') DECK_05.write_text(build_deck5_structure_keyword(), encoding='utf-8') DECK_06.write_text(build_deck6_figma_to_db(ontology_r2, auto, v1), encoding='utf-8') DECK_07.write_text(build_deck7_keyword_pipeline(auto, v1), encoding='utf-8') print('=' * 70) print('Deck 7 페이지 생성 완료 (A4 1 페이지씩)') print('=' * 70) for p in [DECK_01, DECK_02, DECK_03, DECK_04, DECK_05, DECK_06, DECK_07]: size_kb = p.stat().st_size / 1024 print(f' {p.name:38} {size_kb:>8.0f} KB') if __name__ == '__main__': main()