Files
C.E.L_Slide_test2/tests/matching/pipeline_16_deck_4pages.py
KyeongminandClaude Opus 4.8 b836e79ee1 wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷
- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규
- Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가
- templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신
- tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분)
- tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가
- docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서
- scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸
- .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외

미완성 작업의 보존용 스냅샷 커밋 (2026-07-02)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 17:03:42 +09:00

1324 lines
76 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"""<!DOCTYPE html>
<html lang="ko"><head><meta charset="utf-8"><title>{title}</title>
<style>{css}</style></head><body>{body}</body></html>"""
# ============================================================
# 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'<h1>{page_subtitle}</h1>')
body.append('<div class="meta">매칭점수 = '
'<code>0.414×핵심 + 0.320×세트 + 0.265×연관</code> · '
'V2 ko-sroberta cosine · V3 v2 schema rerank · V4 template-fit-v1 판정')
body.append('</div>')
# MDX
body.append('<div class="mdx-card">')
body.append(f'<div class="title">MDX {sid} · {mdx_title}'
+ (f'<span class="popup-tag">{popup_label}</span>' if popup_label else '')
+ '</div>')
if mdx_lines:
body.append('<ul>')
for ln in mdx_lines:
body.append(f'<li>{ln}</li>')
body.append('</ul>')
body.append('</div>')
# 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('<table class="cascade">')
body.append('<thead><tr><th class="method-col">방식</th>'
'<th>1순위</th><th>2순위</th><th>3순위</th></tr></thead><tbody>')
def render_row(method_label, sub_label, top3, score_fmt, extra_fmt=None,
empty_message=None):
cells = [f'<td class="method-cell"><strong>{method_label}</strong><small>{sub_label}</small></td>']
# 후보 없는 경우 — 전체 colspan 으로 메시지
if not top3 and empty_message:
cells.append(f'<td class="frame-cell empty-cell" colspan="3">{empty_message}</td>')
return '<tr>' + ''.join(cells) + '</tr>'
# 후보 채우기 (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'<td class="{cls}">'
f'<img src="{img_uri(fn)}" alt="{fn:02d}"/>'
f'<div class="frame-label">Frame {fn}{badge}</div>'
f'<div class="frame-desc">{descriptions.get(fn, "")[:30]}</div>'
f'<div class="frame-score">{score_html}</div>'
+ (f'<div style="margin-top:2pt">{extra_html}</div>' if extra_html else '')
+ '</td>'
)
else:
cells.append('<td class="frame-cell empty-cell">—</td>')
return '<tr>' + ''.join(cells) + '</tr>'
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'<div style="font-size:9pt;color:#991b1b;font-weight:700;padding:8pt">'
f'사용 가능한 프레임 없음<br/>'
f'<span style="font-size:8pt;color:#7f1d1d;font-weight:400">'
f'(32 프레임 전체 평가에서 모두 reject — 기존 프레임에 없는 신규 콘텐츠 → 재구성 필요)'
f'</span></div>'
)
body.append(render_row(
'V4', '+ 판정', v4_top3,
lambda r: f"conf {r['confidence']:.3f}",
lambda r: f'<span class="label-{r["label"]}">{r["label"]}</span>',
empty_message=empty_msg if not v4_top3 else None,
))
body.append('</tbody></table>')
# V4 후보 개수 안내 (32 전체 기준)
total_usable = v4_full_sec.get('usable_count', 0)
total_reject = v4_full_sec.get('reject_count', 0)
body.append(
f'<div style="margin-top:5pt;font-size:8pt;color:#475569">'
f'※ V4 는 32 프레임 전체 평가 결과에서 <code>reject</code> 제외 후 confidence 상위. '
f'<b>사용 가능 {total_usable}개 / reject {total_reject}개 (총 32)</b>'
f'</div>'
)
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 내부 <details> 팝업',
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('<h1>03. V1~V4 매칭 방법 — 무엇이고 어떻게 적용되는가</h1>')
body.append('<div class="meta">키워드 → 의미 → 구조 → 적용 가능성으로 신호를 누적해 캐스케이드 매칭.</div>')
# V1
body.append('<div class="method-block">')
body.append('<div class="method-name"><span class="badge">V1</span>키워드 매칭 (Baseline)</div>')
body.append('<div class="method-tagline">"이 단어들이 같이 나오나?" — 빠른 후보 필터</div>')
body.append('<div class="method-formula">'
'matching_score = 0.414 × 핵심(단독) + 0.320 × 세트(co-occurrence) + 0.265 × 연관'
'</div>')
body.append('<div class="method-detail">'
'· 프레임 텍스트에서 <b>핵심 / 세트 / 연관</b> 3계층 키워드 추출<br/>'
'· 가중치는 <b>Logistic Regression 학습</b> (TARGET 4 × 32, LOOCV 4/4)<br/>'
'· 32 프레임 중 1위 = 후보 프레임. <b>TARGET 4/4 정답</b>'
'</div>')
body.append('</div>')
body.append('<div class="cascade-arrow">↓ V1 Top-K 후보를 다음 축으로 재정렬</div>')
# V2
body.append('<div class="method-block">')
body.append('<div class="method-name"><span class="badge">V2</span>의미 매칭 (semantic rerank)</div>')
body.append('<div class="method-tagline">"단어가 달라도 의미가 같은가?"</div>')
body.append('<div class="method-formula">'
'similarity = cosine(MDX summary embedding, frame.content embedding) · '
'model = jhgan/ko-sroberta-multitask'
'</div>')
body.append('<div class="method-detail">'
'· V1 Top-K 후보 안에서 의미 유사도로 재정렬<br/>'
'· 키워드는 다르지만 같은 주제를 다룰 때 잡아냄'
'</div>')
body.append('</div>')
body.append('<div class="cascade-arrow">↓</div>')
# V3
body.append('<div class="method-block">')
body.append('<div class="method-name"><span class="badge">V3</span>구조 매칭 (structure rerank)</div>')
body.append('<div class="method-tagline">"이 콘텐츠를 담을 수 있는 레이아웃인가?"</div>')
body.append('<div class="method-formula">'
'score = 0.40 × layout_compat + 0.35 × content_affinity + 0.25 × structure_intent'
'</div>')
body.append('<div class="method-detail">'
'· content_affinity 12 enum (concept_definition / goal_axes / persona_benefit / ...)<br/>'
'· structure_intent 9 enum (binary_compare / multi_parallel / state_transition / ...)<br/>'
'· alternative_patterns — 대안 layout 관계 (cycle ↔ 3col-parallel 등)'
'</div>')
body.append('</div>')
body.append('<div class="cascade-arrow">↓</div>')
# V4
body.append('<div class="method-block">')
body.append('<div class="method-name"><span class="badge">V4</span>적용 가능성 판정 (template-fit-v1)</div>')
body.append('<div class="method-tagline">"실제로 쓸 수 있나? 어느 정도 수정해야 하나?"</div>')
body.append('<div class="method-formula">'
'multi-axis: anchor + cardinality + relation + slot + content - penalty<br/>'
'→ 라벨: '
'<span class="label-use_as_is">use_as_is</span> / '
'<span class="label-light_edit">light_edit</span> / '
'<span class="label-restructure">restructure</span> / '
'<span class="label-reject">reject</span>'
'</div>')
body.append('<div class="method-detail">'
'· 점수 1위 ≠ 실사용 가능. <b>판정 라벨</b>로 결정<br/>'
'· reject 면 기존 프레임에 없는 신규 콘텐츠 → 재구성 신호'
'</div>')
body.append('</div>')
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('<h1>04. MDX 01-2 ↔ Top-3 프레임 매칭 비교 — DX 와 BIM 의 구분</h1>')
body.append('<div class="meta">DECK 01 의 정답 프레임 + 차순위 2개. '
'각 프레임이 어떤 키워드/구조로 MDX 와 매칭되었는지 항목별 비교.</div>')
body.append('<table class="cmp">')
body.append('<colgroup>'
'<col class="lbl"><col class="mdx">'
'<col class="frm"><col class="frm"><col class="frm">'
'</colgroup>')
# ─── 헤더 행: MDX 정보 + Frame 이미지들 ───
body.append('<tr class="img-row">')
body.append('<td class="lbl-col" style="background:#1e293b;color:white;text-align:center;'
'font-weight:700;font-size:8pt">항목</td>')
body.append('<td class="mdx-head">'
'<div class="title">MDX 01-2 · DX 와 BIM 의 구분</div>'
'<div class="meta">4개 비교축 × 2개 항목(BIM ↔ DX) 행렬 비교</div>'
'</td>')
for c in cols:
ans_cls = 'ans' if c['is_ans'] else ''
ans_mark = ' 🎯' if c['is_ans'] else ''
body.append(f'<td class="frm-col">'
f'<img src="{img_uri(c["fn"])}" alt="Frame {c["fn"]}"/>'
f'<div class="name {ans_cls}">Frame {c["fn"]}{ans_mark}<br/>'
f'<span style="font-size:7pt;color:#475569;font-weight:400">{c["title"]}</span>'
f'</div></td>')
body.append('</tr>')
# ─── < 키워드 매칭 > 섹션 ───
body.append('<tr class="section-head"><td colspan="5">&lt; 키워드 매칭 &gt; '
'— 프레임 키워드와 MDX 본문의 일치도</td></tr>')
# 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('<tr>')
body.append('<td class="lbl-col">핵심 키워드'
'<span class="def">해당 프레임에만 등장하는 고유 키워드</span></td>')
body.append(f'<td class="mdx-col"><b>MDX 본문 핵심 단어</b><br/>{mdx_core_ex}</td>')
for c in cols:
if c['sk_hit_count'] == 0:
body.append(f'<td class="frm-col"><span style="color:#dc2626">일치 없음</span> '
f'<span style="color:#64748b">(0/{c["sk_total"]})</span></td>')
else:
kw_show = ', '.join(c['sk_hit'][:6])
rest = f', … <span style="color:#64748b">{c["sk_hit_count"]}/{c["sk_total"]} 일치</span>' \
if c['sk_hit_count'] > 6 \
else f' <span style="color:#64748b">({c["sk_hit_count"]}/{c["sk_total"]} 일치)</span>'
body.append(f'<td class="frm-col">{kw_show}{rest}</td>')
body.append('</tr>')
# 세트 키워드 — 매칭된 세트 raw text + coverage
body.append('<tr>')
body.append('<td class="lbl-col">세트 키워드'
'<span class="def">함께 등장하는 키워드 묶음</span></td>')
body.append(f'<td class="mdx-col"><b>MDX 본문 문장 단위</b><br/>{mdx_set_ex}</td>')
for c in cols:
hit_g = c['hit_groups']
if not hit_g:
body.append(f'<td class="frm-col"><span style="color:#dc2626">일치 없음</span> '
f'<span style="color:#64748b">(0/{c["g_total"]})</span></td>')
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} <span style="color:#059669">({cov:.0%})</span>')
hit_count = len(hit_g)
body.append(f'<td class="frm-col">{"<br/>".join(shows)}'
f'<br/><span style="color:#64748b">등 {hit_count}/{c["g_total"]} 세트 일치</span></td>')
body.append('</tr>')
# 연관 키워드
body.append('<tr>')
body.append('<td class="lbl-col">연관 키워드'
'<span class="def">여러 프레임에 공통 등장하는 보조 키워드</span></td>')
body.append(f'<td class="mdx-col"><b>MDX 본문 일반어</b><br/>{mdx_rel_ex}</td>')
for c in cols:
if c['rel_hit_count'] == 0:
body.append(f'<td class="frm-col"><span style="color:#dc2626">일치 없음</span></td>')
else:
kw_show = ', '.join(c['rel_hit'][:6])
body.append(f'<td class="frm-col">{kw_show}, … '
f'<span style="color:#64748b">{c["rel_hit_count"]}/{c["rel_total"]} 일치</span></td>')
body.append('</tr>')
# V1 점수 행
body.append('<tr class="score-row">')
body.append('<td class="lbl-col">키워드 점수<span class="def">V1 매칭 점수</span></td>')
body.append('<td class="mdx-col">— (정답 텍스트)</td>')
for c in cols:
body.append(f'<td>{c["kw_score"]:.3f}</td>')
body.append('</tr>')
# 키워드 산식
body.append('<tr class="formula-row"><td colspan="5">'
'키워드 점수 = 0.414 × 핵심 + 0.320 × 세트 + 0.265 × 연관 '
'<span style="color:#64748b">(가중치 = Logistic Regression 학습, TARGET 4/4)</span>'
'</td></tr>')
# ─── < 구조 매칭 > 섹션 ───
body.append('<tr class="section-head"><td colspan="5">&lt; 구조 매칭 &gt; '
'— MDX 글 구조 분석 결과와 프레임 4 항목 대조</td></tr>')
# 콘텐츠 성격
mdx_aff = mdx_profile.get('content_affinity', {}).get('primary', '—')
body.append('<tr>')
body.append('<td class="lbl-col">콘텐츠 성격'
'<span class="def">콘텐츠 종류</span></td>')
body.append(f'<td class="mdx-col">{aff_ko.get(mdx_aff, mdx_aff)}</td>')
for c in cols:
body.append(f'<td class="frm-col">{aff_ko.get(c["aff"], c["aff"])}</td>')
body.append('</tr>')
# 시각 의도
mdx_intent = mdx_profile.get('structure_intent', {}).get('primary', '—')
body.append('<tr>')
body.append('<td class="lbl-col">시각 의도'
'<span class="def">레이아웃이 전달하는 시각적 표현 방식</span></td>')
body.append(f'<td class="mdx-col">{intent_ko.get(mdx_intent, mdx_intent)}</td>')
for c in cols:
body.append(f'<td class="frm-col">{intent_ko.get(c["intent"], c["intent"])}</td>')
body.append('</tr>')
# 편집 슬롯
body.append('<tr>')
body.append('<td class="lbl-col">편집 슬롯'
'<span class="def">텍스트 교체 가능한 영역</span></td>')
body.append('<td class="mdx-col">'
'<b>표에서 컬럼 2개 추출</b> — 각 컬럼의 헤더=라벨, 첫 행=본문<br/>'
'<span style="font-size:7pt;color:#1e293b">'
'[1] label=<b>"DX"</b>, body="BIM &lt;&lt; DX (Engineering + Management 통합)"<br/>'
'[2] label=<b>"BIM"</b>, body="Only 3D (형상 구현 중심)"'
'</span><br/>'
'<span style="font-size:7pt;color:#dc2626">'
'※ 표면적 추출 — 첫 행만, 나머지 행 (S/W·프로세스·성과품) 무시됨'
'</span></td>')
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 <span style="color:#dc2626">(항목수 불일치)</span>'
body.append(f'<td class="frm-col">'
f'<b style="color:#059669">{slot_s:.2f}</b> = {within_part} + 0.3 (라벨) + 0.2 (본문)<br/>'
f'<span style="font-size:7pt;color:#64748b">'
f'프레임 슬롯 정의: ' + ', '.join(c["slots"][:4]) + '</span></td>')
body.append('</tr>')
# 항목수 / 관계
mdx_card = mdx_profile.get('cardinality', {}).get('ideal', '—')
mdx_rel = mdx_profile.get('relation_type', '—')
body.append('<tr>')
body.append('<td class="lbl-col">항목 수 / 관계'
'<span class="def">비교 단위 개수와 관계 유형(비교/병렬/순서)</span></td>')
body.append(f'<td class="mdx-col">{mdx_card}개 / {mdx_rel}</td>')
for c in cols:
cv = c['card'].get('ideal', '—')
body.append(f'<td class="frm-col">{cv}개 / {c["rel"]}</td>')
body.append('</tr>')
# V3 점수
body.append('<tr class="score-row">')
body.append('<td class="lbl-col">구조 점수<span class="def">V3 구조 매칭 점수</span></td>')
body.append('<td class="mdx-col">— (정답 텍스트)</td>')
for c in cols:
body.append(f'<td>{c["v3_total"]:.3f}</td>')
body.append('</tr>')
# 구조 산식
body.append('<tr class="formula-row"><td colspan="5">'
'구조 점수 = 0.40 × 레이아웃 일치 + 0.35 × 콘텐츠 성격 일치 + 0.25 × 시각 의도 일치 '
'<span style="color:#64748b">(0~1)</span>'
'</td></tr>')
# ─── < 종합 판정 > 섹션 ───
body.append('<tr class="section-head"><td colspan="5">&lt; 종합 판정 &gt; '
'— 키워드 + 구조 + 의미 신호 가중 합산</td></tr>')
# confidence 행
body.append('<tr class="score-row">')
body.append('<td class="lbl-col">confidence<span class="def">V4 종합 점수 (0~1)</span></td>')
body.append('<td class="mdx-col">— (정답 텍스트)</td>')
for c in cols:
body.append(f'<td style="font-size:10pt">{c["v4_conf"]:.3f}</td>')
body.append('</tr>')
# 라벨 행
body.append('<tr class="label-row">')
body.append('<td class="lbl-col">판정 라벨<span class="def">후속 작업 분기</span></td>')
body.append('<td class="mdx-col">— (정답 텍스트)</td>')
for c in cols:
ko, fg, bg = label_ko.get(c['v4_label'], (c['v4_label'], '#000', '#eee'))
body.append(f'<td class="frm-col">'
f'<span style="background:{bg};color:{fg};padding:2pt 7pt;border-radius:3pt;'
f'font-weight:700;font-size:8pt">{c["v4_label"]}</span><br/>'
f'<span style="font-size:7pt;color:#475569">{ko}</span></td>')
body.append('</tr>')
# 종합 산식
body.append('<tr class="formula-row"><td colspan="5">'
'confidence = 0.25 × 핵심 키워드 + 0.20 × 항목 수 + 0.20 × 관계 유형 '
'+ 0.15 × 편집 슬롯 + 0.20 × 의미 임베딩 − 페널티'
'</td></tr>')
body.append('</table>')
# ─── 라벨 / 후속 작업 비용 표 ───
body.append('<table class="routing-table">'
'<thead><tr>'
'<th style="width:14%">판정 라벨</th>'
'<th style="width:12%">confidence</th>'
'<th style="width:42%">후속 작업</th>'
'<th style="width:32%">비용</th>'
'</tr></thead><tbody>')
body.append('<tr><td class="center"><span class="label-use_as_is">use_as_is</span></td>'
'<td class="num">≥ 0.90</td>'
'<td>슬롯에 텍스트만 매핑</td>'
'<td>자동, AI 호출 0회</td></tr>')
body.append('<tr><td class="center"><span class="label-light_edit">light_edit</span></td>'
'<td class="num">≥ 0.75</td>'
'<td>AI 가 슬롯 텍스트 다듬기</td>'
'<td>AI 1회 호출</td></tr>')
body.append('<tr><td class="center"><span class="label-restructure">restructure</span></td>'
'<td class="num">≥ 0.60</td>'
'<td>항목 수 조정 또는 대안 layout 으로 이동</td>'
'<td>사람 + AI 여러 호출</td></tr>')
body.append('<tr><td class="center"><span class="label-reject">reject</span></td>'
'<td class="num">&lt; 0.60</td>'
'<td>새로 디자인</td>'
'<td>사람 디자인</td></tr>')
body.append('</tbody></table>')
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('<h1>05. 구조 매칭 4 항목 — MDX 본문을 어떻게 분석하나</h1>')
body.append('<div class="meta">DECK 04 의 「구조 매칭」 4 항목이 코드에서 실제로 어떻게 동작하는지 상세 설명. '
'각 항목별 정의 + 매칭 방법 + 사용하는 키워드 사전.</div>')
# ─── 1) 콘텐츠 성격 ───
body.append('<div class="sk-block">')
body.append('<div class="sk-title"><span class="num">1</span>콘텐츠 성격 (content_affinity)'
'<span class="accuracy weak">정확도 약함</span></div>')
body.append('<div class="what"><b>무엇인가</b> : MDX 콘텐츠가 어떤 종류의 정보를 담는가 '
'(개념 정의 / 비교 / 정책·요구사항 / 단계 흐름 / 변화 / 역할 등 12 카테고리)</div>')
body.append('<div class="how"><b>매칭 방법</b> : 사전 정의 12 카테고리의 키워드 사전을 가지고 '
'MDX 본문에서 각 키워드 등장 횟수를 카운트 → <b>가장 많이 매칭된 카테고리 = primary</b>. '
'(2위·3위 = secondary)</div>')
body.append('<table class="dict"><thead><tr><th style="width:32%">카테고리 (영문 enum / 한글)</th><th>매칭 키워드</th></tr></thead><tbody>')
aff_dict = [
('concept_definition', '개념 정의', '정의, 이란, 분류, 구분'),
('concept_comparison', '개념 비교', '비교, 대조, 차이, vs, VS, 상호관계'),
('goal_axes', '목표 축', '목표, 궁극적, 비전, 목적, 지향'),
('persona_benefit', '대상별 효익', '발주자, 설계자, 시공자, 기대효과, 혜택, 이익'),
('process_steps', '단계 흐름', '단계, 순서, Step, 1단계, 2단계, 흐름'),
('before_after_change', '변화 / 전환', 'AS-IS, TO-BE, 전환, 혁신, 변화, 이중 변환, 과정 <span style="color:#dc2626">(STRONG 패턴 필수)</span>'),
('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'<tr><td class="cat">{cat}<br/><span style="color:#475569;font-weight:400">{ko}</span></td><td class="kws">{kws}</td></tr>')
body.append('</tbody></table>')
body.append('<div class="example"><b>예시 — MDX 01-2 (DX 와 BIM 의 구분)</b><br/>'
'본문에 「국내, 도입, 사례, 현황」 다수 등장 → <code>policy_requirements</code> (정책/요구사항) 1위로 잘못 잡힘. '
'<span class="limit">한계 : 단순 카운트라 부정확</span> '
'(사람이 보면 「행렬형 비교」가 맞음)</div>')
body.append('</div>')
# ─── 2) 시각 의도 ───
body.append('<div class="sk-block">')
body.append('<div class="sk-title"><span class="num">2</span>시각 의도 (structure_intent)'
'<span class="accuracy weak">정확도 약함</span></div>')
body.append('<div class="what"><b>무엇인가</b> : 콘텐츠를 어떤 방식으로 시각적으로 표현하려는지 '
'(2 항목 비교 / 매트릭스 / 순환 / 단계 / 위계 / 강조 등 9 카테고리)</div>')
body.append('<div class="how"><b>매칭 방법</b> : 사전 9 카테고리 키워드 매칭 (콘텐츠 성격과 동일 방식) '
'+ 일부 카테고리는 <b>STRONG 구절 패턴 추가 검증</b> 필수 '
'(예: state_transition 은 「AS-IS / TO-BE」 같은 강한 구절 1개 이상)</div>')
body.append('<table class="dict"><thead><tr><th style="width:32%">카테고리 (영문 enum / 한글)</th><th>매칭 키워드</th></tr></thead><tbody>')
int_dict = [
('binary_compare', '2 항목 직접 비교', '2개 비교, 2개 개념, 대조, 양분'),
('state_transition', '상태 전환', 'AS-IS, TO-BE, 전환, 혁신, 이중 Transformation, 과정 <span style="color:#dc2626">(STRONG 필수)</span>'),
('cycle_interrelation', '순환 / 상호 관계', '상호관계, 순환, 조화, 교차, 3원'),
('multi_parallel', '다항목 병렬', '3개 병렬, 4개 병렬, 카드 3열, 3관점'),
('hierarchy', '위계', '<i style="color:#64748b">단일 키워드 매칭 금지 — STRONG 구절 패턴만 인정</i>'),
('sequence', '순차 단계', '단계, 순서, 흐름, step'),
('matrix_coverage', '매트릭스 (모든 축 한눈에)', '다면, 관점별, 여러 관점, 축별'),
('persona_mapping', '주체별 매핑', '주체별, 발주자/설계자/시공자, 역할별'),
('singleton_emphasis', '단일 강조', '강조, 문제, 진단, 약점'),
]
for cat, ko, kws in int_dict:
body.append(f'<tr><td class="cat">{cat}<br/><span style="color:#475569;font-weight:400">{ko}</span></td><td class="kws">{kws}</td></tr>')
body.append('</tbody></table>')
body.append('<div class="example"><b>예시 — MDX 01-2</b><br/>'
'제목·본문에 「상호관계」 단어가 있어 <code>cycle_interrelation</code> (순환/상호 관계) 1위로 잡힘. '
'<span class="limit">한계 : 도메인 키워드 사전이라 일반 표현은 못 잡음</span></div>')
body.append('</div>')
# ─── 3) 편집 슬롯 ───
body.append('<div class="sk-block">')
body.append('<div class="sk-title"><span class="num">3</span>편집 슬롯 (slot)'
'<span class="accuracy weak">표면적 검사만</span></div>')
body.append('<div class="what"><b>무엇인가</b> : 프레임에서 텍스트 교체 가능한 영역 '
'(예: 제목 / A 컬럼 라벨 / B 컬럼 라벨 / 행 데이터). '
'MDX 콘텐츠가 이 슬롯들을 채울 수 있는가가 핵심.</div>')
body.append('<div class="how"><b>매칭 방법</b> : MDX 본문에서 슬롯 후보를 자동 추출 → 후보의 라벨/본문 유무로 점수.<br/>'
'&nbsp;&nbsp;· <b>슬롯 후보 추출 우선순위</b> :<br/>'
'&nbsp;&nbsp;&nbsp;&nbsp;① subsections (`##` 헤딩) 있으면 → 각 헤딩이 슬롯. label = 헤딩 텍스트, body = 헤딩 아래 본문<br/>'
'&nbsp;&nbsp;&nbsp;&nbsp;② 표 있으면 → 각 컬럼이 슬롯. label = 컬럼 헤더, body = 첫 행 데이터<br/>'
'&nbsp;&nbsp;&nbsp;&nbsp;③ 불릿 (`*`, `-`) 있으면 → 각 불릿이 슬롯. label = 불릿 첫 단어, body = 불릿 나머지<br/>'
'&nbsp;&nbsp;· <b>점수 계산</b> (휴리스틱) :<br/>'
'&nbsp;&nbsp;&nbsp;&nbsp;① MDX 항목 수가 frame cardinality [min, max] 범위 안 → +0.5<br/>'
'&nbsp;&nbsp;&nbsp;&nbsp;② 모든 후보에 라벨이 비어있지 않음 → +0.3<br/>'
'&nbsp;&nbsp;&nbsp;&nbsp;③ 모든 후보에 본문이 비어있지 않음 → +0.2 / 최대 1.0</div>')
body.append('<div class="example"><b>예시 — MDX 01-2 ↔ Frame 18</b><br/>'
'MDX 본문에 표 발견 → 컬럼 2개 추출 :<br/>'
'&nbsp;&nbsp;[1] label=<b>"DX"</b>, body="BIM &lt;&lt; DX (Engineering + Management 통합)"<br/>'
'&nbsp;&nbsp;[2] label=<b>"BIM"</b>, body="Only 3D (형상 구현 중심)"<br/>'
'→ Frame 18 cardinality 2/2/2 범위 내 (+0.5) + 라벨 둘 다 있음 (+0.3) + 본문 둘 다 있음 (+0.2) = <b>1.00</b><br/>'
'<span class="limit">한계 1 : 표 첫 행만 추출, 나머지 행 (S/W·프로세스·성과품) 무시</span><br/>'
'<span class="limit">한계 2 : "BIM → col_a_label, DX → col_b_label" 같은 frame 슬롯과의 의미 매핑 없음</span> '
'(후속 단계 텍스트 편집자 책임)</div>')
body.append('</div>')
# ─── 4) 항목 수 / 관계 ───
body.append('<div class="sk-block">')
body.append('<div class="sk-title"><span class="num">4</span>항목 수 / 관계 (cardinality + relation_type)'
'<span class="accuracy good">비교적 정확</span></div>')
body.append('<div class="what"><b>무엇인가</b> : MDX 콘텐츠의 비교 단위 개수와 관계 유형. '
'관계 유형 = <code>compare</code>(비교) / <code>parallel</code>(병렬) / <code>sequence</code>(순서) 중 하나.</div>')
body.append('<div class="how"><b>매칭 방법</b> : <code>detect_mdx_layout_v2</code> 함수가 MDX 본문에서 '
'구조 패턴을 자동 감지 → <code>MDX_LAYOUT_STRUCTURE</code> 사전에서 해당 layout 의 '
'family / relation_type / cardinality_ideal 가져옴.<br/>'
'&nbsp;&nbsp;· 표 형태 (`|`, `|---|`) 패턴 → table family<br/>'
'&nbsp;&nbsp;· `1.`, `2.`, `Step 1`, `→` 등 → sequence<br/>'
'&nbsp;&nbsp;· 카드 나열 / `*` 불릿 N개 → parallel + cardinality</div>')
body.append('<div class="example"><b>예시 — MDX 01-2</b><br/>'
'본문이 4 행 표 (범위/SW/프로세스/성과품 × BIM/DX) → <code>compare-rows</code> layout 감지 '
'→ family=table, relation_type=compare, cardinality=2. '
'<b style="color:#065f46">정확</b> (구조적 신호라 안정적)</div>')
body.append('</div>')
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('<h1>06. Figma 디자인이 들어오면 DB 에 어떻게 저장되나 — Frame 18 샘플</h1>')
body.append('<div class="meta">새 Figma 프레임이 입력되면 「키워드 추출」 + 「구조 분석」 두 갈래로 '
'DB 에 저장. 매칭 시 이 DB 가 후보 풀이 됨. Frame 18 (BIM과 DX의 이해) 실제 저장 형태.</div>')
# ─── 헤더 ───
body.append('<div class="db-header">')
body.append('<div class="img-col">')
body.append(f'<img src="{img_uri(FRAME_NUM)}" alt="Frame {FRAME_NUM}"/>')
body.append('</div>')
body.append('<div class="info-col">')
body.append(f'<div class="name">Frame {FRAME_NUM} · {tpl["source"]["title"]}</div>')
body.append(f'<div class="desc">레이아웃 : <code>{vp["layout"]}</code> '
f'(2 항목 × N 행 행렬형 비교)<br/>'
f'정답 매칭 사례 : MDX 01-2 (DX 와 BIM 의 구분) → use_as_is (confidence 0.946)</div>')
body.append('</div></div>')
# ─── 키워드 DB ───
body.append('<div class="db-section">')
body.append('<div class="sec-title">키워드 DB — 프레임 텍스트에서 추출</div>')
body.append('<div class="sec-desc">Figma 프레임의 모든 텍스트 노드를 토큰화 → '
'식별력(unique) / 구조성(co-occur) / 보조성(common) 3 계층으로 분류해 저장.</div>')
# YAML 형식으로 표시
yaml_kw = []
yaml_kw.append('<span class="com"># 핵심 키워드 (이 프레임에만 등장 — 식별력 강함)</span>')
yaml_kw.append(f'<span class="key">standalone</span>:')
yaml_kw.append(f' <span class="key">total_count</span>: <span class="num">{len(standalone_kws)}</span>')
yaml_kw.append(f' <span class="key">terms</span>:')
for k in standalone_kws[:8]:
yaml_kw.append(f' - <span class="str">{k}</span>')
if len(standalone_kws) > 8:
yaml_kw.append(f' <span class="com"># … 외 {len(standalone_kws)-8}개</span>')
yaml_kw.append('')
yaml_kw.append('<span class="com"># 키워드 세트 (같은 줄에 함께 등장하는 묶음 — 구조 단위)</span>')
yaml_kw.append(f'<span class="key">source_text_sets</span>:')
yaml_kw.append(f' <span class="key">total_count</span>: <span class="num">{len(sets_for_frame)}</span>')
yaml_kw.append(f' <span class="key">sets</span>:')
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'<span class="str">{t}</span>' for t in toks)
if len(s["tokens"]) > 6:
toks_str += f' <span class="com"># … 외 {len(s["tokens"])-6}개</span>'
yaml_kw.append(f' - <span class="key">raw</span>: <span class="str">"{raw}"</span>')
yaml_kw.append(f' <span class="key">tokens</span>: [{toks_str}]')
if len(sets_for_frame) > 3:
yaml_kw.append(f' <span class="com"># … 외 {len(sets_for_frame)-3}개 세트</span>')
yaml_kw.append('')
yaml_kw.append('<span class="com"># 연관 키워드 (여러 프레임 공통 — 보조 신호)</span>')
yaml_kw.append(f'<span class="key">related</span>:')
yaml_kw.append(f' <span class="key">total_count</span>: <span class="num">{len(related_kws)}</span>')
yaml_kw.append(f' <span class="key">terms</span>: ['
+ ', '.join(f'<span class="str">{k}</span>' for k in related_kws[:8])
+ f' <span class="com"># … 외 {len(related_kws)-8}개</span>]')
body.append('<pre class="yaml">' + '\n'.join(yaml_kw) + '</pre>')
body.append('</div>')
# ─── 구조 DB ───
body.append('<div class="db-section">')
body.append('<div class="sec-title">구조 DB — 프레임 분석 결과</div>')
body.append('<div class="sec-desc">Figma 프레임의 레이아웃·항목수·관계를 분석 + '
'AI / 사람이 콘텐츠 성격·시각 의도·슬롯·대안 레이아웃 라벨링 → 저장.</div>')
yaml_st = []
yaml_st.append('<span class="com"># 시각 패턴 (자동 감지)</span>')
yaml_st.append('<span class="key">visual_pattern</span>:')
yaml_st.append(f' <span class="key">layout</span>: <span class="str">{vp["layout"]}</span>'
f' <span class="com"># 표/카드 등 레이아웃 ID</span>')
yaml_st.append(f' <span class="key">family</span>: <span class="str">{vp.get("family")}</span>'
f' <span class="com"># table / cards / visual / emphasis</span>')
yaml_st.append(f' <span class="key">relation_type</span>: <span class="str">{vp.get("relation_type")}</span>'
f' <span class="com"># compare / parallel / sequence</span>')
card = vp.get('cardinality', {})
yaml_st.append(f' <span class="key">cardinality</span>:')
yaml_st.append(f' <span class="key">ideal</span>: <span class="num">{card.get("ideal")}</span>'
f' <span class="key">min</span>: <span class="num">{card.get("min")}</span>'
f' <span class="key">max</span>: <span class="num">{card.get("max")}</span>')
yaml_st.append('')
yaml_st.append('<span class="com"># 콘텐츠 성격 (12 카테고리)</span>')
yaml_st.append('<span class="key">content_affinity</span>:')
yaml_st.append(f' <span class="key">primary</span>: <span class="str">{aff["primary"]}</span>'
f' <span class="com"># 행렬형 비교</span>')
if aff.get('secondary'):
yaml_st.append(f' <span class="key">secondary</span>: ['
+ ', '.join(f'<span class="str">{s}</span>' for s in aff['secondary'])
+ ']')
yaml_st.append('')
yaml_st.append('<span class="com"># 시각 의도 (9 카테고리)</span>')
yaml_st.append('<span class="key">structure_intent</span>:')
yaml_st.append(f' <span class="key">primary</span>: <span class="str">{intent["primary"]}</span>'
f' <span class="com"># 매트릭스</span>')
if intent.get('secondary'):
yaml_st.append(f' <span class="key">secondary</span>: ['
+ ', '.join(f'<span class="str">{s}</span>' for s in intent['secondary'])
+ ']')
yaml_st.append('')
yaml_st.append('<span class="com"># 편집 슬롯 (텍스트 교체 가능 영역)</span>')
yaml_st.append('<span class="key">slots</span>:')
for s in slots:
yaml_st.append(f' - <span class="str">{s["id"]}</span>')
yaml_st.append('')
yaml_st.append('<span class="com"># 대안 레이아웃 (이 프레임으로 못 담을 때 대체 가능)</span>')
yaml_st.append('<span class="key">alternative_patterns</span>:')
for a in alts[:4]:
yaml_st.append(f' - <span class="key">pattern</span>: <span class="str">{a["pattern"]}</span>'
f' <span class="key">confidence</span>: <span class="num">{a["confidence"]:.2f}</span>')
if len(alts) > 4:
yaml_st.append(f' <span class="com"># … 외 {len(alts)-4}개</span>')
body.append('<pre class="yaml">' + '\n'.join(yaml_st) + '</pre>')
body.append('</div>')
# ─── 매칭 시 사용 ───
body.append('<div class="db-section" style="background:#f0fdf4;border-color:#86efac">')
body.append('<div class="sec-title" style="color:#065f46;border-color:#86efac">'
'저장된 DB 가 매칭 시 어떻게 쓰이나</div>')
body.append('<div style="font-size:8.5pt;color:#1e293b;line-height:1.6">')
body.append('· <b>키워드 매칭 (V1)</b> : <code>standalone</code> / <code>source_text_sets</code> / '
'<code>related</code> 가 MDX 본문 단어와 대조 → 키워드 점수<br/>')
body.append('· <b>구조 매칭 (V3)</b> : <code>visual_pattern.layout</code> / <code>content_affinity</code> / '
'<code>structure_intent</code> 가 MDX 의 자동 분석 결과와 대조 → 구조 점수<br/>')
body.append('· <b>종합 판정 (V4)</b> : <code>cardinality</code> / <code>relation_type</code> / '
'<code>slots</code> 가 V4 5축의 cardinality/relation/slot 점수에 사용 → confidence<br/>')
body.append('· <b>적용 불가 시</b> : <code>alternative_patterns</code> 의 후보로 fallback')
body.append('</div></div>')
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('<h1>07. 키워드집 정리 과정 — 어떻게 1,713개로 정리되었나</h1>')
body.append('<div class="meta">Figma + BEPS + MDX 문장을 수집 → 형태소 분석 + 표기 통합 → '
'등장 범위별 분류 → 최종 1,713개 키워드 확정.</div>')
# ─── 1. 수집 ───
body.append('<div class="kp-section">')
body.append('<div class="kp-title"><span class="num">1</span>수집 — 3 출처에서 문장 수집</div>')
body.append('<table class="kp"><thead><tr>'
'<th>출처</th><th style="width:18%">입력 문서</th><th style="width:22%">수집된 문장</th>'
'</tr></thead><tbody>')
body.append('<tr><td>Figma 프레임 텍스트</td><td class="num">32 개</td><td class="num">586</td></tr>')
body.append('<tr><td>BEPS 마스터 텍스트</td><td class="num">1 개</td><td class="num">1,410</td></tr>')
body.append('<tr><td>MDX 검증 구간</td><td class="num">4 개</td><td class="num">129</td></tr>')
body.append('<tr class="total"><td>합계</td><td class="num">37 개</td><td class="num">2,125</td></tr>')
body.append('</tbody></table>')
body.append('</div>')
# ─── 2. 전처리 ───
body.append('<div class="kp-section">')
body.append('<div class="kp-title"><span class="num">2</span>전처리 — 형태소 분석 + 표기 통합</div>')
body.append('<table class="kp"><thead><tr>'
'<th>단계</th><th style="width:22%">전체 등장 수</th><th style="width:22%">고유 단어 수</th>'
'</tr></thead><tbody>')
body.append('<tr><td>형태소 분석 결과 (원시)</td><td class="num">13,913</td><td class="num">1,738</td></tr>')
body.append('<tr><td>전처리 후 (표기 통합 + 보존 규칙 적용)</td><td class="num">14,161</td><td class="num"><b>1,713</b></td></tr>')
body.append('</tbody></table>')
body.append('<div class="kp-rules">')
body.append('<b>적용 규칙</b>')
body.append('<ul>')
body.append('<li><b>중요 표기 보존</b> : <code>DX</code>, <code>BIM</code>, <code>S/W</code>, <code>H/W</code>, '
'<code>2D</code>, <code>3D</code>, <code>AS-IS</code>, <code>TO-BE</code> 등 13개 — 깨지면 안 되는 표기</li>')
body.append('<li><b>표기 통합</b> : "디지털 전환", "디지털전환" → 대표 표기 <code>DX</code> 로 합침</li>')
body.append('<li><b>제외</b> : 1글자 단어, 순수 숫자, 공백/기호만 있는 토큰</li>')
body.append('<li><b>품사 사용</b> : 일반명사 / 고유명사 / 외국어 표기 / 숫자 표기만 (조사·동사 등 제외)</li>')
body.append('</ul></div>')
body.append('</div>')
# ─── 3. 분류 ───
body.append('<div class="kp-section">')
body.append('<div class="kp-title"><span class="num">3</span>분류 — 등장 범위별 3 계층 (1,713 = 451 + 1,238 + 24)</div>')
body.append('<table class="kp"><thead><tr>'
'<th style="width:22%">계층</th><th style="width:14%">개수</th>'
'<th style="width:18%">등장 범위</th><th>의미 / 용도</th>'
'</tr></thead><tbody>')
body.append('<tr><td><b>핵심 키워드</b></td><td class="num">451</td>'
'<td>1 프레임에만</td>'
'<td>특정 프레임에만 등장 → <b>식별력 강함</b>. V1 매칭 가중치 0.414</td></tr>')
body.append('<tr><td><b>연관 키워드</b></td><td class="num">1,238</td>'
'<td>2~9 프레임</td>'
'<td>여러 프레임에 보조 등장 → <b>맥락 보강</b>. V1 매칭 가중치 0.265</td></tr>')
body.append('<tr><td><b>광범위 공통어</b></td><td class="num">24</td>'
'<td>10+ 프레임</td>'
'<td>너무 흔해 식별력 낮음 → 매칭에서 가중 작음</td></tr>')
body.append('<tr class="total"><td>합계</td><td class="num">1,713</td><td>—</td><td>전체 고유 키워드</td></tr>')
body.append('</tbody></table>')
body.append('<div style="font-size:8pt;color:#64748b;margin-top:2pt">'
'※ 위에 더해 <b>키워드 세트 524개</b> 별도 — 같은 프레임 내 함께 등장하는 키워드 묶음 '
'(frame-level 시그니처). V1 매칭 가중치 0.320'
'</div>')
body.append('</div>')
# ─── 결과 ───
body.append('<div class="kp-result">'
'<b>결과</b> &nbsp; 2,125 문장 → 형태소 분석 13,913 등장 (고유 1,738) → '
'전처리 후 <b>최종 1,713 고유 키워드</b> + <b>524 키워드 세트</b>.<br/>'
'키워드는 등장 범위로 3 계층 (451 / 1,238 / 24) — V1 매칭의 핵심·연관·광범위 신호로 사용.'
'</div>')
# ─── 4. 샘플 — Frame 18 ───
body.append('<div class="kp-sample">')
body.append('<div class="sample-head">샘플 — Frame 18 (BIM과 DX의 이해) 의 매칭 키워드</div>')
body.append(f'<div class="row"><span class="label">핵심 키워드</span>'
f'<span class="count">{len(sk_18)} 개</span> · '
f'<span class="terms">{", ".join(sk_18[:12])}'
f'{f", … 외 {len(sk_18)-12}개" if len(sk_18)>12 else ""}</span></div>')
sets_short = []
for s in sets_18[:4]:
sets_short.append(s[:30] + ('…' if len(s) > 30 else ''))
body.append(f'<div class="row"><span class="label">키워드 세트</span>'
f'<span class="count">{len(sets_18)} 개</span> · '
f'<span class="terms">{", ".join(sets_short)}'
f'{f", … 외 {len(sets_18)-4}개" if len(sets_18)>4 else ""}</span></div>')
body.append(f'<div class="row"><span class="label">연관 키워드</span>'
f'<span class="count">{len(related_18)} 개</span> · '
f'<span class="terms">{", ".join(related_18[:12])}'
f'{f", … 외 {len(related_18)-12}개" if len(related_18)>12 else ""}</span></div>')
body.append('<div style="font-size:8pt;color:#78350f;margin-top:5pt;border-top:0.5pt dashed #f59e0b;padding-top:3pt">'
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))
+ '</div>')
body.append('</div>')
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()