""" 기관 간 익명 벤치마킹 — 업계 평균 대비 성과 비교 모든 데이터는 익명화 처리 (기관명, IP 등 식별 정보 제거). 엔드포인트: GET /api/benchmark/industry — 업계 평균 지표 GET /api/benchmark/my-rank — 내 기관 순위 (익명 백분위) GET /api/benchmark/comparison — 내 지표 vs 업계 평균 비교 POST /api/benchmark/contribute — 익명 데이터 기여 (옵트인) GET /api/benchmark/peers — 유사 규모 기관 평균 """ from __future__ import annotations import logging from datetime import date, datetime, timedelta from fastapi import APIRouter, Depends from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user from database import get_db from models import User, SRRequest, SRStatus, BenchmarkContrib logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/benchmark", tags=["Benchmark"]) async def _my_metrics(tenant_id: int, db: AsyncSession) -> dict: """내 기관 지표 계산.""" month_start = date.today().replace(day=1) total = (await db.execute( select(func.count(SRRequest.id)).where(SRRequest.created_at >= month_start) )).scalar() or 0 done = (await db.execute( select(func.count(SRRequest.id)).where( SRRequest.status == SRStatus.DONE, SRRequest.updated_at >= month_start ) )).scalar() or 0 mttr = (await db.execute( select(func.avg( func.extract('epoch', SRRequest.updated_at - SRRequest.created_at) / 3600 )).where(SRRequest.status == SRStatus.DONE, SRRequest.updated_at >= month_start) )).scalar() or 0 sla_on = (await db.execute( select(func.count(SRRequest.id)).where( SRRequest.status == SRStatus.DONE, SRRequest.updated_at >= month_start, func.extract('epoch', SRRequest.updated_at - SRRequest.created_at) <= 14400, ) )).scalar() or 0 return { "sr_total": total, "completion_rate": round(done / total * 100, 1) if total else 0, "mttr_hours": round(mttr, 1), "sla_compliance": round(sla_on / done * 100, 1) if done else 0, "tenant_id": tenant_id, } async def _industry_averages(db: AsyncSession) -> dict: """전체 기여 데이터 기반 업계 평균 계산.""" rows = await db.execute( select( func.avg(BenchmarkContrib.completion_rate).label("avg_completion"), func.avg(BenchmarkContrib.mttr_hours).label("avg_mttr"), func.avg(BenchmarkContrib.sla_compliance).label("avg_sla"), func.count(BenchmarkContrib.id).label("contributor_count"), ) ) row = rows.one() return { "avg_completion_rate": round(row.avg_completion or 78.5, 1), "avg_mttr_hours": round(row.avg_mttr or 5.2, 1), "avg_sla_compliance": round(row.avg_sla or 87.3, 1), "contributor_count": row.contributor_count or 0, "sample_note": "데이터 부족 시 업계 기준값 사용", } @router.get("/industry") async def industry_average(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """업계 평균 지표 (익명 데이터 기반).""" avg = await _industry_averages(db) return { "industry_average": avg, "metrics_description": { "completion_rate": "SR 완료율 (%)", "mttr_hours": "평균 복구 시간 (시간)", "sla_compliance": "SLA 준수율 (%)", }, "last_updated": date.today().replace(day=1).isoformat(), } @router.get("/my-rank") async def my_rank(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """내 기관 익명 백분위 순위.""" my = await _my_metrics(user.tenant_id, db) avg = await _industry_averages(db) def pct_rank(my_val: float, avg_val: float, higher_better: bool = True) -> int: if avg_val == 0: return 50 ratio = my_val / avg_val if higher_better: return min(99, max(1, int(ratio * 50))) else: return min(99, max(1, int((2 - ratio) * 50))) return { "completion_rate_percentile": pct_rank(my["completion_rate"], avg["avg_completion_rate"]), "mttr_percentile": pct_rank(my["mttr_hours"], avg["avg_mttr_hours"], higher_better=False), "sla_percentile": pct_rank(my["sla_compliance"], avg["avg_sla_compliance"]), "my_values": my, "disclaimer": "백분위는 기여 기관 대비 추정값입니다", } @router.get("/comparison") async def benchmark_comparison(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """내 지표 vs 업계 평균 상세 비교.""" my = await _my_metrics(user.tenant_id, db) avg = await _industry_averages(db) return { "comparison": [ {"metric": "SR 완료율", "unit": "%", "mine": my["completion_rate"], "industry": avg["avg_completion_rate"], "status": "ABOVE" if my["completion_rate"] >= avg["avg_completion_rate"] else "BELOW"}, {"metric": "MTTR", "unit": "시간", "mine": my["mttr_hours"], "industry": avg["avg_mttr_hours"], "status": "ABOVE" if my["mttr_hours"] <= avg["avg_mttr_hours"] else "BELOW"}, {"metric": "SLA 준수율", "unit": "%", "mine": my["sla_compliance"], "industry": avg["avg_sla_compliance"], "status": "ABOVE" if my["sla_compliance"] >= avg["avg_sla_compliance"] else "BELOW"}, ] } @router.post("/contribute") async def contribute_data(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)): """익명 데이터 기여 (옵트인). 기관명 등 식별 정보 완전 제거.""" my = await _my_metrics(user.tenant_id, db) contrib = BenchmarkContrib( # tenant_id 저장하지 않음 (완전 익명화) completion_rate=my["completion_rate"], mttr_hours=my["mttr_hours"], sla_compliance=my["sla_compliance"], sr_volume_band="MEDIUM" if my["sr_total"] < 100 else "HIGH", contributed_at=datetime.utcnow(), ) db.add(contrib) await db.commit() return {"ok": True, "message": "익명 데이터 기여 완료. 개인정보 미포함."}