ship-gis/src/api/trackApi.js

214 lines
6.4 KiB
JavaScript
Raw Normal View 히스토리

/**
* 항적 조회 API
* 참조: mda-react-front/src/tracking/services/trackQueryApi.ts
* 참조: mda-react-front/src/api/trackApi.ts
*
* - 엔드포인트: POST /api/v2/tracks/vessels
* - 선박 항적 데이터 조회
* - 응답 데이터 가공 (ProcessedTrack 형태로 변환)
*/
import useShipStore from '../stores/shipStore';
/** API 엔드포인트 (메인 프로젝트와 동일) */
const API_ENDPOINT = '/api/v2/tracks/vessels';
/**
* 항적 데이터 조회
*
* @param {Object} params
* @param {string} params.startTime - 조회 시작 시간 (ISO 8601, e.g. '2026-01-01T00:00:00')
* @param {string} params.endTime - 조회 종료 시간 (ISO 8601)
* @param {Array<{ sigSrcCd: string, targetId: string }>} params.vessels - 조회 대상 선박
* @param {boolean} [params.isIntegration=false] - 통합 조회 여부
* @returns {Promise<Array>} ProcessedTrack 배열
*/
export async function fetchTrackQuery({ startTime, endTime, vessels, isIntegration = false }) {
try {
const body = {
startTime,
endTime,
vessels,
isIntegration: isIntegration ? '1' : '0',
};
const response = await fetch(API_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
// v2 API는 배열을 직접 반환
const rawTracks = Array.isArray(result) ? result : (result?.data || []);
if (!Array.isArray(rawTracks)) {
console.warn('[fetchTrackQuery] Invalid response format:', result);
return [];
}
// 가공: CompactVesselTrack → ProcessedTrack
const processed = rawTracks
.map((raw) => processTrack(raw))
.filter((t) => t !== null);
console.log(`[fetchTrackQuery] Loaded ${processed.length} tracks`);
return processed;
} catch (error) {
console.error('[fetchTrackQuery] Error:', error);
throw error;
}
}
/**
* API 응답 데이터를 ProcessedTrack으로 변환
* 참조: mda-react-front/src/tracking/stores/trackQueryStore.ts - setTracks
*
* @param {Object} raw - API 응답의 개별 항적 데이터
* @returns {Object|null} ProcessedTrack
*/
function processTrack(raw) {
if (!raw || !raw.geometry || raw.geometry.length === 0) return null;
const vesselId = raw.vesselId || `${raw.sigSrcCd}_${raw.targetId}`;
// 타임스탬프를 ms로 변환 (Unix 초 문자열 → ms)
const timestampsMs = (raw.timestamps || []).map((ts) => {
const num = typeof ts === 'string' ? Number(ts) : ts;
// 초 단위면 ms로 변환 (10자리 이하)
return num < 1e12 ? num * 1000 : num;
});
// geometry: [[lon, lat], ...] 형태 확인
const geometry = raw.geometry || [];
// speeds: knots 배열
const speeds = raw.speeds || geometry.map(() => 0);
// 실시간 선박 데이터에서 선명/선종 보강
const liveShip = findLiveShipData(raw.targetId, raw.sigSrcCd);
return {
vesselId,
targetId: raw.targetId || '',
sigSrcCd: raw.sigSrcCd || '',
shipName: raw.shipName || liveShip?.shipName || '',
shipKindCode: raw.shipKindCode || liveShip?.signalKindCode || '000027',
nationalCode: raw.nationalCode || liveShip?.nationalCode || '',
integrationTargetId: raw.integrationTargetId || '',
geometry,
timestampsMs,
speeds,
stats: {
totalDistance: raw.totalDistance || 0,
avgSpeed: raw.avgSpeed || 0,
maxSpeed: raw.maxSpeed || 0,
pointCount: raw.pointCount || geometry.length,
},
};
}
/**
* 실시간 선박 데이터에서 매칭되는 선박 찾기
* @param {string} targetId
* @param {string} sigSrcCd
* @returns {Object|null}
*/
function findLiveShipData(targetId, sigSrcCd) {
if (!targetId) return null;
const features = useShipStore.getState().features;
// 정확한 featureId 매칭
if (sigSrcCd) {
const featureId = sigSrcCd + targetId;
const ship = features.get(featureId);
if (ship) return ship;
}
// featureId로 못 찾으면 originalTargetId로 검색
let found = null;
features.forEach((ship) => {
if (ship.originalTargetId === targetId) {
found = ship;
}
});
return found;
}
/**
* 선박 객체에서 항적 조회용 파라미터 추출
* @param {Object} ship - shipStore의 선박 데이터
* @returns {{ sigSrcCd: string, targetId: string }}
*/
export function extractVesselIdentifier(ship) {
return {
sigSrcCd: ship.signalSourceCode || '',
targetId: ship.originalTargetId || ship.targetId || '',
};
}
/**
* dayjs 없이 Date를 ISO 문자열로 변환 (로컬 시간 기준)
* @param {Date} date
* @returns {string} 'YYYY-MM-DDTHH:mm:ss'
*/
export function toLocalISOString(date) {
const pad = (n) => String(n).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
/**
* 통합선박 TARGET_ID 파싱 장비별 VesselIdentifier 배열
* TARGET_ID 형식: "AIS_VPASS_ENAV_VTSAIS_DMFHF"
* 위치에 00000이면 해당 장비 없음
*
* @param {string} targetId - 통합 TARGET_ID
* @returns {Array<{ sigSrcCd: string, targetId: string }>}
*/
export function parseIntegratedTargetId(targetId) {
if (!targetId) return [];
const parts = targetId.split('_');
// 위치별 장비 매핑: AIS, VPASS, ENAV, VTS_AIS, D_MF_HF
const equipmentMap = [
{ sigSrcCd: '000001', index: 0 }, // AIS
{ sigSrcCd: '000003', index: 1 }, // VPASS
{ sigSrcCd: '000002', index: 2 }, // ENAV
{ sigSrcCd: '000004', index: 3 }, // VTS_AIS
{ sigSrcCd: '000016', index: 4 }, // D_MF_HF
];
const vessels = [];
equipmentMap.forEach(({ sigSrcCd, index }) => {
const id = parts[index];
if (id && id !== '00000' && id !== '0' && id !== '') {
vessels.push({ sigSrcCd, targetId: id });
}
});
return vessels;
}
/**
* 선박 객체에서 항적 조회용 선박 목록 생성
* 통합선박: TARGET_ID 파싱 모든 장비 (레이더 제외)
* 단일선박: 기본 identifier 반환
*
* @param {Object} ship - shipStore 선박 데이터
* @returns {Array<{ sigSrcCd: string, targetId: string }>}
*/
export function buildVesselListForQuery(ship) {
if (ship.integrate && ship.targetId && ship.targetId.includes('_')) {
return parseIntegratedTargetId(ship.targetId);
}
// 단일 장비
return [extractVesselIdentifier(ship)];
}