kcg-ai-monitoring/frontend/src/services/event.ts
htlee 0679c04bfe feat(frontend): 워크플로우 연결 Step 2 — EventList 워크플로우 + MMSI 링크
- EventList 인라인 액션 버튼 4종 추가 (확인/선박상세/단속등록/오탐)
  - 확인(ACK): NEW 상태 이벤트만 활성, ackEvent API 연동
  - 선박 상세: /vessel/{mmsi} 네비게이션
  - 단속 등록: createEnforcementRecord API → 이벤트 RESOLVED 자동 전환
  - 오탐 처리: updateEventStatus(FALSE_POSITIVE) 연동
- MMSI → VesselDetail 링크 3개 화면 적용
  - EventList: MMSI 컬럼 클릭 → /vessel/{mmsi}
  - DarkVesselDetection: MMSI 컬럼 클릭 → /vessel/{mmsi}
  - EnforcementHistory: 대상 선박 컬럼 클릭 → /vessel/{mmsi}
- PredictionEvent 타입에 features 필드 추가 (dark_tier, transship_score 등)
- analysisApi.ts 서비스 신규 생성 (직접 조회 API 5개 연동)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:50:31 +09:00

187 lines
5.3 KiB
TypeScript

/**
* 이벤트/경보 API 서비스 — 실제 백엔드 연동
*/
const API_BASE = import.meta.env.VITE_API_URL ?? '/api';
// ─── 서버 응답 타입 ───────────────────────────────
export interface PredictionEvent {
id: number;
eventUid: string;
occurredAt: string;
level: string;
category: string;
title: string;
detail: string | null;
vesselMmsi: string | null;
vesselName: string | null;
areaName: string | null;
zoneCode: string | null;
lat: number | null;
lon: number | null;
speedKn: number | null;
sourceType: string | null;
aiConfidence: number | null;
status: string;
assigneeId: string | null;
assigneeName: string | null;
ackedAt: string | null;
resolvedAt: string | null;
resolutionNote: string | null;
createdAt: string;
features?: {
dark_suspicion_score?: number;
dark_tier?: string;
dark_patterns?: string[];
dark_history_7d?: number;
transship_tier?: string;
transship_score?: number;
[key: string]: unknown;
} | null;
}
export interface EventPageResponse {
content: PredictionEvent[];
totalElements: number;
totalPages: number;
number: number;
size: number;
}
export interface EventStats {
[status: string]: number;
}
// ─── API 호출 ─────────────────────────────────────
export async function getEvents(params?: {
status?: string;
level?: string;
category?: string;
vesselMmsi?: string;
page?: number;
size?: number;
}): Promise<EventPageResponse> {
const query = new URLSearchParams();
if (params?.status) query.set('status', params.status);
if (params?.level) query.set('level', params.level);
if (params?.category) query.set('category', params.category);
if (params?.vesselMmsi) query.set('vesselMmsi', params.vesselMmsi);
query.set('page', String(params?.page ?? 0));
query.set('size', String(params?.size ?? 20));
const res = await fetch(`${API_BASE}/events?${query}`, { credentials: 'include' });
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export async function getEventById(id: number): Promise<PredictionEvent> {
const res = await fetch(`${API_BASE}/events/${id}`, { credentials: 'include' });
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export async function ackEvent(id: number): Promise<PredictionEvent> {
const res = await fetch(`${API_BASE}/events/${id}/ack`, {
method: 'PATCH',
credentials: 'include',
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export async function updateEventStatus(
id: number,
status: string,
comment?: string,
): Promise<PredictionEvent> {
const res = await fetch(`${API_BASE}/events/${id}/status`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status, comment }),
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export async function getEventStats(): Promise<EventStats> {
const res = await fetch(`${API_BASE}/events/stats`, { credentials: 'include' });
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
// ─── 알림 API ────────────────────────────────────
export interface PredictionAlert {
id: number;
eventId: number;
channel: string;
recipient: string | null;
sentAt: string;
deliveryStatus: string;
aiConfidence: number | null;
}
export interface AlertPageResponse {
content: PredictionAlert[];
totalElements: number;
totalPages: number;
number: number;
size: number;
}
export async function getAlerts(params?: {
eventId?: number;
page?: number;
size?: number;
}): Promise<AlertPageResponse> {
const query = new URLSearchParams();
if (params?.eventId != null) query.set('eventId', String(params.eventId));
query.set('page', String(params?.page ?? 0));
query.set('size', String(params?.size ?? 20));
const res = await fetch(`${API_BASE}/alerts?${query}`, { credentials: 'include' });
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
// ─── 하위 호환 헬퍼 (기존 EventRecord 형식 → PredictionEvent 매핑) ──
/** @deprecated PredictionEvent를 직접 사용하세요 */
export interface LegacyEventRecord {
id: string;
time: string;
level: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
type: string;
title: string;
detail: string;
vesselName?: string;
mmsi?: string;
area?: string;
lat?: number;
lng?: number;
speed?: number;
status?: string;
assignee?: string;
}
/** PredictionEvent → LegacyEventRecord 변환 */
export function toLegacyEvent(e: PredictionEvent): LegacyEventRecord {
return {
id: e.eventUid,
time: e.occurredAt,
level: e.level as LegacyEventRecord['level'],
type: e.category,
title: e.title,
detail: e.detail ?? '',
vesselName: e.vesselName ?? undefined,
mmsi: e.vesselMmsi ?? undefined,
area: e.areaName ?? undefined,
lat: e.lat ?? undefined,
lng: e.lon ?? undefined,
speed: e.speedKn ?? undefined,
status: e.status ?? undefined,
assignee: e.assigneeName ?? undefined,
};
}