"""콘텐츠 액션 가이드 (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}")