kcg-ai-monitoring/frontend/src/services/vesselAnalysisApi.ts
htlee 9251d7593c refactor: 프로젝트 뼈대 정리 — iran 잔재 제거 + 백엔드 계층 분리 + 카탈로그 등록
iran 백엔드 프록시 잔재 제거:
- IranBackendClient dead class 삭제, AppProperties/application.yml iran-backend 블록 제거
- Frontend UI 라벨/주석/system-flow manifest deprecated 마킹
- CLAUDE.md 시스템 구성 다이어그램 최신화

백엔드 계층 분리:
- AlertController/MasterDataController/AdminStatsController 에서 repository/JdbcTemplate 직접 주입 제거
- AlertService/MasterDataService/AdminStatsService 신규 계층 도입 + @Transactional(readOnly=true)
- Proxy controller 의 @PostConstruct RestClient 생성 → RestClientConfig @Bean 으로 통합

감사 로그 보강:
- EnforcementService createRecord/updateRecord/createPlan 에 @Auditable 추가
- VesselAnalysisGroupService.resolveParent 에 PARENT_RESOLVE 액션 기록

카탈로그 정합성:
- performanceStatus 를 catalogRegistry 에 등록 (쇼케이스 자동 노출)
- alertLevels 확장: isValidAlertLevel / isHighSeverity / getAlertLevelOrder
- LiveMapView/DarkVesselDetection 시각 매핑(opacity/radius/tier score) 상수로 추출
- GearIdentification/vesselAnomaly 직접 분기를 타입 가드/헬퍼로 치환
2026-04-16 16:18:18 +09:00

214 lines
6.5 KiB
TypeScript

/**
* prediction 분석 결과 조회 API (레거시 proxy 경로).
* 새 화면은 @/services/analysisApi 의 /api/analysis/* 를 직접 사용한다.
*/
const API_BASE = import.meta.env.VITE_API_URL ?? '/api';
export interface VesselAnalysisStats {
total: number;
dark: number;
spoofing: number;
critical: number;
high: number;
medium: number;
low: number;
clusterCount: number;
gearGroups: number;
gearCount: number;
}
export interface VesselAnalysisItem {
mmsi: string;
timestamp: string;
classification: {
vesselType: string;
confidence: number;
fishingPct: number;
clusterId: number;
season: string;
};
algorithms: {
location: { zone: string; distToBaselineNm: number };
activity: { state: string; ucafScore: number; ucftScore: number };
darkVessel: { isDark: boolean; gapDurationMin: number };
gpsSpoofing: { spoofingScore: number; bd09OffsetM: number; speedJumpCount: number };
cluster: { clusterId: number; clusterSize: number };
fleetRole: { isLeader: boolean; role: string };
riskScore: { score: number; level: string };
transship: { isSuspect: boolean; pairMmsi: string; durationMin: number };
};
features?: Record<string, number>;
}
export interface VesselAnalysisResponse {
serviceAvailable: boolean;
count: number;
stats: VesselAnalysisStats;
items: VesselAnalysisItem[];
}
export interface GearGroupItem {
groupType: 'FLEET' | 'GEAR_IN_ZONE' | 'GEAR_OUT_ZONE';
groupKey: string;
groupLabel?: string;
subClusterId: number;
snapshotTime: string;
polygon: unknown; // GeoJSON geometry
centerLat: number;
centerLon: number;
areaSqNm: number;
memberCount: number;
members: { mmsi: string; name?: string; lat?: number; lon?: number }[];
color?: string;
resolution: {
status: string;
selectedParentMmsi: string | null;
selectedParentName: string | null;
topScore: number | null;
confidence: number | null;
secondScore: number | null;
scoreMargin: number | null;
decisionSource: string | null;
stableCycles: number | null;
approvedAt: string | null;
manualComment: string | null;
} | null;
candidateCount?: number;
liveTopScore?: number; // correlation_scores 실시간 최고 점수
}
export interface GroupsResponse {
serviceAvailable: boolean;
count: number;
items: GearGroupItem[];
}
async function apiGet<T>(path: string): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, { credentials: 'include' });
if (!res.ok) throw new Error(`API ${res.status}: ${path}`);
return res.json();
}
/**
* @deprecated 백엔드 /api/vessel-analysis 는 빈 스텁 응답만 반환한다.
* vessel_analysis_results 조회는 {@link import('./analysisApi').getAnalysisVessels} /
* {@link import('./analysisApi').getAnalysisStats} 를 사용한다.
* 본 함수는 호출처 제거 후 제거 예정.
*/
export function fetchVesselAnalysis() {
return apiGet<VesselAnalysisResponse>('/vessel-analysis');
}
export function fetchGroups(groupType?: string) {
const qs = groupType ? `?groupType=${groupType}` : '';
return apiGet<GroupsResponse>(`/vessel-analysis/groups${qs}`);
}
export function fetchGroupDetail(groupKey: string) {
return apiGet<unknown>(`/vessel-analysis/groups/${encodeURIComponent(groupKey)}/detail`);
}
export function fetchGroupCorrelations(groupKey: string, minScore?: number) {
const qs = minScore ? `?minScore=${minScore}` : '';
return apiGet<unknown>(`/vessel-analysis/groups/${encodeURIComponent(groupKey)}/correlations${qs}`);
}
// ─── 후보 상세 메트릭 + 모선 확정/제외 ─────────────
export interface CandidateMetricItem {
observedAt: string;
proximityRatio: number | null;
visitScore: number | null;
activitySync: number | null;
dtwSimilarity: number | null;
speedCorrelation: number | null;
headingCoherence: number | null;
driftSimilarity: number | null;
shadowStay: boolean;
shadowReturn: boolean;
gearGroupActiveRatio: number | null;
}
export function fetchCandidateMetrics(groupKey: string, targetMmsi: string) {
return apiGet<{ items: CandidateMetricItem[]; count: number }>(
`/vessel-analysis/groups/${encodeURIComponent(groupKey)}/candidates/${encodeURIComponent(targetMmsi)}/metrics`,
);
}
export async function resolveParent(
groupKey: string,
action: 'confirm' | 'reject',
targetMmsi: string,
comment = '',
): Promise<{ ok: boolean; message?: string }> {
const res = await fetch(`${API_BASE}/vessel-analysis/groups/${encodeURIComponent(groupKey)}/resolve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ action, targetMmsi, comment }),
});
return res.json();
}
// ─── 선박 항적 조회 (signal-batch) ─────────────
/** CompactVesselTrack 응답 */
export interface VesselTrack {
vesselId: string;
shipName: string;
geometry: [number, number][]; // [lon, lat][]
timestamps: string[]; // Unix timestamp (초) 문자열 배열
speeds: number[];
pointCount: number;
}
/**
* 선박 항적 일괄 조회.
* POST /api/prediction/v2/tracks/vessels (백엔드 프록시 → signal-batch)
*/
export async function fetchVesselTracks(
vessels: string[],
startTime: string,
endTime: string,
): Promise<VesselTrack[]> {
const res = await fetch(`${API_BASE}/vessel-analysis/tracks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ startTime, endTime, vessels }),
});
if (!res.ok) throw new Error(`tracks API ${res.status}`);
return res.json();
}
// ─── 필터/유틸 ─────────────────────────────────
/**
* Dark Vessel만 필터.
*/
export function filterDarkVessels(items: VesselAnalysisItem[]): VesselAnalysisItem[] {
return items.filter((i) => i.algorithms.darkVessel.isDark);
}
/**
* GPS 스푸핑 의심 (score >= 0.3).
*/
export function filterSpoofingVessels(items: VesselAnalysisItem[], threshold = 0.3): VesselAnalysisItem[] {
return items.filter((i) => i.algorithms.gpsSpoofing.spoofingScore >= threshold);
}
/**
* 전재 의심.
*/
export function filterTransshipSuspects(items: VesselAnalysisItem[]): VesselAnalysisItem[] {
return items.filter((i) => i.algorithms.transship.isSuspect);
}
/**
* 위험도 레벨 필터.
*/
export function filterByRiskLevel(items: VesselAnalysisItem[], levels: string[]): VesselAnalysisItem[] {
return items.filter((i) => levels.includes(i.algorithms.riskScore.level));
}