169 lines
5.9 KiB
Python
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,
|
|
}
|