kcg-ai-monitoring/frontend/src/services/gearCollisionApi.ts
htlee a4e29629fc feat(detection): GEAR_IDENTITY_COLLISION 탐지 패턴 추가
동일 어구 이름이 서로 다른 MMSI 로 같은 5분 사이클에 동시 AIS 송출되는
공존 케이스를 신규 탐지 패턴으로 분리해 기록·분류한다. 부수 효과로
fleet_tracker.track_gear_identity 의 PK 충돌로 인한 사이클 실패도 해소.

Prediction
- algorithms/gear_identity.py: detect_gear_name_collisions + classify_severity
- fleet_tracker.py: 공존/교체 분기 분리, UPSERT helper, savepoint 점수 이전
- output/event_generator.py: run_gear_identity_collision_events 추가
- scheduler.py: track_gear_identity 직후 이벤트 승격 호출

Backend (domain/analysis)
- GearIdentityCollision 엔티티 + Repository(Specification+stats)
- GearIdentityCollisionService (@Transactional readOnly / @Auditable resolve)
- GearCollisionController /api/analysis/gear-collisions (list/stats/detail/resolve)
- GearCollisionResponse / StatsResponse / ResolveRequest (record)

DB
- V030__gear_identity_collision.sql: gear_identity_collisions 테이블
  + auth_perm_tree 엔트리(detection:gear-collision nav_sort=950) + 역할별 권한

Frontend
- shared/constants/gearCollisionStatuses.ts + catalogRegistry 등록
- services/gearCollisionApi.ts (list/stats/get/resolve)
- features/detection/GearCollisionDetection.tsx (PageContainer+Section+DataTable
  + 분류 액션 폼, design system SSOT 준수)
- componentRegistry + feature index + i18n detection.json / common.json(ko/en)
2026-04-17 06:53:12 +09:00

115 lines
3.3 KiB
TypeScript

/**
* gear_identity_collisions 조회 + 분류 액션 API 서비스.
* 백엔드 /api/analysis/gear-collisions 연동.
*/
import type { AnalysisPageResponse } from './analysisApi';
const API_BASE = import.meta.env.VITE_API_URL ?? '/api';
// ─── DTO (백엔드 GearCollisionResponse 1:1 매핑) ─────────────
export interface GearCollision {
id: number;
name: string;
mmsiLo: string;
mmsiHi: string;
parentName: string | null;
parentVesselId: number | null;
firstSeenAt: string;
lastSeenAt: string;
coexistenceCount: number;
swapCount: number;
maxDistanceKm: number | null;
lastLatLo: number | null;
lastLonLo: number | null;
lastLatHi: number | null;
lastLonHi: number | null;
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | string;
status: 'OPEN' | 'REVIEWED' | 'CONFIRMED_ILLEGAL' | 'FALSE_POSITIVE' | string;
resolutionNote: string | null;
evidence: Array<Record<string, unknown>> | null;
updatedAt: string;
}
export interface GearCollisionStats {
total: number;
byStatus: Record<string, number>;
bySeverity: Record<string, number>;
hours: number;
}
export type GearCollisionResolveAction =
| 'REVIEWED'
| 'CONFIRMED_ILLEGAL'
| 'FALSE_POSITIVE'
| 'REOPEN';
export interface GearCollisionResolveRequest {
action: GearCollisionResolveAction;
note?: string;
}
// ─── 내부 헬퍼 ─────────────
function buildQuery(params: Record<string, unknown>): string {
const qs = new URLSearchParams();
for (const [k, v] of Object.entries(params)) {
if (v === undefined || v === null || v === '') continue;
qs.set(k, String(v));
}
const s = qs.toString();
return s ? `?${s}` : '';
}
async function apiGet<T>(path: string, params: Record<string, unknown> = {}): Promise<T> {
const res = await fetch(`${API_BASE}${path}${buildQuery(params)}`, { credentials: 'include' });
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
async function apiPost<T>(path: string, body: unknown): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
// ─── 공개 함수 ─────────────
/** 어구 정체성 충돌 목록 조회 (필터 + 페이징). */
export function listGearCollisions(params?: {
status?: string;
severity?: string;
name?: string;
hours?: number;
page?: number;
size?: number;
}): Promise<AnalysisPageResponse<GearCollision>> {
return apiGet('/analysis/gear-collisions', {
hours: 48, page: 0, size: 50, ...params,
});
}
/** status/severity 집계 */
export function getGearCollisionStats(hours = 48): Promise<GearCollisionStats> {
return apiGet('/analysis/gear-collisions/stats', { hours });
}
/** 단건 상세 조회 */
export function getGearCollision(id: number): Promise<GearCollision> {
return apiGet(`/analysis/gear-collisions/${id}`);
}
/** 운영자 분류 액션 */
export function resolveGearCollision(
id: number,
body: GearCollisionResolveRequest,
): Promise<GearCollision> {
return apiPost(`/analysis/gear-collisions/${id}/resolve`, body);
}