- 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>
187 lines
5.3 KiB
TypeScript
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,
|
|
};
|
|
}
|