Files
blog-writer/dashboard/backend/api_analytics.py
sinmb79 213f57b52d feat: v3.1 대시보드 추가 (React + FastAPI)
Media Engine Control Panel — 6탭 웹 대시보드

[백엔드] FastAPI (dashboard/backend/)
- server.py: 포트 8080, CORS, React SPA 서빙
- api_overview.py: KPI 카드 + 파이프라인 상태 + 활동 로그
- api_content.py: 칸반 보드 + 승인/거부 + 수동 트리거
- api_analytics.py: 방문자 추이 + 플랫폼/코너별 성과
- api_novels.py: 소설 목록/생성/에피소드 관리
- api_settings.py: engine.json CRUD
- api_connections.py: AI 서비스 연결 관리 + 키 저장
- api_tools.py: 기능별 AI 도구 선택
- api_cost.py: 구독 현황 + API 사용량 추적
- api_logs.py: 시스템 로그 필터/검색

[프론트엔드] React + Vite + Tailwind + Recharts (dashboard/frontend/)
- Overview: KPI 카드 + 파이프라인 + 코너별 바차트 + 활동 로그
- Content: 4열 칸반 보드 + 상세 모달 + 승인/거부
- Analytics: LineChart 방문자 추이 + 플랫폼별 성과
- Novel: 소설 목록 + 에피소드 테이블 + 새 소설 생성 폼
- Settings: 5개 서브탭 (AI연결/도구선택/배포채널/품질/비용관리)
- Logs: 필터/검색 시스템 로그 뷰어

[디자인] CNN 다크+골드 테마
- 배경 #0a0a0d + 액센트 #c8a84e
- 모바일 반응형 (Tailscale 외부 접속 대응)

[실행]
- dashboard/start.bat 더블클릭 → http://localhost:8080

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 13:17:53 +09:00

132 lines
4.0 KiB
Python

"""
dashboard/backend/api_analytics.py
Analytics 탭 API — 방문자 통계, KPI, 코너별 성과
"""
import json
from datetime import date, timedelta
from pathlib import Path
from typing import Optional
from fastapi import APIRouter, Query
BASE_DIR = Path(__file__).parent.parent.parent
DATA_DIR = BASE_DIR / "data"
ANALYTICS_DIR = DATA_DIR / "analytics"
router = APIRouter()
def _load_all_analytics() -> list:
"""analytics/*.json 전체 로드"""
records = []
if not ANALYTICS_DIR.exists():
return records
for f in sorted(ANALYTICS_DIR.glob("*.json")):
try:
data = json.loads(f.read_text(encoding="utf-8"))
if isinstance(data, list):
records.extend(data)
elif isinstance(data, dict):
records.append(data)
except Exception:
pass
return records
def _aggregate_kpi(records: list) -> dict:
total_visitors = sum(r.get("visitors", 0) for r in records)
total_pageviews = sum(r.get("pageviews", 0) for r in records)
avg_duration = 0
avg_ctr = 0.0
durations = [r.get("avg_duration_sec", 0) for r in records if r.get("avg_duration_sec")]
if durations:
avg_duration = int(sum(durations) / len(durations))
ctrs = [r.get("ctr", 0.0) for r in records if r.get("ctr")]
if ctrs:
avg_ctr = round(sum(ctrs) / len(ctrs), 2)
return {
"visitors": total_visitors,
"pageviews": total_pageviews,
"avg_duration_sec": avg_duration,
"ctr": avg_ctr,
}
def _aggregate_corners(records: list) -> list:
corner_map: dict = {}
for r in records:
corner = r.get("corner", "기타")
if corner not in corner_map:
corner_map[corner] = {"visitors": 0, "pageviews": 0, "posts": 0}
corner_map[corner]["visitors"] += r.get("visitors", 0)
corner_map[corner]["pageviews"] += r.get("pageviews", 0)
corner_map[corner]["posts"] += r.get("post_count", 1)
result = []
for name, data in corner_map.items():
result.append({"corner": name, **data})
result.sort(key=lambda x: x["visitors"], reverse=True)
return result
def _top_posts(records: list, limit: int = 5) -> list:
posts = []
for r in records:
if "title" in r and "visitors" in r:
posts.append({
"title": r["title"],
"visitors": r["visitors"],
"corner": r.get("corner", ""),
"published_at": r.get("date", ""),
})
posts.sort(key=lambda x: x["visitors"], reverse=True)
return posts[:limit]
def _platform_performance(records: list) -> list:
platform_map: dict = {}
for r in records:
platform = r.get("platform", "blogger")
if platform not in platform_map:
platform_map[platform] = {"visitors": 0, "posts": 0}
platform_map[platform]["visitors"] += r.get("visitors", 0)
platform_map[platform]["posts"] += 1
return [{"platform": k, **v} for k, v in platform_map.items()]
@router.get("/analytics")
async def get_analytics():
records = _load_all_analytics()
return {
"kpi": _aggregate_kpi(records),
"corners": _aggregate_corners(records),
"top_posts": _top_posts(records),
"platforms": _platform_performance(records),
"total_records": len(records),
}
@router.get("/analytics/chart")
async def get_analytics_chart(days: int = Query(default=7, ge=1, le=365)):
"""days일간 방문자 시계열 데이터"""
records = _load_all_analytics()
today = date.today()
date_range = [(today - timedelta(days=i)).isoformat() for i in range(days - 1, -1, -1)]
# 날짜별 집계
daily: dict = {d: {"date": d, "visitors": 0, "pageviews": 0} for d in date_range}
for r in records:
d = r.get("date", "")[:10]
if d in daily:
daily[d]["visitors"] += r.get("visitors", 0)
daily[d]["pageviews"] += r.get("pageviews", 0)
return {"chart": list(daily.values()), "days": days}