"""리포트 탭.
Feature별 미리보기, HTML/CSV 다운로드, 전체 리포트 생성.
"""
from datetime import datetime, timedelta
import streamlit as st
from core.api_client import ChainShiftClient
from core.supabase_client import get_campaign_date_range
from .utils import render_feature_section
from . import full_report
# Period presets: (label, days or None for "all")
PERIOD_PRESETS = [
("최근 1일", 1),
("최근 7일", 7),
("최근 30일", 30),
("최근 90일", 90),
("최근 180일", 180),
("전체 기간", None),
("직접 선택", -1),
]
def render(base_ctx: dict):
"""리포트 탭 렌더링."""
st.markdown("##### 리포트")
st.caption("기간별 AI 가시성 분석 리포트를 생성하고, HTML로 다운로드할 수 있습니다.")
# Get campaign date range for presets
campaign_date_range = get_campaign_date_range(base_ctx["campaign_id"])
if campaign_date_range:
first_date_str, last_date_str = campaign_date_range
first_date = datetime.strptime(first_date_str, "%Y-%m-%d")
last_date = datetime.strptime(last_date_str, "%Y-%m-%d")
total_days = (last_date - first_date).days + 1
else:
first_date = datetime.now() - timedelta(days=30)
last_date = datetime.now()
first_date_str = first_date.strftime("%Y-%m-%d")
last_date_str = last_date.strftime("%Y-%m-%d")
total_days = 31
# Period selection
start_date_str, end_date_str = _render_period_selector(
first_date, last_date, first_date_str, last_date_str, total_days
)
# Show selected period info
selected_days = (datetime.strptime(end_date_str, "%Y-%m-%d") - datetime.strptime(start_date_str, "%Y-%m-%d")).days + 1
st.caption(f"📅 선택된 기간: **{start_date_str} ~ {end_date_str}** ({selected_days}일)")
st.markdown("---")
# 4 Sub-tabs for Features + 1 for Full Report
report_tab_summary, report_tab_visibility, report_tab_citation, report_tab_full = st.tabs([
"📊 Executive Summary",
"👁️ Visibility & Content",
"🔗 Citation Analysis",
"📄 전체 리포트",
])
client = ChainShiftClient(api_key=base_ctx.get("api_key"), access_token=base_ctx.get("access_token"))
# Tab 1: Executive Summary
with report_tab_summary:
render_feature_section(
client=client,
campaign_id=base_ctx["campaign_id"],
feature_key="overview",
title="가시성 개요",
description="AI 플랫폼별 브랜드 노출 현황과 핵심 지표",
start_date=start_date_str,
end_date=end_date_str,
api_key=base_ctx.get("api_key") or "",
access_token=base_ctx.get("access_token") or "",
)
# Tab 2: Visibility & Content
with report_tab_visibility:
render_feature_section(
client=client,
campaign_id=base_ctx["campaign_id"],
feature_key="visibility",
title="플랫폼별 가시성",
description="ChatGPT, Gemini 등 AI 플랫폼별 자사 vs 경쟁사 노출 비교",
start_date=start_date_str,
end_date=end_date_str,
api_key=base_ctx.get("api_key") or "",
access_token=base_ctx.get("access_token") or "",
)
render_feature_section(
client=client,
campaign_id=base_ctx["campaign_id"],
feature_key="content-types",
title="콘텐츠 유형 분포",
description="AI가 인용하는 콘텐츠 유형 (블로그, 뉴스, 공식 사이트 등)",
start_date=start_date_str,
end_date=end_date_str,
api_key=base_ctx.get("api_key") or "",
access_token=base_ctx.get("access_token") or "",
)
render_feature_section(
client=client,
campaign_id=base_ctx["campaign_id"],
feature_key="sentiment",
title="브랜드 감정 분석",
description="브랜드별 긍정/부정/중립 감정 분포",
start_date=start_date_str,
end_date=end_date_str,
api_key=base_ctx.get("api_key") or "",
access_token=base_ctx.get("access_token") or "",
)
# Tab 3: Citation Analysis
with report_tab_citation:
render_feature_section(
client=client,
campaign_id=base_ctx["campaign_id"],
feature_key="citations",
title="인용 출처 순위",
description="AI 답변에서 가장 많이 인용되는 도메인과 출처",
start_date=start_date_str,
end_date=end_date_str,
api_key=base_ctx.get("api_key") or "",
access_token=base_ctx.get("access_token") or "",
)
render_feature_section(
client=client,
campaign_id=base_ctx["campaign_id"],
feature_key="citation-trends",
title="인용 추이",
description="시간에 따른 인용 출처 변화 트렌드",
start_date=start_date_str,
end_date=end_date_str,
api_key=base_ctx.get("api_key") or "",
access_token=base_ctx.get("access_token") or "",
)
render_feature_section(
client=client,
campaign_id=base_ctx["campaign_id"],
feature_key="homepage-citations",
title="홈페이지 인용률",
description="자사 홈페이지가 AI 답변에 직접 인용되는 비율",
start_date=start_date_str,
end_date=end_date_str,
api_key=base_ctx.get("api_key") or "",
access_token=base_ctx.get("access_token") or "",
)
# Tab 4: Full Report
with report_tab_full:
full_report.render(client, base_ctx, start_date_str, end_date_str)
def _render_period_selector(
first_date: datetime,
last_date: datetime,
first_date_str: str,
last_date_str: str,
total_days: int,
) -> tuple[str, str]:
"""기간 선택 UI 렌더링. (start_date, end_date) 반환."""
col_period, col_date1, col_date2 = st.columns([1.5, 1, 1])
with col_period:
period_options = [label for label, _ in PERIOD_PRESETS]
selected_period = st.selectbox(
"분석 기간",
options=period_options,
index=5, # Default to "전체 기간"
key="reports:period_select",
help=f"캠페인 데이터: {first_date_str} ~ {last_date_str} (총 {total_days}일)",
)
period_idx = period_options.index(selected_period)
_, period_days = PERIOD_PRESETS[period_idx]
if period_days == -1: # Custom selection
with col_date1:
report_start = st.date_input(
"시작일",
value=first_date,
min_value=first_date,
max_value=last_date,
key="reports:start_date",
)
with col_date2:
report_end = st.date_input(
"종료일",
value=last_date,
min_value=first_date,
max_value=last_date,
key="reports:end_date",
)
return str(report_start), str(report_end)
elif period_days is None: # All data
with col_date1:
st.text_input("시작일", value=first_date_str, disabled=True, key="reports:start_display")
with col_date2:
st.text_input("종료일", value=last_date_str, disabled=True, key="reports:end_display")
return first_date_str, last_date_str
else: # Preset days
end_date = last_date
start_date = max(first_date, end_date - timedelta(days=period_days - 1))
start_date_str = start_date.strftime("%Y-%m-%d")
end_date_str = end_date.strftime("%Y-%m-%d")
with col_date1:
st.text_input("시작일", value=start_date_str, disabled=True, key="reports:start_display")
with col_date2:
st.text_input("종료일", value=end_date_str, disabled=True, key="reports:end_display")
return start_date_str, end_date_str