/** * 항적분석 검색 결과 CSV 내보내기 * BOM + UTF-8 인코딩 (한글 엑셀 호환) */ import { getShipKindName, getSignalSourceName } from '../../tracking/types/trackQuery.types'; import { getCountryIsoCode } from '../../replay/components/VesselListManager/utils/countryCodeUtils'; function formatTimestamp(ms) { if (!ms) return ''; const d = new Date(ms); const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } function formatPosition(pos) { if (!pos || pos.length < 2) return ''; const lon = pos[0]; const lat = pos[1]; const latDir = lat >= 0 ? 'N' : 'S'; const lonDir = lon >= 0 ? 'E' : 'W'; return `${Math.abs(lat).toFixed(4)}\u00B0${latDir} ${Math.abs(lon).toFixed(4)}\u00B0${lonDir}`; } function escapeCsvField(value) { const str = String(value ?? ''); if (str.includes(',') || str.includes('"') || str.includes('\n')) { return `"${str.replace(/"/g, '""')}"`; } return str; } /** * 검색 결과를 CSV로 내보내기 * * @param {Array} tracks ProcessedTrack 배열 * @param {Object} hitDetails { vesselId: [{ polygonId, entryTimestamp, exitTimestamp }] } * @param {Array} zones 구역 배열 */ export function exportSearchResultToCSV(tracks, hitDetails, zones) { const zoneNames = zones.map((z) => z.name); // 헤더 구성 const baseHeaders = [ '신호원', '식별번호', '선박명', '선종', '국적', '포인트수', '이동거리(NM)', '평균속도(kn)', '최대속도(kn)', ]; const zoneHeaders = []; zoneNames.forEach((name) => { zoneHeaders.push( `구역${name}_진입시각`, `구역${name}_진입위치`, `구역${name}_진출시각`, `구역${name}_진출위치`, ); }); const headers = [...baseHeaders, ...zoneHeaders]; // 데이터 행 생성 const rows = tracks.map((track) => { const baseRow = [ getSignalSourceName(track.sigSrcCd), track.targetId || '', track.shipName || '', getShipKindName(track.shipKindCode), track.nationalCode ? getCountryIsoCode(track.nationalCode) : '', track.stats?.pointCount ?? track.geometry.length, track.stats?.totalDistance != null ? track.stats.totalDistance.toFixed(2) : '', track.stats?.avgSpeed != null ? track.stats.avgSpeed.toFixed(1) : '', track.stats?.maxSpeed != null ? track.stats.maxSpeed.toFixed(1) : '', ]; const hits = hitDetails[track.vesselId] || []; const zoneData = []; zones.forEach((zone) => { const hit = hits.find((h) => h.polygonId === zone.id); if (hit) { zoneData.push( formatTimestamp(hit.entryTimestamp), formatPosition(hit.entryPosition), formatTimestamp(hit.exitTimestamp), formatPosition(hit.exitPosition), ); } else { zoneData.push('', '', '', ''); } }); return [...baseRow, ...zoneData]; }); // CSV 문자열 생성 const csvLines = [ headers.map(escapeCsvField).join(','), ...rows.map((row) => row.map(escapeCsvField).join(',')), ]; const csvContent = csvLines.join('\n'); // BOM + UTF-8 Blob 생성 및 다운로드 const BOM = '\uFEFF'; const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const now = new Date(); const pad = (n) => String(n).padStart(2, '0'); const filename = `항적분석_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}.csv`; const link = document.createElement('a'); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); }