wing-ops/frontend/src/tabs/prediction/services/predictionApi.ts
jeonghyo.k e285f2330f feat(prediction): 역추적 분석 엔진 및 동적 파라미터 입력 기능 구현
- 백엔드: backtrackAnalysisService 신규 개발
  * AIS 기반 선박 항적 API 연동 및 공간 조회
  * 공간(40%)/시간(25%)/행동(20%)/선박유형(15%) 가중치 위험도 점수 산정
  * 상위 5척 리플레이 데이터 및 충돌 이벤트 생성
  * Python 서버 미연동 시 폴백 메커니즘 제공
- 백엔드: 역추적 생성 시 동기 분석 → BacktrackResult 즉시 반환
- 프론트엔드: 모달에서 유출 시각/분석 범위/탐색 반경 직접 입력 가능
- 프론트엔드: 리플레이 바에 실제 분석 시간 범위 동적 표시
- DB: AIS_TRACK 테이블 신규 생성 (선박 항적 이력 + GIS 인덱스)
2026-03-27 14:57:00 +09:00

322 lines
8.2 KiB
TypeScript

import { api } from '@common/services/api';
export interface PredictionAnalysis {
acdntSn: number;
acdntNm: string;
occurredAt: string;
analysisDate: string;
requestor: string;
duration: string;
oilType: string;
volume: number | null;
location: string;
lat: number | null;
lon: number | null;
kospsStatus: string;
poseidonStatus: string;
opendriftStatus: string;
backtrackStatus: string;
analyst: string;
officeName: string;
acdntSttsCd: string;
predRunSn: number | null;
runDtm: string | null;
}
export interface PredictionDetail {
acdnt: {
acdntSn: number;
acdntNm: string;
occurredAt: string;
lat: number | null;
lon: number | null;
location: string;
analyst: string;
officeName: string;
};
spill: {
oilType: string;
volume: number | null;
unit: string;
fcstHr: number | null;
} | null;
vessels: Array<{
vesselInfoSn: number;
imoNo: string;
vesselNm: string;
vesselTp: string;
loaM: number | null;
breadthM: number | null;
draftM: number | null;
gt: number | null;
dwt: number | null;
builtYr: number | null;
flagCd: string;
callsign: string;
engineDc: string;
insuranceData: unknown;
}>;
weather: Array<{
obsDtm: string;
locNm: string;
temp: string;
weatherDc: string;
wind: string;
wave: string;
humid: string;
vis: string;
sst: string;
}>;
}
export interface BacktrackResult {
backtrackSn: number;
acdntSn: number;
estSpilDtm: string | null;
anlysRange: string | null;
lon: number | null;
lat: number | null;
srchRadiusNm: number | null;
totalVessels: number | null;
execSttsCd: string;
rsltData: Record<string, unknown> | null;
}
export const fetchPredictionAnalyses = async (params?: {
search?: string;
acdntSn?: number;
}): Promise<PredictionAnalysis[]> => {
const response = await api.get<PredictionAnalysis[]>('/prediction/analyses', { params });
return response.data;
};
export const fetchPredictionDetail = async (acdntSn: number): Promise<PredictionDetail> => {
const response = await api.get<PredictionDetail>(`/prediction/analyses/${acdntSn}`);
return response.data;
};
export const fetchBacktrack = async (sn: number): Promise<BacktrackResult> => {
const response = await api.get<BacktrackResult>(`/prediction/backtrack/${sn}`);
return response.data;
};
export const fetchBacktrackByAcdnt = async (
acdntSn: number,
): Promise<BacktrackResult | null> => {
const response = await api.get<BacktrackResult[]>('/prediction/backtrack', {
params: { acdntSn },
});
return response.data.length > 0 ? response.data[0] : null;
};
export const createBacktrack = async (input: {
acdntSn: number;
lon: number;
lat: number;
srchRadiusNm?: number;
anlysRange?: string;
estSpilDtm?: string;
}): Promise<BacktrackResult> => {
const response = await api.post<BacktrackResult>('/prediction/backtrack', input);
return response.data;
};
// ============================================================
// 확산 예측 시뮬레이션 (OpenDrift 연동)
// ============================================================
export interface SimulationRunResponse {
success: boolean;
execSn: number; // 하위 호환 유지 (첫 번째 모델의 execSn)
execSns: Array<{ model: string; execSn: number }>;
acdntSn: number | null;
status: 'RUNNING';
}
export interface WindPoint {
lat: number;
lon: number;
wind_speed: number;
wind_direction: number;
}
export interface HydrGrid {
lonInterval: number[];
boundLonLat: { top: number; bottom: number; left: number; right: number };
rows: number;
cols: number;
latInterval: number[];
}
export interface HydrDataStep {
value: [number[][], number[][]]; // [u_2d, v_2d]
grid: HydrGrid;
}
export interface CenterPoint {
lat: number;
lon: number;
time: number;
model?: string;
}
export interface OilParticle {
lat: number;
lon: number;
time: number;
particle?: number;
stranded?: 0 | 1;
model?: string;
}
export interface SimulationSummary {
remainingVolume: number;
weatheredVolume: number;
evaporationVolume: number;
dispersionVolume: number;
pollutionArea: number;
beachedVolume: number;
pollutionCoastLength: number;
}
export interface SimulationStatusResponse {
status: 'PENDING' | 'RUNNING' | 'DONE' | 'ERROR';
progress?: number;
trajectory?: OilParticle[];
summary?: SimulationSummary;
centerPoints?: CenterPoint[];
windData?: WindPoint[][];
hydrData?: (HydrDataStep | null)[];
error?: string;
}
export interface RunModelSyncResult {
model: string;
execSn: number;
status: 'DONE' | 'ERROR';
trajectory?: OilParticle[];
summary?: SimulationSummary;
stepSummaries?: SimulationSummary[];
centerPoints?: CenterPoint[];
windData?: WindPoint[][];
hydrData?: (HydrDataStep | null)[];
error?: string;
}
export interface RunModelSyncResponse {
success: boolean;
acdntSn: number | null;
execSns: Array<{ model: string; execSn: number }>;
results: RunModelSyncResult[];
}
export interface TrajectoryResponse {
trajectory: OilParticle[] | null;
summary: SimulationSummary | null;
centerPoints?: CenterPoint[];
windDataByModel?: Record<string, WindPoint[][]>;
hydrDataByModel?: Record<string, (HydrDataStep | null)[]>;
summaryByModel?: Record<string, SimulationSummary>;
stepSummariesByModel?: Record<string, SimulationSummary[]>;
}
export const fetchAnalysisTrajectory = async (acdntSn: number, predRunSn?: number): Promise<TrajectoryResponse> => {
const response = await api.get<TrajectoryResponse>(
`/prediction/analyses/${acdntSn}/trajectory`,
predRunSn != null ? { params: { predRunSn } } : undefined,
);
return response.data;
};
export interface SensitiveResourceCategory {
category: string;
count: number;
totalArea: number | null;
}
export const fetchSensitiveResources = async (
acdntSn: number,
): Promise<SensitiveResourceCategory[]> => {
const response = await api.get<SensitiveResourceCategory[]>(
`/prediction/analyses/${acdntSn}/sensitive-resources`,
);
return response.data;
};
export interface SensitiveResourceFeature {
type: 'Feature';
geometry: { type: string; coordinates: unknown };
properties: {
srId: number;
category: string;
[key: string]: unknown;
};
}
export interface SensitiveResourceFeatureCollection {
type: 'FeatureCollection';
features: SensitiveResourceFeature[];
}
export const fetchSensitiveResourcesGeojson = async (
acdntSn: number,
): Promise<SensitiveResourceFeatureCollection> => {
const response = await api.get<SensitiveResourceFeatureCollection>(
`/prediction/analyses/${acdntSn}/sensitive-resources/geojson`,
);
return response.data;
};
export interface SpreadParticlesGeojson {
type: 'FeatureCollection';
features: Array<{
type: 'Feature';
geometry: { type: 'Point'; coordinates: [number, number] };
properties: { model: string; time: number; stranded: 0 | 1; isLastStep: boolean };
}>;
maxStep: number;
}
export const fetchPredictionParticlesGeojson = async (
acdntSn: number,
): Promise<SpreadParticlesGeojson> => {
const response = await api.get<SpreadParticlesGeojson>(
`/prediction/analyses/${acdntSn}/spread-particles`,
);
return response.data;
};
export const fetchSensitivityEvaluationGeojson = async (
acdntSn: number,
): Promise<{ type: 'FeatureCollection'; features: unknown[] }> => {
const response = await api.get<{ type: 'FeatureCollection'; features: unknown[] }>(
`/prediction/analyses/${acdntSn}/sensitivity-evaluation`,
);
return response.data;
};
// ============================================================
// 이미지 업로드 분석
// ============================================================
export interface ImageAnalyzeResult {
acdntSn: number;
lat: number;
lon: number;
oilType: string;
area: number;
volume: number;
fileId: string;
occurredAt: string;
}
export const analyzeImage = async (file: File): Promise<ImageAnalyzeResult> => {
const formData = new FormData();
formData.append('image', file);
const response = await api.post<ImageAnalyzeResult>('/prediction/image-analyze', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 330_000,
});
return response.data;
};