kcg-ai-monitoring/frontend/src/services/kpi.ts
htlee 19b1613157 feat: 프론트 전수 mock 정리 + UTC→KST 통일 + i18n 수정 + stats hourly API
## 시간 표시 KST 통일
- shared/utils/dateFormat.ts 공통 유틸 신규 (formatDateTime/formatDate/formatTime/toDateParam)
- 14개 파일에서 인라인 toLocaleString → 공통 유틸 교체

## i18n 'group.parentInference' 사이드바 미번역 수정
- ko/en common.json의 'group' 키 중복 정의를 병합
  (95행 두번째 group 객체가 35행을 덮어써서 parentInference 누락)

## Dashboard/MonitoringDashboard/Statistics 더미→실 API
- 백엔드 GET /api/stats/hourly 신규 (PredictionStatsHourly 엔티티/리포지토리)
- Dashboard: HOURLY_DETECTION/VESSEL_TYPE/AREA_RISK 하드코딩 제거 →
  getHourlyStats(24) + getDailyStats(today) 결과로 useMemo 변환
- MonitoringDashboard: TREND Math.random() 제거 → getHourlyStats 기반
  위험도 가중평균 + 경보 카운트
- Statistics: KPI_DATA 하드코딩 제거 → getKpiMetrics() 결과를 표 행으로

## Store mock 의존성 제거
- eventStore.alerts/MOCK_ALERTS 제거 (MobileService는 events에서 직접 추출)
- enforcementStore.plans 제거 (EnforcementPlan은 이미 직접 API 호출)
- transferStore + MOCK_TRANSFERS 완전 제거
  (ChinaFishing/TransferDetection은 RealTransshipSuspects 컴포넌트 사용)
- mock/events.ts, mock/enforcement.ts, mock/transfers.ts 파일 삭제

## RiskMap 랜덤 격자 제거
- generateGrid() Math.random() 제거 → 빈 배열 + 'AI 분석 데이터 수집 중' 안내
- MTIS 외부 통계 5개 탭에 [MTIS 외부 통계] 배지 추가

## 12개 mock 화면에 '데모 데이터' 노란색 배지 추가
- patrol/PatrolRoute, FleetOptimization
- admin/AdminPanel, DataHub, NoticeManagement, SystemConfig
- ai-operations/AIModelManagement, MLOpsPage
- field-ops/ShipAgent
- statistics/ReportManagement, ExternalService
- surveillance/MapControl

## 백엔드 NUMERIC precision 동기화
- PredictionKpi.deltaPct: 5,2 → 12,2
- PredictionStatsDaily/Monthly.aiAccuracyPct: 5,2 → 12,2
- (V015 마이그레이션과 동기화)

44 files changed, +346 / -787

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 15:36:38 +09:00

145 lines
4.3 KiB
TypeScript

/**
* 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<string, number> | null;
byZone: Record<string, number> | null;
byRiskLevel: Record<string, number> | null;
byGearType: Record<string, number> | null;
byViolationType: Record<string, number> | null;
eventCount: number;
criticalEventCount: number;
falsePositiveCount: number;
aiAccuracyPct: number | null;
}
// ─── API 호출 ───────────────────
export async function getKpiMetrics(): Promise<PredictionKpi[]> {
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<PredictionStatsMonthly[]> {
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();
}
export interface PredictionStatsDaily {
statDate: string;
totalDetections: number;
enforcementCount: number;
eventCount: number;
criticalEventCount: number;
byCategory: Record<string, number> | null;
byZone: Record<string, number> | null;
byRiskLevel: Record<string, number> | null;
byGearType: Record<string, number> | null;
byViolationType: Record<string, number> | null;
aiAccuracyPct: number | null;
}
export interface PredictionStatsHourly {
statHour: string;
totalDetections: number;
eventCount: number;
criticalCount: number;
byCategory: Record<string, number> | null;
byZone: Record<string, number> | null;
byRiskLevel: Record<string, number> | null;
}
export async function getDailyStats(
from: string,
to: string,
): Promise<PredictionStatsDaily[]> {
const res = await fetch(`${API_BASE}/stats/daily?from=${from}&to=${to}`, {
credentials: 'include',
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export async function getHourlyStats(
hours: number = 24,
): Promise<PredictionStatsHourly[]> {
const res = await fetch(`${API_BASE}/stats/hourly?hours=${hours}`, {
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<string, number> = {};
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);
}