- authStore: 메인 프로젝트 세션 쿠키 기반 인증 상태 관리 - fetchWithAuth: 401 응답 시 메인 프로젝트 로그인 페이지 리다이렉트 - SessionGuard: 앱 진입 시 세션 유효성 검증 래퍼 컴포넌트 - 기존 API 모듈 fetch → fetchWithAuth 전환 - 환경변수에 VITE_MAIN_APP_URL, VITE_DEV_SKIP_AUTH 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
214 lines
6.5 KiB
JavaScript
214 lines
6.5 KiB
JavaScript
/**
|
|
* 항적 조회 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';
|
|
import { fetchWithAuth } from './fetchWithAuth';
|
|
|
|
/** 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 fetchWithAuth(API_ENDPOINT, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
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)];
|
|
}
|