/** * 항적 조회 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} 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)]; }