import axios from 'axios' // API Key를 환경변수에서 로드 (소스코드 노출 방지) const AIS_API_KEY = import.meta.env.VITE_AIS_API_KEY || '' const AIS_BASE_URL = import.meta.env.VITE_AIS_API_URL || 'https://ais-api.spgi.kr/api/v1' // 선박 위치 데이터 타입 export interface VesselPosition { mmsi: string // Maritime Mobile Service Identity imo?: string // International Maritime Organization number shipName: string // 선박명 callSign?: string // 호출부호 shipType: number // 선박 유형 코드 shipTypeText?: string // 선박 유형 (한글) latitude: number // 위도 longitude: number // 경도 speed: number // 속도 (노트) course: number // 침로 (0-359도) heading: number // 선수방위 navStatus: number // 항해 상태 코드 navStatusText?: string // 항해 상태 (한글) timestamp: string // 데이터 수신 시각 destination?: string // 목적지 eta?: string // 예정 도착 시각 draught?: number // 흘수 (미터) length?: number // 전장 (미터) width?: number // 폭 (미터) } // 영역 내 선박 조회 파라미터 export interface BoundingBox { minLat: number // 최소 위도 maxLat: number // 최대 위도 minLng: number // 최소 경도 maxLng: number // 최대 경도 } // 선박 유형 코드 → 텍스트 변환 export function getShipTypeText(code: number): string { const types: { [key: number]: string } = { 0: '미상', 30: '어선', 31: '예인선', 32: '예인선', 33: '준설선', 34: '잠수작업선', 35: '군사작전선', 36: '범선', 37: '레저선', 50: '도선선', 51: '구조선', 52: '예인선', 53: '항만선', 54: '오염방지선', 55: '법집행선', 60: '여객선', 61: '여객선', 62: '여객선', 63: '여객선', 64: '여객선', 65: '여객선', 66: '여객선', 67: '여객선', 68: '여객선', 69: '여객선', 70: '화물선', 71: '화물선', 72: '화물선', 73: '화물선', 74: '화물선', 75: '화물선', 76: '화물선', 77: '화물선', 78: '화물선', 79: '화물선', 80: '유조선', 81: '유조선', 82: '유조선', 83: '유조선', 84: '유조선', 85: '유조선', 86: '유조선', 87: '유조선', 88: '유조선', 89: '유조선', 90: '기타선박', } return types[code] || `선박(${code})` } // 항해 상태 코드 → 텍스트 변환 export function getNavStatusText(code: number): string { const statuses: { [key: number]: string } = { 0: '기관 사용 항해중', 1: '정박중', 2: '조종 불능', 3: '조종 제한', 4: '흘수 제약', 5: '계류중', 6: '좌초', 7: '어로중', 8: '범주 항해중', 9: '예약', 10: '예약', 11: '예약', 12: '예약', 13: '예약', 14: '예약', 15: '미정의', } return statuses[code] || '알 수 없음' } // 1. 특정 선박 위치 조회 (MMSI 기반) export async function getVesselByMMSI(mmsi: string): Promise { try { const response = await axios.get(`${AIS_BASE_URL}/vessel/${mmsi}`, { headers: { 'Authorization': `Bearer ${AIS_API_KEY}`, 'Content-Type': 'application/json', }, }) const data = response.data return { ...data, shipTypeText: getShipTypeText(data.shipType), navStatusText: getNavStatusText(data.navStatus), } } catch (error) { console.error(`선박 조회 오류 (MMSI: ${mmsi}):`, error) return null } } // 2. 영역 내 모든 선박 조회 (Bounding Box) export async function getVesselsInArea(bbox: BoundingBox): Promise { try { const response = await axios.get(`${AIS_BASE_URL}/vessels/area`, { headers: { 'Authorization': `Bearer ${AIS_API_KEY}`, 'Content-Type': 'application/json', }, params: { minLat: bbox.minLat, maxLat: bbox.maxLat, minLng: bbox.minLng, maxLng: bbox.maxLng, }, }) return response.data.map((vessel: Record) => ({ ...vessel, shipTypeText: getShipTypeText(vessel.shipType as number), navStatusText: getNavStatusText(vessel.navStatus as number), })) } catch (error) { console.error('영역 내 선박 조회 오류:', error) return [] } } // 3. 중심점 기반 반경 내 선박 조회 export async function getVesselsNearby( lat: number, lng: number, radiusKm: number = 10 ): Promise { // 대략적인 BBox 계산 (1도 ≈ 111km) const latDelta = radiusKm / 111 const lngDelta = radiusKm / (111 * Math.cos(lat * Math.PI / 180)) const bbox: BoundingBox = { minLat: lat - latDelta, maxLat: lat + latDelta, minLng: lng - lngDelta, maxLng: lng + lngDelta, } return getVesselsInArea(bbox) } // 4. 선박 이동 경로 조회 (Track) export async function getVesselTrack( mmsi: string, startTime: string, endTime: string ): Promise { try { const response = await axios.get(`${AIS_BASE_URL}/vessel/${mmsi}/track`, { headers: { 'Authorization': `Bearer ${AIS_API_KEY}`, 'Content-Type': 'application/json', }, params: { start: startTime, // ISO 8601 format: '2025-02-10T00:00:00Z' end: endTime, }, }) return response.data.map((position: Record) => ({ ...position, shipTypeText: getShipTypeText(position.shipType as number), navStatusText: getNavStatusText(position.navStatus as number), })) } catch (error) { console.error(`선박 경로 조회 오류 (MMSI: ${mmsi}):`, error) return [] } } // 5. 선박 유형별 필터링 export async function getVesselsByType( bbox: BoundingBox, shipTypes: number[] ): Promise { const allVessels = await getVesselsInArea(bbox) return allVessels.filter(v => shipTypes.includes(v.shipType)) } // 6. 위험 선박 필터링 (정박/좌초/조종불능) export async function getDangerousVessels(bbox: BoundingBox): Promise { const allVessels = await getVesselsInArea(bbox) const dangerStatuses = [2, 3, 6] // 조종 불능, 조종 제한, 좌초 return allVessels.filter(v => dangerStatuses.includes(v.navStatus)) } // 7. 유조선만 필터링 (유출 사고 대비) export async function getTankers(bbox: BoundingBox): Promise { const tankerTypes = [80, 81, 82, 83, 84, 85, 86, 87, 88, 89] return getVesselsByType(bbox, tankerTypes) } // 8. 선박 속도로 필터링 (정박 중 판단) export function filterBySpeed( vessels: VesselPosition[], minSpeed: number = 0, maxSpeed: number = 100 ): VesselPosition[] { return vessels.filter(v => v.speed >= minSpeed && v.speed <= maxSpeed) } // 9. 거리 계산 (Haversine formula) export function calculateDistance( lat1: number, lng1: number, lat2: number, lng2: number ): number { const R = 6371 // 지구 반경 (km) const dLat = (lat2 - lat1) * Math.PI / 180 const dLng = (lng2 - lng1) * Math.PI / 180 const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) * Math.sin(dLng / 2) const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) return R * c } // 10. 가장 가까운 선박 찾기 export function findNearestVessel( targetLat: number, targetLng: number, vessels: VesselPosition[] ): VesselPosition | null { if (vessels.length === 0) return null let nearest = vessels[0] let minDistance = calculateDistance(targetLat, targetLng, nearest.latitude, nearest.longitude) vessels.forEach(vessel => { const distance = calculateDistance(targetLat, targetLng, vessel.latitude, vessel.longitude) if (distance < minDistance) { minDistance = distance nearest = vessel } }) return nearest } // 11. Mock 데이터 (테스트용) export function getMockVessels(): VesselPosition[] { return [ { mmsi: '440123456', imo: 'IMO9123456', shipName: '씨프린스호', callSign: 'HLCS', shipType: 80, shipTypeText: '유조선', latitude: 34.5, longitude: 127.8, speed: 0.2, course: 135, heading: 140, navStatus: 1, navStatusText: '정박중', timestamp: new Date().toISOString(), destination: 'YEOSU', draught: 12.5, length: 180, width: 32, }, { mmsi: '440234567', shipName: '여수경비정', shipType: 55, shipTypeText: '법집행선', latitude: 34.52, longitude: 127.82, speed: 12.5, course: 270, heading: 268, navStatus: 0, navStatusText: '기관 사용 항해중', timestamp: new Date().toISOString(), destination: 'PATROL', length: 45, width: 8, }, ] }