"""Phase 18~21 일괄 실행 + 각 Phase 별 MD/HTML 생성
Phase 18: 키워드만 (synonym X)
Phase 19: synonym + 키워드
Phase 20: synonym + 키워드 + 내용
Phase 21: synonym + 키워드 + 내용 + 구조
"""
import sys
import json
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from phase_common import (
TARGET_UNITS, load_synonyms, load_32_frames, compute_df_idf_tier,
extract_mdx_keywords, keyword_score, content_scores_batch,
detect_mdx_layout_v2, structural_match_v2,
load_target_units, load_frame_index,
)
ROOT = Path(r"d:\ad-hoc\kei\design_agent")
def match_phase(phase_num, use_synonyms, use_content, use_structure,
units_full, units_title, frames, df, idf, tier, vocab, synonyms,
idx_data, frame_to_short):
"""매칭 수행 + 결과 반환"""
# 가중치 설정
if phase_num == 18:
w_kw, w_content, w_struct = 1.0, 0.0, 0.0
elif phase_num == 19:
w_kw, w_content, w_struct = 1.0, 0.0, 0.0
elif phase_num == 20:
w_kw, w_content, w_struct = 0.5, 0.5, 0.0
elif phase_num == 21:
w_kw, w_content, w_struct = 0.5, 0.3, 0.2
reports = []
hits = 0
for uid, display, correct_sid, *_ in TARGET_UNITS:
text = units_full[uid]
title = units_title[uid]
syns = synonyms if use_synonyms else None
# MDX 키워드
mdx_kws = extract_mdx_keywords(text, vocab, synonyms=syns)
# 내용 (제목 계층)
mdx_content_query = title
# MDX 구조
mdx_layout = detect_mdx_layout_v2(text) if use_structure else ""
# 내용 점수 (한 번에)
c_scores = content_scores_batch(mdx_content_query, frames) if use_content else {}
ranked = []
for fid, v in frames.items():
fig_kws = set(v["keywords"])
k_s, inter = keyword_score(mdx_kws, fig_kws, idf, tier)
c_s = c_scores.get(fid, 0.0) if use_content else 0.0
s_s = structural_match_v2(mdx_layout, v["layout"]) if use_structure else 0.0
final = w_kw * k_s + w_content * c_s + w_struct * s_s
ranked.append((fid, final, {"kw": k_s, "content": c_s, "struct": s_s,
"inter": sorted(inter), "final": final}))
ranked.sort(key=lambda x: -x[1])
top3 = ranked[:3]
top_sid = frame_to_short.get(top3[0][0], "?")
if top_sid == correct_sid:
hits += 1
reports.append({
"uid": uid, "display": display, "correct_sid": correct_sid,
"mdx_kws": mdx_kws, "mdx_content": mdx_content_query,
"mdx_layout": mdx_layout if use_structure else None,
"top3": top3,
})
return reports, hits, (w_kw, w_content, w_struct)
def write_md(phase_num, description, reports, hits, weights, frames,
idx_data, frame_to_short):
w_kw, w_content, w_struct = weights
png_rel = "../../data/figma_previews/"
lines = []
lines.append(f"# Phase {phase_num} — {description}")
lines.append("")
# 공식
formula_parts = []
if w_kw > 0: formula_parts.append(f"{w_kw} × 키워드")
if w_content > 0: formula_parts.append(f"{w_content} × 내용")
if w_struct > 0: formula_parts.append(f"{w_struct} × 구조")
lines.append(f"**공식**: `점수 = {' + '.join(formula_parts)}`")
lines.append("")
lines.append(f"**결과: {hits}/4 정답**")
lines.append("")
# 1. 수행 프로세스 (Phase 17 스타일 — 가로 Step 표 + 예시)
lines.append("## 1. 수행 프로세스")
lines.append("")
one_liner = {
18: "MDX에서 형태소 추출 → Figma 키워드 DB와 겹침만으로 매칭 (baseline).",
19: "MDX에 동의어 치환(synonym) 전처리 추가 → Figma 키워드 DB와 겹침으로 매칭.",
20: "키워드 매칭 + 제목 내용 의미 유사도(Cross-encoder) 합산.",
21: "키워드 + 내용 + 구조(표 헤더/섹션 감지)까지 3축 합산.",
}
lines.append(f"**{one_liner.get(phase_num, '')}**")
lines.append("")
# 헤더: Step1~5
step_titles = ["Step 1. Figma 정리", "Step 2. 단어 가중치 정리",
"Step 3. MDX 키워드 추출", "Step 4. 유사도 계산", "Step 5. 최종 선정"]
lines.append("| " + " | ".join(step_titles) + " |")
lines.append("|" + "|".join(["-------"] * 5) + "|")
# 내용 행 — phase별 축 반영
s3 = ("원문 →
**synonym 치환** →
Kiwi 형태소 추출 →
Figma vocab 교집합"
if phase_num >= 19 else
"원문 →
Kiwi 형태소 추출 →
Figma vocab 교집합")
formula_parts = []
if w_kw > 0: formula_parts.append(f"{w_kw} × 키워드")
if w_content > 0: formula_parts.append(f"{w_content} × 내용")
if w_struct > 0: formula_parts.append(f"{w_struct} × 구조")
s4 = f"공식:
`점수 = {' + '.join(formula_parts)}`
"
s4 += "① 키워드: IDF 가중 Jaccard"
if w_content > 0: s4 += "
② 내용: ko-reranker
(Cross-encoder 로컬)"
if w_struct > 0: s4 += "
③ 구조: MDX 표헤더/섹션 감지 → Figma layout family 매칭"
lines.append(
"| 각 Figma 프레임에 analysis.md 작성
(내용 1문장 + 후보 키워드 15~25개)"
" | 32개 프레임에서 키워드 등장 빈도(df) 기반 3단계 가중치
핵심×1.0, 중간×0.5, 일반×0.1"
f" | {s3}"
f" | {s4}"
" | 32개 프레임 점수 계산 → 1위 선정"
" |"
)
# MDX 유닛별 실제 추출 키워드 표시 (Phase 18/19 비교용)
if phase_num in (18, 19):
pass # 예시 행 이후에 추가 섹션 삽입
syn_example = ("'필수 요건'→'필수조건'
'디지털 전환'→'DX'
"
if phase_num >= 19 else "")
ex_kw = ("0.289" if phase_num == 18 else
"0.289" if phase_num == 19 else
"0.289" if phase_num == 20 else
"0.289")
ex_content = "0.731" if w_content > 0 else "-"
ex_struct = "1.00" if w_struct > 0 else "-"
ex_final_parts = [f"{w_kw}×0.289"]
if w_content > 0: ex_final_parts.append(f"{w_content}×0.731")
if w_struct > 0: ex_final_parts.append(f"{w_struct}×1.00")
# 실제 계산
ex_final_val = w_kw * 0.289 + w_content * 0.731 + w_struct * 1.00
ex_final = " + ".join(ex_final_parts) + f" = **{ex_final_val:.3f}**"
lines.append(
"| Frame 13 (필수조건)
"
f"
"
"내용: \"DX 시행 3대 필수조건\"
키워드: 필수조건, 기술, 디지털기술, 사람, 역량, 자연, 여건…"
" | '필수조건' 1/32→핵심×1.0
'여건' 1/32→핵심×1.0
'디지털' 5/32→중간×0.5"
f" | {syn_example}MDX03-1 본문 →
Kiwi 127개 →
Figma vocab ∩ 22개:
DX, 필수, 요건, 기술, 사람, 여건…"
f" | MDX03-1 ↔ Frame 13
키워드: {ex_kw}
"
+ (f"내용: {ex_content}
" if w_content > 0 else "")
+ (f"구조: {ex_struct}
" if w_struct > 0 else "")
+ f"최종: {ex_final}"
" | MDX03-1 결과
1위 Frame 13
2위 Frame 15
3위 Frame 18"
" |"
)
lines.append("")
# 1-B. Figma 기준 키워드 pool (Phase 18/19에만)
if phase_num in (18, 19):
# vocab + tier 정보 필요
from phase_common import compute_df_idf_tier
df, idf, tier, N = compute_df_idf_tier(frames)
vocab_all = sorted(set().union(*[set(f["keywords"]) for f in frames.values()]))
core = sorted([k for k in vocab_all if tier.get(k) == "core"], key=lambda k: -df.get(k, 0))
mid = sorted([k for k in vocab_all if tier.get(k) == "mid"], key=lambda k: -df.get(k, 0))
gen = sorted([k for k in vocab_all if tier.get(k) == "general"], key=lambda k: -df.get(k, 0))
lines.append("## 1-B. Figma 기준 추출 키워드 (매칭 pool)")
lines.append("")
lines.append(f"**32개 프레임 analysis.md의 후보 키워드 합집합 = {len(vocab_all)}개** "
f"(이 pool이 매칭의 기준이며 **Phase 18/19 동일**)")
lines.append("")
lines.append(f"- 핵심 (1~3 프레임만 등장, `×1.0`): **{len(core)}개**")
lines.append(f"- 중간 (4~9 프레임, `×0.5`): **{len(mid)}개**")
lines.append(f"- 일반 (10+ 프레임, `×0.1`): **{len(gen)}개**")
lines.append("")
if phase_num == 19:
lines.append("> **Phase 19 차이점**: 이 pool은 동일하지만, MDX 본문을 Kiwi 처리 전에 "
"`synonyms.yaml` (8개)로 치환. 예: MDX의 `\"필수 요건\"` → `\"필수조건\"` "
"→ pool에 있는 canonical 표기로 정규화 → 교집합 성공.")
lines.append("")
lines.append("**synonyms.yaml (Phase 19 적용되는 치환 규칙):**")
lines.append("")
lines.append("| Canonical (pool에 이미 존재) | MDX에서 치환 대상 Variants |")
lines.append("|------|-----------|")
for canonical, variants in synonyms.items():
vs = ", ".join(f"`{v}`" for v in variants)
lines.append(f"| `{canonical}` | {vs} |")
lines.append("")
# 핵심 키워드 예시 (빈도 상위 30)
lines.append("### 핵심 키워드 예시 (distinctive, 1~3 프레임만 등장) — 상위 30개")
lines.append("")
lines.append(", ".join(f"`{k}`" for k in core[:30]))
lines.append(f"
... (전체 {len(core)}개 중 30개)")
lines.append("")
# 중간 키워드 전체
if mid:
lines.append(f"### 중간 키워드 전체 ({len(mid)}개, 4~9 프레임)")
lines.append("")
lines.append(", ".join(f"`{k}` (df={df.get(k, 0)})" for k in mid))
lines.append("")
# 일반 키워드
if gen:
lines.append(f"### 일반 키워드 ({len(gen)}개, 10+ 프레임)")
lines.append("")
lines.append(", ".join(f"`{k}` (df={df.get(k, 0)})" for k in gen))
lines.append("")
else:
lines.append("### 일반 키워드")
lines.append("")
lines.append("(없음 — corpus 규모가 작아 10+ 프레임 등장 키워드 없음)")
lines.append("")
# 2. 테스트 결과 (매트릭스)
lines.append("## 2. 테스트 결과")
lines.append("")
lines.append("| 콘텐츠 | 1순위 | 2순위 | 3순위 |")
lines.append("|--------|-------|-------|-------|")
for r in reports:
row = [f"**{r['display']}**
정답 Frame **{r['correct_sid']}**"]
for rank_idx in range(3):
fid, score, bd = r["top3"][rank_idx]
sid = frame_to_short.get(fid, "?")
info = idx_data.get(sid, {})
png = info.get("png", "")
title = info.get("title_text", "").strip().replace("\n", " ") or ""
if len(title) > 15: title = title[:15] + "…"
inner = f"
**{sid}** ({score:.3f})
{title}"
if sid == r["correct_sid"]:
cell = f"