"""감성분석 오버뷰 탭. 전체 감성 분석 + 브랜드 멘션 분석 + LLM 2차 검증 결과. """ import streamlit as st from core.charts import create_brand_sentiment_chart from core.supabase_client import get_polarity_stats, get_answers_by_polarity from core.utils import truncate_text def render(data: dict): """오버뷰 탭 렌더링.""" # --- 전체 감성 분석 --- st.markdown("##### 📈 전체 감성 분석") st.caption("AI 답변의 전체 감성 분포를 확인합니다 (긍정/중립/부정)") polarity_in_house_only = st.checkbox( "🏠 자사 브랜드 언급 답변만 보기", value=False, key="sentiment:polarity_in_house_filter", help="체크 시 자사 브랜드가 언급된 답변만 표시합니다.", ) try: polarity_stats = get_polarity_stats(data["campaign_id"], in_house_only=polarity_in_house_only) positive_count = polarity_stats.get("positive", 0) neutral_count = polarity_stats.get("neutral", 0) negative_count = polarity_stats.get("negative", 0) total_answers_pol = positive_count + neutral_count + negative_count pol_col1, pol_col2, pol_col3, pol_col4 = st.columns(4) with pol_col1: st.metric("전체 분석", f"{total_answers_pol:,}건") with pol_col2: pos_rate = (positive_count / total_answers_pol * 100) if total_answers_pol > 0 else 0 st.metric("😊 긍정", f"{positive_count:,}건", f"{pos_rate:.1f}%") with pol_col3: neu_rate = (neutral_count / total_answers_pol * 100) if total_answers_pol > 0 else 0 st.metric("😐 중립", f"{neutral_count:,}건", f"{neu_rate:.1f}%") with pol_col4: neg_rate = (negative_count / total_answers_pol * 100) if total_answers_pol > 0 else 0 st.metric("😞 부정", f"{negative_count:,}건", f"{neg_rate:.1f}%") st.markdown("---") polarity_filter = st.selectbox( "감성 분류 선택", options=["positive", "neutral", "negative"], format_func=lambda x: {"positive": "😊 긍정", "neutral": "😐 중립", "negative": "😞 부정"}[x], key="sentiment:polarity_filter_tab6", ) polarity_page = st.number_input("페이지", min_value=1, value=1, key="sentiment:polarity_page") polarity_items, polarity_total = get_answers_by_polarity( data["campaign_id"], polarity_filter, page=polarity_page, page_size=20, in_house_only=polarity_in_house_only, ) st.markdown(f"**{polarity_total:,}건** 중 {len(polarity_items)}건 표시") for item in polarity_items: _render_polarity_item(item) except Exception as e: st.error(f"감성 데이터 로드 실패: {e}") st.info("Supabase 연결 설정을 확인하세요") # --- LLM 2차 검증 결과 --- _render_llm_verification_summary(data) # --- 브랜드 분석 --- st.markdown("---") st.markdown("##### 🏷️ 브랜드 멘션 분석") st.caption("자사 브랜드와 경쟁사 브랜드가 AI 답변에서 어떻게 언급되는지 분석합니다") brand_data = data["brand_data"] or {} in_house_summary = brand_data.get("in_house_summary", []) competitor_summary = brand_data.get("competitor_summary", []) total_answers = brand_data.get("total_answers", 0) st.markdown(f"**분석된 AI 답변**: {total_answers}건") st.markdown("---") brand_col1, brand_col2 = st.columns(2) with brand_col1: st.markdown("##### 🏠 자사 브랜드") if in_house_summary: for brand in in_house_summary[:5]: _render_brand_card(brand, "in_house") else: st.info("자사 브랜드 데이터가 없습니다") with brand_col2: st.markdown("##### 🏢 경쟁사 브랜드") if competitor_summary: for brand in competitor_summary[:5]: _render_brand_card(brand, "competitor") else: st.info("경쟁사 브랜드 데이터가 없습니다") if in_house_summary or competitor_summary: st.markdown("---") st.markdown("##### 📊 브랜드별 감성 비교") all_brands = in_house_summary + competitor_summary if all_brands: fig = create_brand_sentiment_chart(all_brands) st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False}) def _render_llm_verification_summary(data: dict): """LLM 2차 검증 결과 요약 렌더링.""" st.markdown("---") st.markdown("##### 🤖 LLM 2차 검증 결과") st.caption( "DeBERTa(1차 AI)가 부정 감지한 답변을 LLM(2차 AI)이 재검증한 결과입니다. " "🔴 정탐 = 실제 부정 확인 (리스크) | 🟢 오탐 = 부정 아님 확인 (안전)" ) llm_stats = data.get("llm_verification_stats") or {} total_nudge = data.get("total_nudge", 0) total_verified = llm_stats.get("total_verified", 0) true_negatives = llm_stats.get("true_negatives", 0) false_positives = llm_stats.get("false_positives", 0) pending = total_nudge - total_verified if total_nudge == 0: st.info("부정 감지된 넛지 후보가 없습니다.") return # --- Metrics --- llm_col1, llm_col2, llm_col3, llm_col4 = st.columns(4) with llm_col1: st.metric("🔍 넛지 후보", f"{total_nudge:,}건") with llm_col2: verify_rate = (total_verified / total_nudge * 100) if total_nudge > 0 else 0 st.metric("✅ 검증 완료", f"{total_verified:,}건", f"{verify_rate:.0f}%") with llm_col3: tp_rate = (true_negatives / total_verified * 100) if total_verified > 0 else 0 st.metric("🎯 정탐", f"{true_negatives:,}건", f"{tp_rate:.1f}%") with llm_col4: fp_rate = (false_positives / total_verified * 100) if total_verified > 0 else 0 st.metric("🚫 오탐", f"{false_positives:,}건", f"{fp_rate:.1f}%") # --- Visual bar --- if total_verified > 0: tp_pct = true_negatives / total_nudge * 100 fp_pct = false_positives / total_nudge * 100 pending_pct = pending / total_nudge * 100 st.markdown(f"""