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 직접 분기를 타입 가드/헬퍼로 치환
214 lines
6.5 KiB
TypeScript
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));
|
|
}
|