## 시간 표시 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>
145 lines
4.3 KiB
TypeScript
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);
|
|
}
|