Spaces:
Sleeping
Sleeping
| """ν€μλ λΆμ Excel λ΄λ³΄λ΄κΈ° β Supabase μ§μ 쿼리 + Athena Full context.""" | |
| import streamlit as st | |
| def render_export( | |
| data: dict, | |
| keyword: str | None = None, | |
| sentiment: str | None = None, | |
| brand_only: bool = False, | |
| no_brand: bool = False, | |
| platform: str | None = None, | |
| llm_verified: str | None = None, | |
| llm_is_negative: bool | None = None, | |
| ): | |
| """ν€μλ λΆμ Excel λ€μ΄λ‘λ (μ§μ Supabase, Vercel μ°ν). | |
| Args: | |
| llm_is_negative: True=μ ν, False=μ€ν, None=μ 체 | |
| """ | |
| # Build filter description for UI | |
| filter_parts = [] | |
| if keyword: | |
| filter_parts.append(f"ν€μλ: {keyword}") | |
| else: | |
| filter_parts.append("ν€μλ: μ 체") | |
| if sentiment: | |
| label = {"positive": "κΈμ ", "neutral": "μ€λ¦½", "negative": "λΆμ "}.get(sentiment, sentiment) | |
| filter_parts.append(f"κ°μ±: {label}") | |
| if brand_only: | |
| filter_parts.append("λΈλλ λ©μ λ§") | |
| elif no_brand: | |
| filter_parts.append("λΉλΈλλλ§") | |
| if platform: | |
| filter_parts.append(f"νλ«νΌ: {platform}") | |
| if llm_is_negative is True: | |
| filter_parts.append("2μ°¨κ²μ¦: μ ν") | |
| elif llm_is_negative is False: | |
| filter_parts.append("2μ°¨κ²μ¦: μ€ν") | |
| elif llm_verified: | |
| label = {"verified": "κ²μ¦μλ£", "unverified": "λ―Έκ²μ¦"}.get(llm_verified, llm_verified) | |
| filter_parts.append(f"2μ°¨κ²μ¦: {label}") | |
| filter_desc = " | ".join(filter_parts) if filter_parts else "μ 체" | |
| st.caption(f"π₯ λ€μ΄λ‘λ νν°: {filter_desc}") | |
| if st.button("π₯ Excel λ€μ΄λ‘λ", key="sentiment:kw_export_btn"): | |
| try: | |
| xlsx = _export_keyword_direct( | |
| data["campaign_id"], | |
| keyword=keyword, | |
| sentiment=sentiment, | |
| brand_only=brand_only, | |
| no_brand=no_brand, | |
| platform=platform, | |
| llm_verified=llm_verified, | |
| llm_is_negative=llm_is_negative, | |
| ) | |
| st.session_state["sentiment:kw_excel_data"] = xlsx | |
| st.session_state["sentiment:kw_excel_ready"] = True | |
| except Exception as e: | |
| st.error(f"λ€μ΄λ‘λ μ€ν¨: {e}") | |
| if st.session_state.get("sentiment:kw_excel_ready"): | |
| st.download_button( | |
| label="πΎ νμΌ μ μ₯", | |
| data=st.session_state["sentiment:kw_excel_data"], | |
| file_name=f"keyword_analysis_{data['campaign_id']}.xlsx", | |
| mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", | |
| key="sentiment:kw_dl_btn", | |
| ) | |
| def _export_keyword_direct( | |
| campaign_id: int, | |
| keyword: str | None = None, | |
| sentiment: str | None = None, | |
| brand_only: bool = False, | |
| no_brand: bool = False, | |
| platform: str | None = None, | |
| llm_verified: str | None = None, | |
| llm_is_negative: bool | None = None, | |
| ) -> bytes: | |
| """μ§μ Supabase cursor νμ΄μ§λ€μ΄μ μΌλ‘ νν°λ§λ ν€μλ λ°μ΄ν° μΆμΆ. | |
| Vercel 10μ΄ νμμμ μ°ν. νν° μ μ©μΌλ‘ λμ ν μ κ°μ. | |
| Full contextλ Athenaμμ λ°°μΉ fetchνμ¬ λ³ν©. | |
| Args: | |
| llm_is_negative: True=μ ν(λΆμ νμ ), False=μ€ν(λΆμ μλ), None=μ 체 | |
| """ | |
| from io import BytesIO | |
| from core.supabase_client import get_supabase_client | |
| from core.athena_client import fetch_full_answers_batch | |
| sb = get_supabase_client() | |
| batch_size = 1000 | |
| export_warn_threshold = 100_000 | |
| warned = False | |
| last_id = 0 | |
| all_rows: list[dict] = [] | |
| # Cursor-based pagination (id > last_id) β O(1) per batch | |
| progress = st.progress(0, text="λ°μ΄ν° λ‘λ© μ€...") | |
| batch_num = 0 | |
| while True: | |
| query = ( | |
| sb.table("keyword_sentiment_results") | |
| .select( | |
| "id, answer_id, keyword, matched_sentence, keyword_sentiment, keyword_confidence, " | |
| "brand_name, brand_sentiment, brand_confidence, brand_mentions, " | |
| "llm_verified, llm_sentiment, llm_is_negative, llm_confidence, llm_reason_tags, " | |
| "llm_reason_summary, llm_reasoning, " | |
| "competitor_brand_name, competitor_llm_verified, competitor_llm_sentiment, " | |
| "competitor_llm_reason_tags, competitor_llm_reason_summary, " | |
| "citation_urls, citation_count, platform, question_content, created_at" | |
| ) | |
| .eq("campaign_id", campaign_id) | |
| .gt("id", last_id) | |
| ) | |
| # Apply filters | |
| if keyword: | |
| query = query.eq("keyword", keyword) | |
| if sentiment: | |
| query = query.eq("keyword_sentiment", sentiment) | |
| if brand_only: | |
| query = query.not_.is_("brand_name", "null") | |
| elif no_brand: | |
| query = query.is_("brand_name", "null") | |
| if platform: | |
| query = query.eq("platform", platform) | |
| if llm_is_negative is not None: | |
| query = query.eq("llm_is_negative", llm_is_negative) | |
| elif llm_verified == "verified": | |
| query = query.eq("llm_verified", True) | |
| elif llm_verified == "unverified": | |
| query = query.or_("llm_verified.is.null,llm_verified.eq.false") | |
| result = query.order("id").limit(batch_size).execute() | |
| rows = result.data or [] | |
| if not rows: | |
| break | |
| all_rows.extend(rows) | |
| last_id = rows[-1]["id"] | |
| batch_num += 1 | |
| pct = min(len(all_rows) / max(len(all_rows) + batch_size, 1), 0.99) | |
| progress.progress(pct, text=f"λ‘λ© μ€... {len(all_rows):,}건") | |
| if len(rows) < batch_size: | |
| break | |
| if not warned and len(all_rows) >= export_warn_threshold: | |
| st.warning(f"λμ©λ Export ({len(all_rows):,}건+). μλ£κΉμ§ μκ°μ΄ 걸릴 μ μμ΅λλ€.") | |
| warned = True | |
| progress.progress(1.0, text=f"{len(all_rows):,}건 λ‘λ μλ£. Full context κ°μ Έμ€λ μ€...") | |
| # Batch fetch full answers from Athena | |
| unique_ids = list({row["answer_id"] for row in all_rows if row.get("answer_id")}) | |
| full_answers: dict[int, str] = {} | |
| athena_batch_size = 500 | |
| for i in range(0, len(unique_ids), athena_batch_size): | |
| chunk = unique_ids[i:i + athena_batch_size] | |
| full_answers.update(fetch_full_answers_batch(chunk)) | |
| pct = min((i + athena_batch_size) / max(len(unique_ids), 1), 0.99) | |
| progress.progress(pct, text=f"Full context λ‘λ©... {min(i + athena_batch_size, len(unique_ids)):,}/{len(unique_ids):,} λ΅λ³") | |
| progress.progress(1.0, text=f"Full context {len(full_answers):,}건 λ‘λ μλ£!") | |
| # Build Excel | |
| from openpyxl import Workbook | |
| from openpyxl.utils import get_column_letter | |
| wb = Workbook() | |
| ws = wb.active | |
| ws.title = "Keyword Sentiment" | |
| headers = [ | |
| "ν€μλ", "λ§€μΉ λ¬Έμ₯", "Full Context", "ν€μλ κ°μ±", "ν€μλ νμ λ", | |
| "λΈλλλͺ ", "λΈλλ κ°μ±", "λΈλλ νμ λ", "λΈλλ μΈκΈ", | |
| "LLM κ²μ¦", "LLM κ°μ±", "LLM νμ λ", "LLM νκ·Έ", "LLM μμ½", "LLM κ·Όκ±°", | |
| "κ²½μμ¬ λΈλλ", "κ²½μμ¬ LLM κ²μ¦", "κ²½μμ¬ LLM κ°μ±", | |
| "κ²½μμ¬ LLM νκ·Έ", "κ²½μμ¬ LLM μμ½", | |
| "μΈμ© URL", "μΈμ© μ", "νλ«νΌ", "μ§λ¬Έ", "Answer ID", "μμ±μΌ", | |
| ] | |
| ws.append(headers) | |
| progress.progress(0.0, text="Excel μμ± μ€...") | |
| for i, row in enumerate(all_rows): | |
| # Brand mentions | |
| brand_mentions_str = "" | |
| bm = row.get("brand_mentions") | |
| if bm and isinstance(bm, dict): | |
| parts = [] | |
| if bm.get("in_house"): | |
| parts.append(f"μμ¬: {', '.join(bm['in_house'])}") | |
| if bm.get("competitor"): | |
| parts.append(f"κ²½μμ¬: {', '.join(bm['competitor'])}") | |
| brand_mentions_str = "; ".join(parts) | |
| # Tags (list -> comma-separated) | |
| def _tags_str(tags): | |
| if tags and isinstance(tags, list): | |
| return ", ".join(str(t) for t in tags) | |
| return "" | |
| # Citations | |
| cite_str = "" | |
| cites = row.get("citation_urls") | |
| if cites and isinstance(cites, list): | |
| cite_str = "\n".join(str(u) for u in cites[:10]) | |
| # Full context from Athena | |
| answer_id = row.get("answer_id") | |
| full_text = full_answers.get(answer_id, "") if answer_id else "" | |
| ws.append([ | |
| row.get("keyword", ""), | |
| row.get("matched_sentence", ""), | |
| full_text, | |
| row.get("keyword_sentiment", ""), | |
| row.get("keyword_confidence"), | |
| row.get("brand_name", ""), | |
| row.get("brand_sentiment", ""), | |
| row.get("brand_confidence"), | |
| brand_mentions_str, | |
| "Y" if row.get("llm_verified") else "N", | |
| row.get("llm_sentiment", ""), | |
| row.get("llm_confidence"), | |
| _tags_str(row.get("llm_reason_tags")), | |
| row.get("llm_reason_summary", ""), | |
| (row.get("llm_reasoning") or "")[:500], | |
| row.get("competitor_brand_name", ""), | |
| "Y" if row.get("competitor_llm_verified") else "N", | |
| row.get("competitor_llm_sentiment", ""), | |
| _tags_str(row.get("competitor_llm_reason_tags")), | |
| row.get("competitor_llm_reason_summary", ""), | |
| cite_str, | |
| row.get("citation_count"), | |
| row.get("platform", ""), | |
| row.get("question_content", ""), | |
| answer_id, | |
| (row.get("created_at") or "")[:19].replace("T", " "), | |
| ]) | |
| if i % 10000 == 0: | |
| progress.progress(min(i / max(len(all_rows), 1), 0.99), text=f"Excel μμ± μ€... {i:,}/{len(all_rows):,}") | |
| # Column widths | |
| widths = [12, 50, 80, 10, 8, 12, 10, 8, 25, 6, 10, 8, 25, 30, 40, 12, 6, 10, 25, 30, 40, 6, 10, 40, 10, 16] | |
| for i, w in enumerate(widths, 1): | |
| if i <= len(widths): | |
| ws.column_dimensions[get_column_letter(i)].width = w | |
| progress.progress(1.0, text="νμΌ μμ± μλ£!") | |
| buf = BytesIO() | |
| wb.save(buf) | |
| return buf.getvalue() | |