Spaces:
Sleeping
Sleeping
| """์ฝํ ์ธ ์ก์ ๊ฐ์ด๋ (R-5). | |
| GapScore OPPORTUNITY ํ ํฝ ๊ธฐ๋ฐ ์ฝํ ์ธ ์ ์ ์ ์. | |
| Cross-Model ๋ถ์ ๋ฐ์ดํฐ๋ฅผ ํ์ฉํ์ฌ ๊ตฌ์ฒด์ ์ก์ ์์ดํ ์์ฑ. | |
| """ | |
| import streamlit as st | |
| import pandas as pd | |
| from core.supabase_client import get_gap_scores, get_topic_clusters | |
| # Content strategy templates per quadrant | |
| STRATEGY_TEMPLATES = { | |
| "OPPORTUNITY": { | |
| "priority": "๐ด ๋์", | |
| "action": "์ฝํ ์ธ ์ ์", | |
| "detail": ( | |
| "์ฌ์ฉ์๊ฐ ์์ฃผ ๊ฒ์ํ์ง๋ง AI๊ฐ ์ธ์ฉํ ๋งํ ์ฝํ ์ธ ๊ฐ ๋ถ์กฑํฉ๋๋ค. " | |
| "์ด ํ ํฝ์ ๋ํ ์ ๋ฌธ ์ฝํ ์ธ ๋ฅผ ์ ์ํ๋ฉด AI ๋ต๋ณ์ ์ธ์ฉ๋ ํ๋ฅ ์ด ๋์ต๋๋ค." | |
| ), | |
| "tactics": [ | |
| "FAQ ํ์ด์ง์ ์ด ํ ํฝ ๊ด๋ จ ์ง๋ฌธ-๋ต๋ณ ์ถ๊ฐ", | |
| "ํต๊ณ/๋ฐ์ดํฐ ํฌํจ โ AI ์ธ์ฉ ํ๋ฅ +41% (GEO ์ฐ๊ตฌ)", | |
| "30-50๋จ์ด ์๊ธฐ ์๊ฒฐํ ๋ต๋ณ ๋ฌธ๋จ ํฌํจ (Answer Capsule)", | |
| "Schema Markup ์ถ๊ฐ โ AI ์ธ์ฉ ํ๋ฅ 2.5๋ฐฐ ์ฆ๊ฐ", | |
| ], | |
| }, | |
| "SATURATED": { | |
| "priority": "๐ก ์ค๊ฐ", | |
| "action": "์ฐจ๋ณํ ๊ฐํ", | |
| "detail": ( | |
| "์์์ ๊ณต๊ธ ๋ชจ๋ ๋์ ๊ฒฝ์ ํ ํฝ์ ๋๋ค. " | |
| "๊ธฐ์กด ์ฝํ ์ธ ์ ์ฐจ๋ณํ๋ ์ ๋ฌธ์ฑ์ด๋ ๊ณ ์ ๋ฐ์ดํฐ๊ฐ ํ์ํฉ๋๋ค." | |
| ), | |
| "tactics": [ | |
| "์์ฒด ์ฐ๊ตฌ ๋ฐ์ดํฐ/์ผ์ด์ค ์คํฐ๋ ์ถ๊ฐ", | |
| "๊ธฐ์กด ์ธ์ฉ ์์ค ๋ถ์ โ ๋น ์ง ๊ฐ๋(angle) ๋ฐ๊ตด", | |
| "E-E-A-T ์ ํธ ๊ฐํ (์ ์ ์ ๋ฌธ์ฑ, ์ธ์ฉ ์ถ์ฒ ๋ช ์)", | |
| "๋น๊ตํ/๋ฐ์ดํฐ ์๊ฐํ๋ก ์ ๋ณด ๋ฐ๋ ๋์ด๊ธฐ", | |
| ], | |
| }, | |
| "LATENT_AUTHORITY": { | |
| "priority": "๐ข ๋ฎ์", | |
| "action": "์ ์ง + ๋ชจ๋ํฐ๋ง", | |
| "detail": ( | |
| "์ด๋ฏธ AI์ ์ธ์ฉ๋๊ณ ์์ง๋ง ๊ฒ์ ์์๊ฐ ๋ฎ์ต๋๋ค. " | |
| "๊ธฐ์กด ์ฝํ ์ธ ๋ฅผ ์ ์งํ๋ฉฐ ์์ ๋ณํ๋ฅผ ๋ชจ๋ํฐ๋งํ์ธ์." | |
| ), | |
| "tactics": [ | |
| "๊ธฐ์กด ์ธ์ฉ ์ฝํ ์ธ ์ ์ต์ ์ ๋ฐ์ดํธ ์ ์ง", | |
| "Demand ์ฆ๊ฐ ์ถ์ธ ๊ฐ์ง ์ ์ฝํ ์ธ ํ์ฅ", | |
| "์ธ์ฉ๋๋ ๊ตฌ์ฒด์ ๋ฌธ์ฅ/๊ตฌ์ ํ์ โ ๊ฐํ", | |
| ], | |
| }, | |
| "NICHE": { | |
| "priority": "โช ๊ด๋ง", | |
| "action": "์ ํ์ ์คํ", | |
| "detail": ( | |
| "์์์ ๊ณต๊ธ ๋ชจ๋ ๋ฎ์ ํ์ ์์ญ์ ๋๋ค. " | |
| "์์ฅ์ด ์ฑ์ฅํ๋ฉด ์ ์ ํจ๊ณผ๋ฅผ ๋ณผ ์ ์์ต๋๋ค." | |
| ), | |
| "tactics": [ | |
| "๋ฎ์ ๋น์ฉ์ผ๋ก ๊ธฐ๋ณธ ์ฝํ ์ธ ๋ง๋ จ (์ ์ )", | |
| "๊ด๋ จ ํค์๋ ํธ๋ ๋ ๋ชจ๋ํฐ๋ง", | |
| ], | |
| }, | |
| } | |
| def render_content_actions(base_ctx: dict, pair: dict): | |
| """Render content action guide based on GapScore analysis.""" | |
| campaign_chatgpt = pair["campaign_chatgpt"] | |
| campaign_gemini = pair["campaign_gemini"] | |
| st.caption( | |
| "Demand-Supply Gap ๋ถ์ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก **๊ตฌ์ฒด์ ์ฝํ ์ธ ์ ๋ต**์ ์ ์ํฉ๋๋ค." | |
| ) | |
| matches = get_gap_scores(campaign_chatgpt, campaign_gemini) | |
| if not matches: | |
| st.warning("GapScore ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.") | |
| return | |
| # Fetch cluster details for sample_fanouts and top_sources | |
| chatgpt_clusters = get_topic_clusters(campaign_chatgpt, source="chatgpt") | |
| gemini_clusters = get_topic_clusters(campaign_gemini, source="gemini") | |
| chatgpt_map = {c["id"]: c for c in chatgpt_clusters} | |
| gemini_map = {c["id"]: c for c in gemini_clusters} | |
| # --- Overview: Quadrant distribution --- | |
| quadrant_counts = {} | |
| for m in matches: | |
| q = m.get("quadrant", "NICHE") | |
| quadrant_counts[q] = quadrant_counts.get(q, 0) + 1 | |
| opp_count = quadrant_counts.get("OPPORTUNITY", 0) | |
| sat_count = quadrant_counts.get("SATURATED", 0) | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.metric("์ฝํ ์ธ ์ ์ ํ์", f"{opp_count}๊ฐ ํ ํฝ", | |
| help="OPPORTUNITY: Demand ๋์ + Supply ๋ฎ์") | |
| with col2: | |
| st.metric("์ฐจ๋ณํ ํ์", f"{sat_count}๊ฐ ํ ํฝ", | |
| help="SATURATED: Demand ๋์ + Supply ๋์") | |
| with col3: | |
| st.metric("์ด ๋ถ์ ํ ํฝ", f"{len(matches)}๊ฐ") | |
| st.markdown("---") | |
| # --- Priority Action List --- | |
| st.markdown("### ์ฐ์ ์ก์ ๋ฆฌ์คํธ") | |
| st.caption("GapScore ์์ผ๋ก ์ ๋ ฌ. OPPORTUNITY ํ ํฝ์ด ์ต์ฐ์ ์ ๋๋ค.") | |
| # Summary table | |
| rows = [] | |
| for i, m in enumerate(matches, 1): | |
| q = m.get("quadrant", "NICHE") | |
| strategy = STRATEGY_TEMPLATES.get(q, STRATEGY_TEMPLATES["NICHE"]) | |
| rows.append({ | |
| "์์": i, | |
| "ํ ํฝ (Demand)": (m.get("chatgpt_label") or "")[:35], | |
| "ํ ํฝ (Supply)": (m.get("gemini_label") or "")[:35], | |
| "GapScore": f"{float(m.get('gap_score', 0)):.4f}", | |
| "Quadrant": q.replace("_", " ").title(), | |
| "์ก์ ": strategy["action"], | |
| "์ฐ์ ์์": strategy["priority"], | |
| }) | |
| df = pd.DataFrame(rows) | |
| st.dataframe(df, use_container_width=True, hide_index=True) | |
| st.markdown("---") | |
| # --- Detailed Action Cards for Top OPPORTUNITY topics --- | |
| opportunity_matches = [m for m in matches if m.get("quadrant") == "OPPORTUNITY"] | |
| if opportunity_matches: | |
| st.markdown("### OPPORTUNITY ํ ํฝ ์์ธ ๊ฐ์ด๋") | |
| st.caption( | |
| "์ฝํ ์ธ ์ ์ ROI๊ฐ ๊ฐ์ฅ ๋์ ํ ํฝ์ ๋๋ค. " | |
| "์ฌ์ฉ์๊ฐ ์์ฃผ ๋ฌป์ง๋ง AI๊ฐ ์ธ์ฉํ ์ฝํ ์ธ ๊ฐ ๋ถ์กฑํฉ๋๋ค." | |
| ) | |
| for i, m in enumerate(opportunity_matches[:10], 1): | |
| chatgpt_label = m.get("chatgpt_label", "Unknown") | |
| gap = float(m.get("gap_score") or 0) | |
| demand = float(m.get("demand_percentile") or 0) | |
| supply = float(m.get("supply_percentile") or 0) | |
| chatgpt_detail = chatgpt_map.get(m["chatgpt_cluster_id"], {}) | |
| gemini_detail = gemini_map.get(m["gemini_cluster_id"], {}) | |
| with st.expander(f"#{i} {chatgpt_label} (GapScore: {gap:.4f})", key=f"research:opp_action:{m['id']}"): | |
| left, right = st.columns(2) | |
| with left: | |
| st.markdown("**Gap ๋ถ์**") | |
| st.write(f"- Demand (ChatGPT): {demand:.0%}") | |
| st.write(f"- Supply (Gemini): {supply:.0%}") | |
| st.write(f"- Gap: Demand {demand:.0%} vs Supply {supply:.0%}") | |
| # Sample fanouts = what users ask | |
| samples = chatgpt_detail.get("sample_fanouts") or [] | |
| if samples: | |
| st.markdown("**์ฌ์ฉ์๊ฐ ๋ฌป๋ ์ง๋ฌธ๋ค:**") | |
| for s in samples[:5]: | |
| st.write(f" - {s}") | |
| with right: | |
| st.markdown("**์ฝํ ์ธ ์ ๋ต**") | |
| strategy = STRATEGY_TEMPLATES["OPPORTUNITY"] | |
| st.info(strategy["detail"]) | |
| st.markdown("**๊ตฌ์ฒด์ ์คํ ํญ๋ชฉ:**") | |
| for tactic in strategy["tactics"]: | |
| st.write(f"- {tactic}") | |
| # Show Gemini citation examples if available | |
| gemini_samples = gemini_detail.get("sample_fanouts") or [] | |
| if gemini_samples: | |
| st.markdown("**AI๊ฐ ํ์ฌ ์ธ์ฉํ๋ ๋ฌธ๊ตฌ ์์:**") | |
| for s in gemini_samples[:3]: | |
| st.write(f" > {s}") | |
| st.caption("์ด๋ฐ ํํ์ ๋ฌธ์ฅ์ ์ฝํ ์ธ ์ ํฌํจํ์ธ์.") | |
| # Show top sources if available | |
| top_sources = gemini_detail.get("top_sources") | |
| if top_sources and isinstance(top_sources, list): | |
| domains = [s.get("host_url", s.get("url", "")) for s in top_sources[:5]] | |
| if domains: | |
| st.markdown("**ํ์ฌ AI ์ธ์ฉ ์ถ์ฒ:**") | |
| for d in domains: | |
| st.write(f" - {d}") | |
| st.caption("์ด ์ถ์ฒ๋ค์ด ๋ค๋ฃจ์ง ์๋ ๊ฐ๋๋ฅผ ์ฐพ์ผ์ธ์.") | |
| else: | |
| st.success("๋ชจ๋ ํ ํฝ์ ์ถฉ๋ถํ Supply๊ฐ ์์ต๋๋ค. ์ฐจ๋ณํ ์ ๋ต์ ์ง์คํ์ธ์.") | |
| # --- SATURATED topics brief --- | |
| saturated_matches = [m for m in matches if m.get("quadrant") == "SATURATED"] | |
| if saturated_matches: | |
| st.markdown("---") | |
| st.markdown("### SATURATED ํ ํฝ ์์ฝ") | |
| st.caption("์ฐจ๋ณํ๊ฐ ํ์ํ ๊ฒฝ์ ํ ํฝ์ ๋๋ค.") | |
| for i, m in enumerate(saturated_matches[:5], 1): | |
| label = m.get("chatgpt_label", "Unknown") | |
| gap = float(m.get("gap_score", 0)) | |
| with st.expander(f"#{i} {label} (GapScore: {gap:.4f})", key=f"research:sat_action:{m['id']}"): | |
| strategy = STRATEGY_TEMPLATES["SATURATED"] | |
| st.info(strategy["detail"]) | |
| st.markdown("**์คํ ํญ๋ชฉ:**") | |
| for tactic in strategy["tactics"]: | |
| st.write(f"- {tactic}") | |