"""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"![13]({png_rel}13.png)
" "내용: \"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}]({png_rel}{png})
**{sid}** ({score:.3f})
{title}" if sid == r["correct_sid"]: cell = f"
🎯 정답
{inner}
" else: cell = inner row.append(cell) lines.append("| " + " | ".join(row) + " |") lines.append("") # 3. 테스트 결과 세부 lines.append("## 3. 테스트 결과 세부") lines.append("") for r in reports: lines.append(f"### {r['display']} — 정답 Frame **{r['correct_sid']}**") lines.append("") lines.append("**MDX 추출 사항**") lines.append("") lines.append(f"- **내용**: {r['mdx_content']}") lines.append(f"- **키워드** ({len(r['mdx_kws'])}개): {', '.join(sorted(r['mdx_kws']))}") if r.get("mdx_layout"): lines.append(f"- **구조**: {r['mdx_layout']}") lines.append("") for rank, (fid, score, bd) in enumerate(r["top3"], 1): sid = frame_to_short.get(fid, "?") info = idx_data.get(sid, {}) png = info.get("png", "") fig_content = frames[fid].get("content", "") fig_layout = frames[fid].get("layout", "") common = ", ".join(bd["inter"]) if bd["inter"] else "(없음)" is_correct = sid == r["correct_sid"] if is_correct: lines.append(f"**🎯 {rank}위 Frame {sid} (정답)**") else: lines.append(f"**{rank}위 Frame {sid}**") lines.append("") lines.append(f"![{sid}]({png_rel}{png})") lines.append("") lines.append(f"- **매칭 내용**: {fig_content}") lines.append(f"- **매칭 키워드**: {common}") # 구조 축이 있을 때 Figma 구조 + 매칭 사유 표시 if w_struct > 0: mdx_layout = r.get("mdx_layout") or "" sc = bd["struct"] if sc >= 1.0: verdict = "정확 일치 (동일 구조)" elif sc >= 0.75: verdict = "매우 호환 (2/3열 동일, 표현만 다름)" elif sc >= 0.5: verdict = "부분 호환 (일부 축 공유)" elif sc >= 0.3: verdict = "약한 호환 (컬럼 수/의도 다름)" else: verdict = "거의 무관" lines.append(f"- **Figma 구조**: `{fig_layout}`") lines.append(f"- **구조 매칭 사유**: `{mdx_layout}` ↔ `{fig_layout}` → **{verdict}** → 점수 {sc:.2f}") # 축별 점수 (해당 축만) score_parts = [f"키워드 {bd['kw']:.3f}"] if w_content > 0: score_parts.append(f"내용 {bd['content']:.3f}") if w_struct > 0: score_parts.append(f"구조 {bd['struct']:.2f}") lines.append(f"- **축별 점수**: {' / '.join(score_parts)}") lines.append(f"- **매칭 점수 (최종)**: **{score:.3f}**") lines.append("") return "\n".join(lines) def main(): synonyms = load_synonyms() frames = load_32_frames() df, idf, tier, N = compute_df_idf_tier(frames) vocab = set() for v in frames.values(): vocab.update(v["keywords"]) units_full, units_title = load_target_units() idx_data, frame_to_short = load_frame_index() print(f"Figma: {N}개, Vocab: {len(vocab)}개") print(f"Tier: 핵심 {sum(1 for t in tier.values() if t=='core')}개, " f"중간 {sum(1 for t in tier.values() if t=='mid')}개, " f"일반 {sum(1 for t in tier.values() if t=='general')}개") print() configs = [ (18, "키워드만 (baseline)", False, False, False), (19, "synonym + 키워드", True, False, False), (20, "synonym + 키워드 + 내용", True, True, False), (21, "synonym + 키워드 + 내용 + 구조", True, True, True), ] results_all = {} for phase_num, desc, use_syn, use_cont, use_str in configs: print(f"=== Phase {phase_num}: {desc} ===") reports, hits, weights = match_phase( phase_num, use_syn, use_cont, use_str, units_full, units_title, frames, df, idf, tier, vocab, synonyms, idx_data, frame_to_short, ) results_all[phase_num] = {"reports": reports, "hits": hits, "weights": weights, "desc": desc} for r in reports: top_sid = frame_to_short.get(r["top3"][0][0], "?") mark = "✓" if top_sid == r["correct_sid"] else "✗" print(f" {r['uid']}(정답={r['correct_sid']}) → Top1={top_sid} ({r['top3'][0][1]:.3f}) {mark}") print(f" ⇒ {hits}/4") print() md = write_md(phase_num, desc, reports, hits, weights, frames, idx_data, frame_to_short) out = Path(__file__).parent / f"MATRIX_PHASE{phase_num}.md" out.write_text(md, encoding="utf-8") # results_all 저장 (phase22에서 재사용) import pickle pkl = Path(__file__).parent / "_phase18_21_results.pkl" # pickle엔 reports만 직렬화하려면 lambda 등 빼야 — simplify # just store minimal for phase22 serializable = {} for pn, d in results_all.items(): serializable[pn] = { "desc": d["desc"], "hits": d["hits"], "weights": d["weights"], "reports": [{ "uid": r["uid"], "display": r["display"], "correct_sid": r["correct_sid"], "mdx_kws_count": len(r["mdx_kws"]), "top3": [(fid, score, frame_to_short.get(fid, "?"), bd) for fid, score, bd in r["top3"]], } for r in d["reports"]] } with open(pkl, "wb") as f: pickle.dump(serializable, f) print(f"\n저장: {pkl}") print("\n각 Phase MD 생성 완료.") if __name__ == "__main__": main()