wing-ops/frontend/src/tabs/incidents/services/incidentsApi.ts
jeonghyo.k 2640d882da feat(incidents): 이미지 분석 연동 강화 및 사고 팝업 리뉴얼
- 사고별 이미지 분석 API 및 항공 미디어 조회 연동
- 사고 마커 팝업 디자인 개선, 필터링된 사고만 지도 표시
- 이미지 분석 시 사고명 파라미터 지원, 기본 예측시간 6시간으로 변경
- 유출량 정밀도 NUMERIC(14,10) 확대 (migration 031)
- OpenDrift 유종 매핑 수정 (원유, 등유)
2026-04-13 16:41:56 +09:00

245 lines
6.3 KiB
TypeScript

import { api } from '@common/services/api';
// ============================================================
// 백엔드 API 응답 타입
// ============================================================
export interface IncidentListItem {
acdntSn: number;
acdntCd: string;
acdntNm: string;
acdntTpCd: string;
acdntSttsCd: string;
lat: number;
lng: number;
locDc: string;
occrnDtm: string;
regionNm: string;
officeNm: string;
svrtCd: string | null;
vesselTp: string | null;
phaseCd: string;
analystNm: string | null;
oilTpCd: string | null;
spilQty: number | null;
spilUnitCd: string | null;
fcstHr: number | null;
hasPredCompleted: boolean;
mediaCnt: number;
hasImgAnalysis: boolean;
}
export interface PredExecItem {
predExecSn: number;
algoCd: string;
execSttsCd: string;
bgngDtm: string | null;
cmplDtm: string | null;
reqdSec: number | null;
}
export interface WeatherInfo {
locNm: string;
obsDtm: string;
icon: string;
temp: string;
weatherDc: string;
wind: string;
wave: string;
humid: string;
vis: string;
sst: string;
tide: string;
highTide: string;
lowTide: string;
forecast: Array<{ hour: string; icon: string; temp: string }>;
impactDc: string;
}
export interface MediaInfo {
photoCnt: number;
videoCnt: number;
satCnt: number;
cctvCnt: number;
photoMeta: Record<string, unknown> | null;
droneMeta: Record<string, unknown> | null;
satMeta: Record<string, unknown> | null;
cctvMeta: Record<string, unknown> | null;
}
export interface IncidentDetail extends IncidentListItem {
predictions: PredExecItem[];
weather: WeatherInfo | null;
media: MediaInfo | null;
}
// ============================================================
// 프론트 호환 타입
// ============================================================
export interface IncidentCompat {
id: string;
name: string;
status: 'active' | 'investigating' | 'closed';
date: string;
time: string;
region: string;
office: string;
location: { lat: number; lon: number };
causeType?: string;
oilType?: string;
prediction?: string;
vesselName?: string;
mediaCount?: number;
hasImgAnalysis?: boolean;
}
function toCompat(item: IncidentListItem): IncidentCompat {
const dt = new Date(item.occrnDtm);
const statusMap: Record<string, 'active' | 'investigating' | 'closed'> = {
ACTIVE: 'active',
INVESTIGATING: 'investigating',
CLOSED: 'closed',
};
return {
id: String(item.acdntSn),
name: item.acdntNm,
status: statusMap[item.acdntSttsCd] ?? 'active',
date: dt.toISOString().slice(0, 10),
time: dt.toTimeString().slice(0, 5),
region: item.regionNm,
office: item.officeNm,
location: { lat: item.lat, lon: item.lng },
causeType: item.acdntTpCd,
oilType: item.oilTpCd ?? undefined,
prediction: item.hasPredCompleted ? '예측완료' : undefined,
mediaCount: item.mediaCnt,
hasImgAnalysis: item.hasImgAnalysis || undefined,
};
}
// ============================================================
// API 호출 함수
// ============================================================
export async function fetchIncidentsRaw(): Promise<IncidentListItem[]> {
const { data } = await api.get<IncidentListItem[]>('/incidents');
return data;
}
export async function fetchIncidents(filters?: {
status?: string;
region?: string;
search?: string;
startDate?: string;
endDate?: string;
}): Promise<IncidentCompat[]> {
const params = new URLSearchParams();
if (filters?.status) params.set('status', filters.status);
if (filters?.region) params.set('region', filters.region);
if (filters?.search) params.set('search', filters.search);
if (filters?.startDate) params.set('startDate', filters.startDate);
if (filters?.endDate) params.set('endDate', filters.endDate);
const query = params.toString();
const url = query ? `/incidents?${query}` : '/incidents';
const { data } = await api.get<IncidentListItem[]>(url);
return data.map(toCompat);
}
export async function fetchIncidentDetail(sn: number): Promise<IncidentDetail> {
const { data } = await api.get<IncidentDetail>(`/incidents/${sn}`);
return data;
}
export async function fetchIncidentWeather(sn: number): Promise<WeatherInfo | null> {
try {
const { data } = await api.get<WeatherInfo>(`/incidents/${sn}/weather`);
return data;
} catch {
return null;
}
}
export async function fetchIncidentMedia(sn: number): Promise<MediaInfo | null> {
try {
const { data } = await api.get<MediaInfo>(`/incidents/${sn}/media`);
return data;
} catch {
return null;
}
}
export async function fetchIncidentPredictions(sn: number): Promise<PredExecItem[]> {
const { data } = await api.get<PredExecItem[]>(`/incidents/${sn}/predictions`);
return data;
}
export interface NearbyOrgItem {
orgSn: number;
orgTp: string;
jrsdNm: string;
areaNm: string;
orgNm: string;
addr: string;
tel: string;
lat: number;
lng: number;
pinSize: string;
vesselCnt: number;
skimmerCnt: number;
pumpCnt: number;
vehicleCnt: number;
sprayerCnt: number;
totalAssets: number;
distanceNm: number;
}
export async function fetchNearbyOrgs(
lat: number,
lng: number,
radiusNm: number,
): Promise<NearbyOrgItem[]> {
const { data } = await api.get<NearbyOrgItem[]>('/assets/orgs/nearby', {
params: { lat, lng, radius: radiusNm },
});
return data;
}
// ============================================================
// 사고 관련 이미지 (AERIAL_MEDIA)
// ============================================================
export interface AerialMediaItem {
aerialMediaSn: number;
acdntSn: number | null;
fileNm: string;
orgnlNm: string | null;
filePath: string | null;
lon: number | null;
lat: number | null;
locDc: string | null;
equipTpCd: string | null;
equipNm: string | null;
mediaTpCd: string | null;
takngDtm: string | null;
fileSz: string | null;
resolution: string | null;
regDtm: string;
}
export async function fetchIncidentAerialMedia(acdntSn: number): Promise<AerialMediaItem[]> {
try {
const { data } = await api.get<AerialMediaItem[]>('/aerial/media', {
params: { acdntSn },
});
return data;
} catch {
return [];
}
}
export function getMediaImageUrl(aerialMediaSn: number): string {
return `/api/aerial/media/${aerialMediaSn}/download`;
}