"""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"""
팝업',
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(''
' MDX 01-2 · DX 와 BIM 의 구분 '
'4개 비교축 × 2개 항목(BIM ↔ DX) 행렬 비교 '
' | ')
for c in cols:
ans_cls = 'ans' if c['is_ans'] else ''
ans_mark = ' 🎯' if c['is_ans'] else ''
body.append(f''
f' '
f'Frame {c["fn"]}{ans_mark} '
f'{c["title"]}'
f' | ')
body.append('
')
# ─── < 키워드 매칭 > 섹션 ───
body.append('| < 키워드 매칭 > '
'— 프레임 키워드와 MDX 본문의 일치도 |
')
# 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'MDX 본문 핵심 단어 {mdx_core_ex} | ')
for c in cols:
if c['sk_hit_count'] == 0:
body.append(f'일치 없음 '
f'(0/{c["sk_total"]}) | ')
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'{kw_show}{rest} | ')
body.append('
')
# 세트 키워드 — 매칭된 세트 raw text + coverage
body.append('')
body.append('| 세트 키워드'
'함께 등장하는 키워드 묶음 | ')
body.append(f'MDX 본문 문장 단위 {mdx_set_ex} | ')
for c in cols:
hit_g = c['hit_groups']
if not hit_g:
body.append(f'일치 없음 '
f'(0/{c["g_total"]}) | ')
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'{" ".join(shows)}'
f' 등 {hit_count}/{c["g_total"]} 세트 일치 | ')
body.append('
')
# 연관 키워드
body.append('')
body.append('| 연관 키워드'
'여러 프레임에 공통 등장하는 보조 키워드 | ')
body.append(f'MDX 본문 일반어 {mdx_rel_ex} | ')
for c in cols:
if c['rel_hit_count'] == 0:
body.append(f'일치 없음 | ')
else:
kw_show = ', '.join(c['rel_hit'][:6])
body.append(f'{kw_show}, … '
f'{c["rel_hit_count"]}/{c["rel_total"]} 일치 | ')
body.append('
')
# V1 점수 행
body.append('')
body.append('| 키워드 점수V1 매칭 점수 | ')
body.append('— (정답 텍스트) | ')
for c in cols:
body.append(f'{c["kw_score"]:.3f} | ')
body.append('
')
# 키워드 산식
body.append('| '
'키워드 점수 = 0.414 × 핵심 + 0.320 × 세트 + 0.265 × 연관 '
'(가중치 = Logistic Regression 학습, TARGET 4/4)'
' |
')
# ─── < 구조 매칭 > 섹션 ───
body.append('| < 구조 매칭 > '
'— MDX 글 구조 분석 결과와 프레임 4 항목 대조 |
')
# 콘텐츠 성격
mdx_aff = mdx_profile.get('content_affinity', {}).get('primary', '—')
body.append('')
body.append('| 콘텐츠 성격'
'콘텐츠 종류 | ')
body.append(f'{aff_ko.get(mdx_aff, mdx_aff)} | ')
for c in cols:
body.append(f'{aff_ko.get(c["aff"], c["aff"])} | ')
body.append('
')
# 시각 의도
mdx_intent = mdx_profile.get('structure_intent', {}).get('primary', '—')
body.append('')
body.append('| 시각 의도'
'레이아웃이 전달하는 시각적 표현 방식 | ')
body.append(f'{intent_ko.get(mdx_intent, mdx_intent)} | ')
for c in cols:
body.append(f'{intent_ko.get(c["intent"], c["intent"])} | ')
body.append('
')
# 편집 슬롯
body.append('')
body.append('| 편집 슬롯'
'텍스트 교체 가능한 영역 | ')
body.append(''
'표에서 컬럼 2개 추출 — 각 컬럼의 헤더=라벨, 첫 행=본문 '
''
'[1] label="DX", body="BIM << DX (Engineering + Management 통합)" '
'[2] label="BIM", body="Only 3D (형상 구현 중심)"'
' '
''
'※ 표면적 추출 — 첫 행만, 나머지 행 (S/W·프로세스·성과품) 무시됨'
' | ')
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''
f'{slot_s:.2f} = {within_part} + 0.3 (라벨) + 0.2 (본문) '
f''
f'프레임 슬롯 정의: ' + ', '.join(c["slots"][:4]) + ' | ')
body.append('
')
# 항목수 / 관계
mdx_card = mdx_profile.get('cardinality', {}).get('ideal', '—')
mdx_rel = mdx_profile.get('relation_type', '—')
body.append('')
body.append('| 항목 수 / 관계'
'비교 단위 개수와 관계 유형(비교/병렬/순서) | ')
body.append(f'{mdx_card}개 / {mdx_rel} | ')
for c in cols:
cv = c['card'].get('ideal', '—')
body.append(f'{cv}개 / {c["rel"]} | ')
body.append('
')
# V3 점수
body.append('')
body.append('| 구조 점수V3 구조 매칭 점수 | ')
body.append('— (정답 텍스트) | ')
for c in cols:
body.append(f'{c["v3_total"]:.3f} | ')
body.append('
')
# 구조 산식
body.append('| '
'구조 점수 = 0.40 × 레이아웃 일치 + 0.35 × 콘텐츠 성격 일치 + 0.25 × 시각 의도 일치 '
'(0~1)'
' |
')
# ─── < 종합 판정 > 섹션 ───
body.append('| < 종합 판정 > '
'— 키워드 + 구조 + 의미 신호 가중 합산 |
')
# confidence 행
body.append('')
body.append('| confidenceV4 종합 점수 (0~1) | ')
body.append('— (정답 텍스트) | ')
for c in cols:
body.append(f'{c["v4_conf"]:.3f} | ')
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''
f'{c["v4_label"]} '
f'{ko} | ')
body.append('
')
# 종합 산식
body.append('| '
'confidence = 0.25 × 핵심 키워드 + 0.20 × 항목 수 + 0.20 × 관계 유형 '
'+ 0.15 × 편집 슬롯 + 0.20 × 의미 임베딩 − 페널티'
' |
')
body.append('
')
# ─── 라벨 / 후속 작업 비용 표 ───
body.append(''
''
'| 판정 라벨 | '
'confidence | '
'후속 작업 | '
'비용 | '
'
')
body.append('| use_as_is | '
'≥ 0.90 | '
'슬롯에 텍스트만 매핑 | '
'자동, AI 호출 0회 |
')
body.append('| light_edit | '
'≥ 0.75 | '
'AI 가 슬롯 텍스트 다듬기 | '
'AI 1회 호출 |
')
body.append('| restructure | '
'≥ 0.60 | '
'항목 수 조정 또는 대안 layout 으로 이동 | '
'사람 + AI 여러 호출 |
')
body.append('| reject | '
'< 0.60 | '
'새로 디자인 | '
'사람 디자인 |
')
body.append('
')
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('
| 카테고리 (영문 enum / 한글) | 매칭 키워드 |
')
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'{cat} {ko} | {kws} |
')
body.append('
')
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('
| 카테고리 (영문 enum / 한글) | 매칭 키워드 |
')
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'{cat} {ko} | {kws} |
')
body.append('
')
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('')
# ─── 키워드 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('| Figma 프레임 텍스트 | 32 개 | 586 |
')
body.append('| BEPS 마스터 텍스트 | 1 개 | 1,410 |
')
body.append('| MDX 검증 구간 | 4 개 | 129 |
')
body.append('| 합계 | 37 개 | 2,125 |
')
body.append('
')
body.append('
')
# ─── 2. 전처리 ───
body.append('')
body.append('
2전처리 — 형태소 분석 + 표기 통합
')
body.append('
'
'| 단계 | 전체 등장 수 | 고유 단어 수 | '
'
')
body.append('| 형태소 분석 결과 (원시) | 13,913 | 1,738 |
')
body.append('| 전처리 후 (표기 통합 + 보존 규칙 적용) | 14,161 | 1,713 |
')
body.append('
')
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('| 핵심 키워드 | 451 | '
'1 프레임에만 | '
'특정 프레임에만 등장 → 식별력 강함. V1 매칭 가중치 0.414 |
')
body.append('| 연관 키워드 | 1,238 | '
'2~9 프레임 | '
'여러 프레임에 보조 등장 → 맥락 보강. V1 매칭 가중치 0.265 |
')
body.append('| 광범위 공통어 | 24 | '
'10+ 프레임 | '
'너무 흔해 식별력 낮음 → 매칭에서 가중 작음 |
')
body.append('| 합계 | 1,713 | — | 전체 고유 키워드 |
')
body.append('
')
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()