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>
This commit is contained in:
2026-07-02 17:03:42 +09:00
co-authored by Claude Opus 4.8
parent 97b7833a1b
commit b836e79ee1
527 changed files with 673036 additions and 717 deletions
@@ -0,0 +1,941 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>슬라이드 변환 프로세스 정리</title>
<style>
:root {
--ink: #17211b;
--muted: #66736b;
--line: rgba(35, 50, 40, 0.14);
--paper: #fbf7ec;
--paper-strong: #fffaf0;
--green: #1f5c45;
--green-soft: #dbeadd;
--clay: #9d5d3b;
--clay-soft: #f1dfcf;
--gold: #c99b2f;
--gold-soft: #f6edce;
--blue: #315c8a;
--blue-soft: #dce8f4;
--danger: #8f3f32;
--shadow: 0 24px 80px rgba(38, 45, 38, 0.14);
--radius-lg: 28px;
--radius-md: 18px;
--radius-sm: 12px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
font-family: "Pretendard", "Noto Sans KR", "Malgun Gothic", sans-serif;
background:
radial-gradient(circle at 10% 6%, rgba(201, 155, 47, 0.24), transparent 27rem),
radial-gradient(circle at 86% 14%, rgba(49, 92, 138, 0.18), transparent 24rem),
linear-gradient(135deg, #f8efd9 0%, #f4f0e6 44%, #e8efe5 100%);
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.22;
background-image:
linear-gradient(rgba(23, 33, 27, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(23, 33, 27, 0.04) 1px, transparent 1px);
background-size: 34px 34px;
mask-image: linear-gradient(to bottom, #000, transparent 88%);
}
a {
color: inherit;
}
.shell {
position: relative;
width: min(1760px, calc(100vw - 36px));
min-height: calc(100vh - 36px);
margin: 18px auto;
display: grid;
grid-template-columns: 300px minmax(520px, 1fr) 420px;
gap: 18px;
}
.panel {
background: rgba(255, 250, 240, 0.78);
border: 1px solid rgba(54, 69, 56, 0.16);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.sidebar {
padding: 22px;
display: flex;
flex-direction: column;
gap: 18px;
}
.brand {
padding: 18px 18px 20px;
border-radius: 22px;
background: linear-gradient(145deg, rgba(31, 92, 69, 0.96), rgba(35, 62, 52, 0.96));
color: #fff8e8;
overflow: hidden;
position: relative;
}
.brand::after {
content: "";
position: absolute;
width: 170px;
height: 170px;
border-radius: 50%;
right: -74px;
top: -74px;
background: rgba(255, 255, 255, 0.12);
}
.eyebrow {
margin: 0 0 10px;
color: rgba(255, 248, 232, 0.72);
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
}
h1 {
margin: 0;
font-size: 29px;
line-height: 1.14;
letter-spacing: -0.05em;
}
.brand p {
margin: 14px 0 0;
color: rgba(255, 248, 232, 0.78);
line-height: 1.58;
font-size: 14px;
}
.stage-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.stage-button {
width: 100%;
border: 1px solid var(--line);
border-radius: 20px;
padding: 16px;
text-align: left;
background: rgba(255, 255, 255, 0.42);
color: var(--ink);
cursor: pointer;
transition: transform 180ms ease, background 180ms ease, border 180ms ease, box-shadow 180ms ease;
}
.stage-button:hover {
transform: translateY(-2px);
background: rgba(255, 255, 255, 0.72);
}
.stage-button.active {
background: #fffdf7;
border-color: rgba(31, 92, 69, 0.36);
box-shadow: 0 16px 34px rgba(31, 92, 69, 0.14);
}
.stage-no {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 62px;
height: 27px;
margin-bottom: 12px;
border-radius: 999px;
color: #fffdf7;
background: var(--green);
font-size: 12px;
font-weight: 800;
}
.stage-button:nth-child(2) .stage-no {
background: var(--clay);
}
.stage-button:nth-child(3) .stage-no {
background: var(--blue);
}
.stage-title {
display: block;
font-size: 18px;
font-weight: 850;
letter-spacing: -0.04em;
}
.stage-desc {
display: block;
margin-top: 8px;
color: var(--muted);
font-size: 13px;
line-height: 1.52;
}
.note {
margin-top: auto;
padding: 16px;
border-radius: 18px;
background: rgba(23, 33, 27, 0.06);
color: var(--muted);
font-size: 13px;
line-height: 1.55;
}
.main {
padding: 26px;
overflow: hidden;
}
.hero {
min-height: 214px;
padding: 30px;
border-radius: var(--radius-lg);
color: #fffaf0;
background:
linear-gradient(120deg, rgba(23, 33, 27, 0.92), rgba(31, 92, 69, 0.88)),
radial-gradient(circle at right top, rgba(201, 155, 47, 0.55), transparent 28rem);
position: relative;
overflow: hidden;
}
.hero::after {
content: "";
position: absolute;
inset: auto -50px -110px auto;
width: 360px;
height: 360px;
border-radius: 50%;
border: 44px solid rgba(255, 250, 240, 0.08);
}
.hero-meta {
display: flex;
gap: 10px;
flex-wrap: wrap;
position: relative;
z-index: 1;
}
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 250, 240, 0.12);
border: 1px solid rgba(255, 250, 240, 0.18);
color: rgba(255, 250, 240, 0.88);
font-size: 12px;
font-weight: 700;
}
.hero h2 {
position: relative;
z-index: 1;
margin: 24px 0 12px;
font-size: clamp(34px, 4vw, 58px);
line-height: 0.98;
letter-spacing: -0.075em;
}
.hero p {
position: relative;
z-index: 1;
width: min(760px, 100%);
margin: 0;
color: rgba(255, 250, 240, 0.76);
font-size: 17px;
line-height: 1.7;
word-break: keep-all;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 18px;
}
.summary-card {
border: 1px solid var(--line);
border-radius: 20px;
padding: 18px;
background: rgba(255, 255, 255, 0.52);
}
.summary-card strong {
display: block;
margin-bottom: 8px;
font-size: 13px;
color: var(--green);
}
.summary-card p {
margin: 0;
color: var(--ink);
font-size: 14px;
line-height: 1.62;
word-break: keep-all;
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin: 28px 0 14px;
}
.section-title h3 {
margin: 0;
font-size: 21px;
letter-spacing: -0.045em;
}
.section-title span {
color: var(--muted);
font-size: 13px;
}
.method-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.method-card {
border: 1px solid var(--line);
border-radius: 22px;
padding: 19px;
background: rgba(255, 253, 247, 0.74);
cursor: pointer;
min-height: 190px;
transition: transform 180ms ease, box-shadow 180ms ease, border 180ms ease;
}
.method-card:hover {
transform: translateY(-3px);
box-shadow: 0 18px 40px rgba(23, 33, 27, 0.11);
border-color: rgba(31, 92, 69, 0.32);
}
.method-card.active {
border-color: rgba(31, 92, 69, 0.54);
box-shadow: inset 0 0 0 2px rgba(31, 92, 69, 0.08), 0 18px 42px rgba(31, 92, 69, 0.12);
}
.method-kicker {
display: inline-flex;
padding: 6px 10px;
margin-bottom: 14px;
border-radius: 999px;
background: var(--green-soft);
color: var(--green);
font-size: 12px;
font-weight: 850;
}
.method-card:nth-child(2) .method-kicker {
background: var(--clay-soft);
color: var(--clay);
}
.method-card:nth-child(3) .method-kicker {
background: var(--blue-soft);
color: var(--blue);
}
.method-card h4 {
margin: 0 0 10px;
font-size: 19px;
letter-spacing: -0.04em;
}
.method-card p {
margin: 0;
color: var(--muted);
line-height: 1.58;
font-size: 14px;
word-break: keep-all;
}
.evidence {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 14px;
margin-top: 14px;
}
.evidence-box {
border: 1px solid var(--line);
border-radius: 22px;
padding: 20px;
background: rgba(255, 255, 255, 0.48);
}
.evidence-box h4 {
margin: 0 0 12px;
font-size: 16px;
}
.bullets {
margin: 0;
padding-left: 18px;
color: var(--muted);
line-height: 1.72;
font-size: 14px;
word-break: keep-all;
}
.metric-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 14px;
padding: 10px 0;
border-bottom: 1px dashed rgba(23, 33, 27, 0.14);
font-size: 14px;
}
.metric-row:last-child {
border-bottom: 0;
}
.metric-row span {
color: var(--muted);
}
.metric-row strong {
font-size: 16px;
}
.detail {
padding: 22px;
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
}
.detail-head {
padding: 20px;
border-radius: 22px;
background: #17211b;
color: #fffaf0;
}
.detail-head p {
margin: 0 0 8px;
color: rgba(255, 250, 240, 0.62);
font-size: 12px;
letter-spacing: 0.15em;
text-transform: uppercase;
}
.detail-head h3 {
margin: 0;
font-size: 24px;
line-height: 1.22;
letter-spacing: -0.055em;
}
.detail-body {
overflow: auto;
padding-right: 4px;
}
.detail-block {
padding: 18px;
margin-bottom: 12px;
border: 1px solid var(--line);
border-radius: 20px;
background: rgba(255, 255, 255, 0.54);
}
.detail-block h4 {
margin: 0 0 10px;
font-size: 16px;
}
.detail-block p {
margin: 0;
color: var(--muted);
line-height: 1.68;
font-size: 14px;
word-break: keep-all;
}
.detail-list {
margin: 0;
padding-left: 18px;
color: var(--muted);
line-height: 1.72;
font-size: 14px;
word-break: keep-all;
}
.links {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.report-link {
display: inline-flex;
padding: 9px 12px;
border-radius: 999px;
border: 1px solid rgba(31, 92, 69, 0.2);
background: rgba(255, 255, 255, 0.56);
color: var(--green);
font-size: 13px;
font-weight: 800;
text-decoration: none;
}
.status {
display: inline-flex;
align-items: center;
padding: 7px 10px;
border-radius: 999px;
background: var(--gold-soft);
color: #765715;
font-size: 12px;
font-weight: 850;
}
.footer-line {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 14px;
}
@media (max-width: 1260px) {
.shell {
grid-template-columns: 260px 1fr;
}
.detail {
grid-column: 1 / -1;
min-height: 420px;
}
}
@media (max-width: 860px) {
.shell {
width: min(100vw - 20px, 760px);
grid-template-columns: 1fr;
margin: 10px auto;
}
.summary-grid,
.method-grid,
.evidence {
grid-template-columns: 1fr;
}
.brand h1,
h1 {
font-size: 25px;
}
.hero {
padding: 24px;
}
}
</style>
</head>
<body>
<div class="shell">
<aside class="sidebar panel">
<div class="brand">
<p class="eyebrow">Process Review</p>
<h1>슬라이드 변환 프로세스 정리</h1>
<p>MDX 내용을 어떤 Figma 프레임으로 변환할지 찾기 위해 거친 세 단계의 실험과 판단 근거를 한 화면에서 검토합니다.</p>
</div>
<nav class="stage-list" aria-label="검토 단계">
<button class="stage-button active" type="button" data-stage="stage1">
<span class="stage-no">1단계</span>
<span class="stage-title">기계적 매칭</span>
<span class="stage-desc">단어, 구조, 의미 유사도를 각각 계산해 프레임 후보를 비교한 초기 실험입니다.</span>
</button>
<button class="stage-button" type="button" data-stage="stage2">
<span class="stage-no">2단계</span>
<span class="stage-title">AI 분석 기반 매칭</span>
<span class="stage-desc">AI가 Figma 프레임을 해석하고 키워드, 의미, 구조 정보를 보강해 매칭한 단계입니다.</span>
</button>
<button class="stage-button" type="button" data-stage="stage3">
<span class="stage-no">3단계</span>
<span class="stage-title">사전 정의 기반 매칭</span>
<span class="stage-desc">키워드집과 프레임별 대표 키워드를 먼저 정리한 뒤, MDX를 점수로 매칭한 단계입니다.</span>
</button>
</nav>
<div class="note">
사용 방식: 왼쪽에서 단계를 선택하고, 가운데 카드에서 세부 항목을 누르면 오른쪽 창에 실행 방식, 결과, 한계가 표시됩니다.
</div>
</aside>
<main class="main panel">
<section class="hero" id="hero">
<div class="hero-meta" id="heroMeta"></div>
<h2 id="stageTitle"></h2>
<p id="stageLead"></p>
</section>
<section class="summary-grid" id="summaryGrid"></section>
<div class="section-title">
<h3>세부 검토 항목</h3>
<span>카드를 누르면 오른쪽에 상세가 열립니다</span>
</div>
<section class="method-grid" id="methodGrid"></section>
<section class="evidence">
<div class="evidence-box">
<h4>핵심 판단</h4>
<ul class="bullets" id="judgementList"></ul>
</div>
<div class="evidence-box">
<h4>대표 수치</h4>
<div id="metricList"></div>
</div>
</section>
</main>
<aside class="detail panel" aria-live="polite">
<div class="detail-head">
<p id="detailKicker"></p>
<h3 id="detailTitle"></h3>
</div>
<div class="detail-body" id="detailBody"></div>
</aside>
</div>
<script>
const data = {
stage1: {
meta: ["초기 실험", "단어 기반", "구조 기반", "의미 기반"],
title: "1단계. 기계적 매칭",
lead: "MDX 구간과 Figma 프레임을 수치적으로 비교한 첫 단계입니다. 단어가 얼마나 겹치는지, 구조가 닮았는지, 의미 임베딩이 가까운지를 각각 계산해 Top 후보를 뽑았습니다.",
summaries: [
["했던 방식", "TF-IDF, BM25, 문자 n-gram, Kiwi+BM25, 구조 유사도, AI 메타데이터 유사도 등을 비교했습니다."],
["실행 방법", "MDX 01~03의 섹션과 Figma 32개 프레임 텍스트를 대상으로 각 방법의 Top 1~3 후보를 산출했습니다."],
["실행 결과", "명확한 일부 구간은 잘 맞았지만, 구조 단독 또는 표면 단어 단독 방식은 후보가 흔들렸습니다."],
["한계", "왜 그 프레임이어야 하는지 설명이 약하고, 단어가 적거나 추상적인 MDX에서는 점수 신뢰도가 낮았습니다."]
],
methods: [
{
kicker: "단어 기반",
title: "TF-IDF / BM25 / Kiwi+BM25",
short: "MDX와 프레임 텍스트의 단어 겹침을 점수화했습니다.",
detailTitle: "단어 기반 매칭 상세",
blocks: [
["적용 방식", "MDX 섹션을 문서로 보고, Figma 프레임의 텍스트와 단어 빈도 및 희소도를 비교했습니다. 한국어 형태소 분석을 붙인 Kiwi+BM25 방식도 함께 비교했습니다."],
["실행 결과", "BIM, DX, Process, Product처럼 명확한 용어가 있는 구간은 상위 후보가 잘 잡혔습니다. 반면 단어가 일반적이거나 짧은 구간은 다른 프레임과 쉽게 섞였습니다."],
["한계", "단어가 같다고 같은 의도는 아니고, 단어가 다르다고 다른 의도도 아닙니다. 그래서 표면 단어 기반만으로는 슬라이드 선택 근거가 부족했습니다."]
]
},
{
kicker: "구조 기반",
title: "레이아웃/구성 유사도",
short: "제목, 카드, 비교, 단계, 관계 구조가 비슷한지 비교했습니다.",
detailTitle: "구조 기반 매칭 상세",
blocks: [
["적용 방식", "프레임의 구성 패턴을 비교형, 카드형, 단계형, 관계형처럼 분류하고 MDX 내용의 구조와 맞춰 보았습니다."],
["실행 결과", "구조가 강한 슬라이드는 후보군을 좁히는 데 도움이 됐습니다. 하지만 내용 주제가 다른데 구조만 비슷한 프레임도 함께 올라왔습니다."],
["한계", "구조는 좋은 보조 신호지만, 단독으로는 주제 적합성을 보장하지 못했습니다. 같은 3열 카드라도 담아야 할 의미가 다르면 다른 프레임이어야 합니다."]
]
},
{
kicker: "의미 기반",
title: "임베딩/메타데이터 유사도",
short: "문장 의미와 요약 정보를 벡터 또는 메타 정보로 비교했습니다.",
detailTitle: "의미 기반 매칭 상세",
blocks: [
["적용 방식", "MDX 섹션 요약과 프레임 설명을 의미적으로 비교했습니다. 단어가 완전히 같지 않아도 주제가 가까우면 후보로 올라오도록 설계했습니다."],
["실행 결과", "추상적 주제에서는 단어 기반보다 보완 효과가 있었습니다. 그러나 점수만 보면 판단 근거가 불투명해지는 문제가 있었습니다."],
["한계", "의미 유사도는 그럴듯한 후보를 잘 찾지만, 사람이 검수할 수 있는 명확한 근거를 남기기 어렵습니다. 설명 가능한 규칙이 추가로 필요했습니다."]
]
}
],
judgements: [
"기계적 매칭은 후보 탐색에는 유용했지만, 최종 프레임 선택 근거로는 부족했습니다.",
"단어, 구조, 의미 중 하나만 쓰면 각각 다른 종류의 오답이 발생했습니다.",
"다음 단계에서는 AI가 프레임의 의도와 사용 맥락을 더 풍부하게 해석하도록 실험했습니다."
],
metrics: [
["비교 대상", "MDX 7개 섹션 / Figma 32개 프레임"],
["비교 방식", "8개 매칭 방식"],
["대표 결과", "일부 명확 구간 Top 1 적중"],
["주요 문제", "설명력 및 재현성 부족"]
],
links: [
["초기 비교 결과", "RESULT.md"],
["Phase 26 비교", "MATRIX_PHASE26.html"]
]
},
stage2: {
meta: ["중간 실험", "AI 프레임 해석", "키워드 보강", "구조 보강"],
title: "2단계. AI 분석 기반 매칭",
lead: "AI가 Figma 프레임을 직접 분석해 키워드, 의미, 구조, 사용 가능성을 보강한 단계입니다. 사람의 사전 정의 없이도 프레임의 의도를 넓게 읽어내는지 확인했습니다.",
summaries: [
["했던 방식", "AI가 프레임별 핵심 키워드, 내용 요약, 구조 의도, 적용 가능성을 해석하도록 했습니다."],
["실행 방법", "키워드 점수에 의미 요약, 구조 정보, 템플릿 적합성, 적용 방식 판단을 단계적으로 더했습니다."],
["실행 결과", "검증 대상 4개에서는 기대 프레임이 1순위로 올라오는 결과가 나왔습니다."],
["한계", "결과가 좋아 보여도 AI가 어떤 기준을 만들었는지, 그 기준이 재현 가능한지 검증하기 어려웠습니다."]
],
methods: [
{
kicker: "키워드 추가",
title: "AI가 뽑은 프레임 키워드",
short: "프레임을 보고 핵심어와 후보 anchor를 만들었습니다.",
detailTitle: "AI 키워드 보강 상세",
blocks: [
["적용 방식", "Figma 프레임의 텍스트와 구성 맥락을 보고 AI가 주요 키워드와 대표 표현을 추출했습니다."],
["실행 결과", "사람이 놓칠 수 있는 의미 연결을 일부 보완했습니다. 단순 단어 매칭보다 프레임 의도를 더 잘 설명하는 경우가 있었습니다."],
["한계", "AI가 만든 키워드가 원문에 있는 것인지, 해석으로 추가된 것인지 경계가 흐려졌습니다. 사용자는 이 지점을 가장 강하게 의심했고, 그 의심은 타당했습니다."]
]
},
{
kicker: "의미 추가",
title: "내용 요약과 의미 유사도",
short: "프레임의 설명 가능성을 높이기 위해 내용 요약을 매칭 신호로 넣었습니다.",
detailTitle: "의미 보강 상세",
blocks: [
["적용 방식", "프레임별 내용 요약과 MDX 섹션 요약을 비교해 키워드만으로 부족한 추상적 유사성을 보완했습니다."],
["실행 결과", "4개 검증 대상에서는 기대 프레임이 모두 1순위가 되는 결과가 나왔습니다."],
["한계", "정답을 아는 4개만 보면 좋아 보일 수 있습니다. 홀드아웃 검증이 없으면 과적합인지 일반화 가능한지 판단하기 어렵습니다."]
]
},
{
kicker: "구조 추가",
title: "템플릿 적합성 판단",
short: "프레임 구조와 MDX 내용이 실제 슬라이드로 적용 가능한지 함께 봤습니다.",
detailTitle: "구조 및 적용 가능성 상세",
blocks: [
["적용 방식", "프레임의 카드 수, 관계 구조, 슬롯, 사용 가능성, 수정 필요 여부를 함께 판단했습니다."],
["실행 결과", "단순히 비슷한 프레임을 찾는 것을 넘어, 실제 변환에 쓸 수 있는지 판단하는 방향으로 확장됐습니다."],
["한계", "이 단계도 여전히 AI 판단 비중이 큽니다. 따라서 사람이 검수 가능한 사전 범위와 명시적 규칙이 필요해졌습니다."]
]
}
],
judgements: [
"AI 분석 기반 매칭은 성능을 끌어올렸지만, 근거의 통제 가능성이 약했습니다.",
"좋은 후보를 찾는 능력과 검증 가능한 기준을 갖는 것은 별개의 문제였습니다.",
"그래서 3단계에서는 먼저 키워드집과 프레임별 적용 범위를 정리하는 방식으로 전환했습니다."
],
metrics: [
["검증 대상", "대표 MDX 4개 구간"],
["결과", "기대 프레임 4/4 Top 1"],
["대표 보강", "키워드 + 의미 + 구조 + 적용성"],
["주요 문제", "AI 기준의 불투명성"]
],
links: [
["Phase 26 결과", "MATRIX_PHASE26.html"],
["템플릿 적합성", "TEMPLATE_FIT_V1.md"]
]
},
stage3: {
meta: ["현재 정리", "키워드집", "프레임별 적용", "MDX Top 3"],
title: "3단계. 사전 정의 기반 매칭",
lead: "먼저 전체 문서에서 키워드집을 만들고, 그 키워드집을 각 Figma 프레임에 적용한 뒤, MDX 구간과 프레임을 점수로 비교한 단계입니다. 지금 가장 설명 가능성이 높은 방식입니다.",
summaries: [
["했던 방식", "전체 문서에서 키워드를 추출하고, 프레임별 단독 대표 키워드, 대표 키워드 묶음, 연관 키워드를 정리했습니다."],
["실행 방법", "MDX 키워드와 프레임별 키워드를 비교해 단독 대표, 묶음, 연관 점수를 합산했습니다."],
["실행 결과", "검증 대상 4개 MDX에서 기대 프레임이 모두 1순위로 매칭됐고, Top 3 후보와 점수를 함께 확인할 수 있게 됐습니다."],
["남은 보완", "영어 기능어 잡음 제거와 홀드아웃 검증이 남아 있습니다."]
],
methods: [
{
kicker: "키워드집",
title: "전체 문서 기반 키워드 정리",
short: "어떤 문서에서 몇 개의 문장과 단어를 모았는지 먼저 투명하게 기록했습니다.",
detailTitle: "키워드집 생성 상세",
blocks: [
["입력 범위", "Figma 프레임 32개, BEPS 마스터 텍스트 1개, MDX 검증 구간 4개를 입력으로 사용했습니다."],
["추출 결과", "고유 문장 2,125개와 형태소 분석 결과 13,913개를 거쳐 최종 고유 키워드 1,743개를 만들었습니다."],
["정리 방식", "키워드를 특정 프레임에만 등장, 일부 프레임에 등장, 여러 프레임에 넓게 등장으로 나누고, DX/BIM/S/W 같은 중요 표기는 별도 보조 표시로 관리했습니다."]
]
},
{
kicker: "프레임 적용",
title: "프레임별 3층 키워드",
short: "각 프레임에 키워드집을 적용해 대표 키워드와 묶음을 만들었습니다.",
detailTitle: "프레임별 키워드 적용 상세",
blocks: [
["단독 대표 키워드", "특정 프레임을 강하게 가리키는 희소 키워드입니다. 단독으로 발견돼도 프레임 후보를 좁히는 데 도움을 줍니다."],
["대표 키워드 묶음", "사람 + 자연 + 기술처럼 함께 등장할 때 의미가 강해지는 조합입니다. 단어 하나보다 묶음 일치율이 더 중요한 신호가 됩니다."],
["연관 키워드", "프레임의 주변 맥락을 보조하는 키워드입니다. 단독 결정력은 약하지만 전체 점수를 안정화합니다."]
]
},
{
kicker: "MDX 매칭",
title: "점수 기반 Top 1~3 후보",
short: "MDX 구간별로 가장 적합한 프레임 1~3순위를 점수와 함께 보여줍니다.",
detailTitle: "MDX 매칭 상세",
blocks: [
["점수식", "종합 매칭 점수는 단독 대표 키워드 0.3, 대표 키워드 묶음 0.5, 연관 키워드 0.2를 합산해 계산했습니다."],
["실행 결과", "MDX 01-2는 Frame 18, MDX 02-2.2는 Frame 14, MDX 03-1은 Frame 13, MDX 03-2는 Frame 29가 1순위로 매칭됐습니다."],
["남은 검증", "현재는 알려진 4개 정답 구간 중심입니다. 다른 MDX 섹션으로 홀드아웃 검증을 해야 점수식의 일반화 가능성을 말할 수 있습니다."]
]
}
],
judgements: [
"3단계는 AI가 임의로 만든 체계가 아니라, 원문 기반 키워드와 규칙 기반 점수를 남기는 방식입니다.",
"다만 완전 자동 객관 체계는 아니며, 수동 phrase rule과 user dictionary가 포함된 hybrid 방식입니다.",
"현재 가장 설득력 있는 방향이지만, 영어 잡음 제거와 홀드아웃 검증 후 적용 가능성을 확정하는 것이 안전합니다."
],
metrics: [
["입력 문서", "Figma 32개 + BEPS 1개 + MDX 4개"],
["고유 문장", "2,125개"],
["최종 키워드", "1,743개"],
["검증 결과", "4/4 기대 프레임 Top 1"]
],
links: [
["키워드집 보고서", "KEYWORD_INVENTORY.html"],
["프레임별 적용", "FRAME_KEYWORD_REVIEW.html"],
["MDX 매칭 결과", "MDX_MATCHING_REPORT.html"]
]
}
};
let currentStage = "stage1";
let currentMethod = 0;
const stageButtons = [...document.querySelectorAll(".stage-button")];
const heroMeta = document.getElementById("heroMeta");
const stageTitle = document.getElementById("stageTitle");
const stageLead = document.getElementById("stageLead");
const summaryGrid = document.getElementById("summaryGrid");
const methodGrid = document.getElementById("methodGrid");
const judgementList = document.getElementById("judgementList");
const metricList = document.getElementById("metricList");
const detailKicker = document.getElementById("detailKicker");
const detailTitle = document.getElementById("detailTitle");
const detailBody = document.getElementById("detailBody");
function renderStage(stageKey, methodIndex = 0) {
currentStage = stageKey;
currentMethod = methodIndex;
const stage = data[stageKey];
stageButtons.forEach((button) => {
button.classList.toggle("active", button.dataset.stage === stageKey);
});
heroMeta.innerHTML = stage.meta.map((item) => `<span class="chip">${item}</span>`).join("");
stageTitle.textContent = stage.title;
stageLead.textContent = stage.lead;
summaryGrid.innerHTML = stage.summaries.map(([label, text]) => `
<article class="summary-card">
<strong>${label}</strong>
<p>${text}</p>
</article>
`).join("");
methodGrid.innerHTML = stage.methods.map((method, index) => `
<article class="method-card ${index === methodIndex ? "active" : ""}" role="button" tabindex="0" data-method="${index}">
<span class="method-kicker">${method.kicker}</span>
<h4>${method.title}</h4>
<p>${method.short}</p>
</article>
`).join("");
judgementList.innerHTML = stage.judgements.map((item) => `<li>${item}</li>`).join("");
metricList.innerHTML = stage.metrics.map(([label, value]) => `
<div class="metric-row">
<span>${label}</span>
<strong>${value}</strong>
</div>
`).join("");
renderDetail(methodIndex);
bindMethodCards();
}
function renderDetail(methodIndex) {
const stage = data[currentStage];
const method = stage.methods[methodIndex];
detailKicker.textContent = `${stage.title} / ${method.kicker}`;
detailTitle.textContent = method.detailTitle;
const blockHtml = method.blocks.map(([title, text]) => `
<section class="detail-block">
<h4>${title}</h4>
<p>${text}</p>
</section>
`).join("");
const linkHtml = stage.links.length ? `
<section class="detail-block">
<h4>관련 산출물</h4>
<div class="links">
${stage.links.map(([label, href]) => `<a class="report-link" href="${href}">${label}</a>`).join("")}
</div>
</section>
` : "";
const cautionHtml = `
<section class="detail-block">
<h4>검토 상태</h4>
<p><span class="status">검토용 시안</span></p>
<div class="footer-line">
<span class="report-link">기존 리포트는 수정하지 않음</span>
<span class="report-link">내용 보강 가능</span>
</div>
</section>
`;
detailBody.innerHTML = blockHtml + linkHtml + cautionHtml;
}
function bindMethodCards() {
[...document.querySelectorAll(".method-card")].forEach((card) => {
const activate = () => {
currentMethod = Number(card.dataset.method);
[...document.querySelectorAll(".method-card")].forEach((item) => {
item.classList.toggle("active", item === card);
});
renderDetail(currentMethod);
};
card.addEventListener("click", activate);
card.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
activate();
}
});
});
}
stageButtons.forEach((button) => {
button.addEventListener("click", () => renderStage(button.dataset.stage));
});
renderStage(currentStage);
</script>
</body>
</html>