""" 통계·보고 API (모바일 기능 #93~#97). GET /api/stats/my — 나의 SR 처리 통계 GET /api/stats/institutions — 기관별 SR 현황 비교 GET /api/stats/deploy-history — 배포 이력 타임라인 (VibeSession) GET /api/stats/kpi — KPI 대시보드 GET /api/stats/export-pdf — 리포트 JSON (앱에서 PDF 변환) """ from __future__ import annotations from datetime import datetime, timedelta from typing import Optional from fastapi import APIRouter, Depends from sqlalchemy import select, func, case from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import ( SRRequest, SRStatus, Institution, User, UserRole, VibeSession, ) router = APIRouter(prefix="/api/stats", tags=["Statistics"]) def _this_month(): now = datetime.now() return datetime(now.year, now.month, 1) async def _inst_ids_for(user: User, db: AsyncSession): if user.role != UserRole.CUSTOMER: return None rows = (await db.execute( select(Institution.inst_id).where(Institution.inst_code == user.inst_code) )).scalars().all() return rows or [-1] @router.get("/my") async def my_stats( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): now = datetime.now() this_m = datetime(now.year, now.month, 1) last_m = datetime(now.year, now.month - 1, 1) if now.month > 1 else datetime(now.year - 1, 12, 1) base = select(SRRequest).where(SRRequest.requested_by == current_user.username) async def _count(q): return (await db.execute(select(func.count()).select_from(q.subquery()))).scalar_one() total = await _count(base) this_done = await _count(base.where(SRRequest.status == SRStatus.COMPLETED, SRRequest.created_at >= this_m)) last_done = await _count(base.where(SRRequest.status == SRStatus.COMPLETED, SRRequest.created_at >= last_m, SRRequest.created_at < this_m)) this_all = await _count(base.where(SRRequest.created_at >= this_m)) last_all = await _count(base.where(SRRequest.created_at >= last_m, SRRequest.created_at < this_m)) return { "total": total, "this_month": {"created": this_all, "completed": this_done, "rate": round(this_done / this_all * 100, 1) if this_all else 0}, "last_month": {"created": last_all, "completed": last_done, "rate": round(last_done / last_all * 100, 1) if last_all else 0}, } @router.get("/institutions") async def institution_stats( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): q = ( select( Institution.inst_id, Institution.inst_name, func.count(SRRequest.sr_id).label("total"), func.sum(case((SRRequest.status == SRStatus.COMPLETED, 1), else_=0)).label("done"), ) .outerjoin(SRRequest, SRRequest.inst_id == Institution.inst_id) .group_by(Institution.inst_id, Institution.inst_name) .order_by(func.count(SRRequest.sr_id).desc()) ) rows = (await db.execute(q)).all() return { "items": [ { "inst_id": r.inst_id, "inst_name": r.inst_name, "total": r.total or 0, "completed": r.done or 0, "rate": round((r.done or 0) / r.total * 100, 1) if r.total else 0, } for r in rows ] } @router.get("/deploy-history") async def deploy_history( limit: int = 30, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): q = ( select(VibeSession) .order_by(VibeSession.started_at.desc()) .limit(limit) ) rows = (await db.execute(q)).scalars().all() return { "items": [ { "id": r.id, "project": r.project_name if hasattr(r, "project_name") else "N/A", "status": r.status, "started_at": r.started_at.isoformat() if r.started_at else None, "deployed_at": r.deployed_at.isoformat() if r.deployed_at else None, "duration_sec": int((r.deployed_at - r.started_at).total_seconds()) if r.deployed_at and r.started_at else None, "deployed_by": r.requested_by if hasattr(r, "requested_by") else None, } for r in rows ] } @router.get("/kpi") async def kpi_dashboard( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): now = datetime.now() month_start = datetime(now.year, now.month, 1) total_sr = (await db.execute(select(func.count(SRRequest.sr_id)).where(SRRequest.created_at >= month_start))).scalar_one() done_sr = (await db.execute(select(func.count(SRRequest.sr_id)).where(SRRequest.created_at >= month_start, SRRequest.status == SRStatus.COMPLETED))).scalar_one() breach = (await db.execute(select(func.count(SRRequest.sr_id)).where(SRRequest.created_at >= month_start, SRRequest.sla_breached == True))).scalar_one() return { "period": month_start.strftime("%Y-%m"), "sr_completion_rate": round(done_sr / total_sr * 100, 1) if total_sr else 0, "sla_compliance_rate": round((total_sr - breach) / total_sr * 100, 1) if total_sr else 100, "total_sr": total_sr, "completed_sr": done_sr, "sla_breach": breach, "csap_score": 82.5, "targets": { "sr_completion_rate": 90, "sla_compliance_rate": 95, "csap_score": 85, }, } @router.get("/export-pdf") async def export_pdf_data( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): kpi = await kpi_dashboard(db=db, current_user=current_user) my = await my_stats(db=db, current_user=current_user) return { "generated_at": datetime.now().isoformat(), "generated_by": current_user.username, "kpi": kpi, "my_stats": my, }