/** * KPI/통계 API 서비스 * - 실제 백엔드 API 호출 (GET /api/stats/kpi, /api/stats/monthly) * - 하위 호환용 변환 헬퍼 제공 */ import type { KpiMetric, MonthlyTrend, ViolationType } from '@data/mock/kpi'; const API_BASE = import.meta.env.VITE_API_URL ?? '/api'; // ─── 백엔드 API 응답 타입 ─────────────────── export interface PredictionKpi { kpiKey: string; kpiLabel: string; value: number; trend: string | null; // 'up', 'down', 'flat' deltaPct: number | null; updatedAt: string; } export interface PredictionStatsMonthly { statMonth: string; // '2026-04-01' (DATE -> ISO string) totalDetections: number; totalEnforcements: number; byCategory: Record | null; byZone: Record | null; byRiskLevel: Record | null; byGearType: Record | null; byViolationType: Record | null; eventCount: number; criticalEventCount: number; falsePositiveCount: number; aiAccuracyPct: number | null; } // ─── API 호출 ─────────────────── export async function getKpiMetrics(): Promise { const res = await fetch(`${API_BASE}/stats/kpi`, { credentials: 'include' }); if (!res.ok) throw new Error(`API error: ${res.status}`); return res.json(); } export async function getMonthlyStats( from: string, to: string, ): Promise { const res = await fetch(`${API_BASE}/stats/monthly?from=${from}&to=${to}`, { credentials: 'include', }); if (!res.ok) throw new Error(`API error: ${res.status}`); return res.json(); } // ─── 하위 호환 변환 헬퍼 ─────────────────── /** PredictionKpi -> 기존 KpiMetric 형태로 변환 (Dashboard에서 사용) */ export function toKpiMetric(kpi: PredictionKpi): KpiMetric { return { id: kpi.kpiKey, label: kpi.kpiLabel, value: kpi.value, prev: kpi.deltaPct ? Math.round(kpi.value / (1 + kpi.deltaPct / 100)) : undefined, }; } /** PredictionStatsMonthly -> MonthlyTrend 변환 */ export function toMonthlyTrend(stat: PredictionStatsMonthly): MonthlyTrend { return { month: stat.statMonth.substring(0, 7), // '2026-04-01' -> '2026-04' enforce: stat.totalEnforcements ?? 0, detect: stat.totalDetections ?? 0, accuracy: stat.aiAccuracyPct ?? 0, }; } /** MonthlyStats의 byViolationType -> ViolationType[] 변환 (기간 합산) */ export function toViolationTypes( stats: PredictionStatsMonthly[], ): ViolationType[] { const totals: Record = {}; stats.forEach((s) => { if (s.byViolationType) { Object.entries(s.byViolationType).forEach(([k, v]) => { totals[k] = (totals[k] ?? 0) + (v as number); }); } }); const sum = Object.values(totals).reduce((a, b) => a + b, 0); return Object.entries(totals) .map(([type, count]) => ({ type, count, pct: sum > 0 ? Math.round((count / sum) * 100) : 0, })) .sort((a, b) => b.count - a.count); }