guardia-itsm/routers/stats.py
2026-06-07 00:13:38 +09:00

169 lines
5.9 KiB
Python

"""
통계·보고 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,
}