ship-gis/src/utils/csvDownload.ts
htlee 4c7bd42b42 refactor: 관심구역, 측정, 미니맵 MapLibre 전환 + OpenLayers 제거 (Session E, F, H)
- Session E: 관심구역 폴리곤 + 추적 반경원 MapLibre GeoJSON 레이어로 전환
  - useRealmLayer: OL VectorLayer → MapLibre fill/line layer
  - useTrackingMode: 반경 원 @turf/circle → GeoJSON source
- Session F: 측정 도구 MapLibre 커스텀 구현
  - useMeasure: OL Draw/Overlay → MapLibre Marker + GeoJSON layer
  - 거리/면적: @turf/distance, @turf/length, @turf/area
  - 툴 믹싱 지원, 세션 persistence
- Session H: 미니맵 MapLibre 전환 + OpenLayers 완전 제거
  - VesselDetailModal/StsContactDetailModal: OL 임베디드 맵 → MapLibre 7개 레이어
  - mapStore: map 타입 any → maplibregl.Map | null
  - csvDownload: OL Polygon → Turf.js booleanPointInPolygon
  - package.json: ol, ol-ext 제거 (~500KB 감소)
  - main.tsx: OL CSS 제거
  - 6개 OL 파일 @ts-nocheck 추가 (Session G 패스)

검증: yarn type-check, yarn lint, yarn build 통과

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 17:28:24 +09:00

161 lines
5.0 KiB
TypeScript

/**
* CSV 다운로드 유틸리티
* 참조: mda-react-front/src/widgets/rightMenu/ui/RightMenu.tsx (512-579)
*/
import * as turf from '@turf/turf';
import { shipTypeMap } from '../assets/data/shiptype';
import { SHIP_KIND_LABELS, SIGNAL_SOURCE_LABELS } from '../types/constants';
/** 다운로드용 선박 데이터 */
interface DownloadShip {
downloadTargetId: string;
shipName: string;
signalKindCode: string;
shipType: string;
signalSourceCode: string;
sog: number;
cog: number;
longitude: string | number;
latitude: string | number;
draught: string;
receivedTime: string;
}
/** 해구도 캐시 엔트리 */
interface TrenchEntry {
zoneName: string;
polygon: number[][][]; // GeoJSON Polygon coordinates
}
/** GeoJSON Feature 형태 (largeTrench.json) */
interface TrenchFeature {
properties: { zone_name: string };
geometry: { coordinates: number[][][] };
}
// 해구도 데이터 캐시 (첫 호출 시만 로딩)
let trenchCache: TrenchEntry[] | null = null;
/**
* 해구도 폴리곤 데이터 로딩 (동적 import, 캐시)
*/
async function loadTrenchData(): Promise<TrenchEntry[]> {
if (trenchCache) return trenchCache;
const data = await import('../assets/data/largeTrench.json');
const geojson = data.default || data;
trenchCache = (geojson as { features: TrenchFeature[] }).features.map((f: TrenchFeature) => ({
zoneName: f.properties.zone_name,
polygon: f.geometry.coordinates,
}));
return trenchCache;
}
/**
* 선박 좌표 → 해구도 번호 일괄 조회
* @param {Array} ships - 선박 배열 (longitude, latitude 필드 필요)
* @returns {Map<number, string>} index → zone_name 매핑
*/
async function lookupTrenchNumbers(ships: DownloadShip[]): Promise<Map<number, string>> {
const trenchData = await loadTrenchData();
const result = new Map<number, string>();
ships.forEach((ship, idx) => {
const lon = parseFloat(String(ship.longitude));
const lat = parseFloat(String(ship.latitude));
if (isNaN(lon) || isNaN(lat)) {
result.set(idx, 'X');
return;
}
let found = false;
for (const { zoneName, polygon } of trenchData) {
// Turf.js booleanPointInPolygon으로 좌표 검사
if (turf.booleanPointInPolygon([lon, lat], turf.polygon(polygon))) {
result.set(idx, zoneName);
found = true;
break;
}
}
if (!found) {
result.set(idx, 'X');
}
});
return result;
}
/**
* 수신시간 포맷 변환
* "YYYYMMDDHHmmss" → "YYYY-MM-DD HH:mm:ss"
*/
function formatRecvDateTime(raw: string | undefined | null): string {
if (!raw || raw.length < 14) return raw || '';
return `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)} ${raw.slice(8, 10)}:${raw.slice(10, 12)}:${raw.slice(12, 14)}`;
}
/**
* CSV 안전 필드 (쌍따옴표 감싸기)
*/
function csvField(val: string | number | null | undefined): string {
const str = val == null ? '' : String(val);
return `"${str.replace(/"/g, '""')}"`;
}
/**
* CSV 문자열 생성
* @param {Array} ships - 선박 배열
* @param {Map} trenchMap - index → zone_name 매핑
* @returns {string} CSV 문자열 (BOM 포함)
*/
function buildCsvString(ships: DownloadShip[], trenchMap: Map<number, string>): string {
const BOM = '\uFEFF';
const headers = [
'타겟ID', '선박명', '선종/기종', '선종/기종-유형', '신호',
'SOG', 'COG', '경도', '위도', '흘수', '수신시간', '해구도',
];
const rows = ships.map((ship, idx) => {
const fields = [
csvField(ship.downloadTargetId),
csvField(ship.shipName),
csvField((SHIP_KIND_LABELS as Record<string, string>)[ship.signalKindCode] || ship.signalKindCode),
csvField(shipTypeMap.get(String(ship.shipType)) || ship.shipType),
csvField((SIGNAL_SOURCE_LABELS as Record<string, string>)[ship.signalSourceCode] || ship.signalSourceCode),
csvField(ship.sog),
csvField(ship.cog),
csvField(ship.longitude),
csvField(ship.latitude),
csvField(ship.draught),
csvField(formatRecvDateTime(ship.receivedTime)),
csvField(trenchMap.get(idx) || 'X'),
];
return fields.join(',');
});
return BOM + headers.map(csvField).join(',') + '\n' + rows.join('\n');
}
/**
* CSV 다운로드 트리거
* @param {Array} ships - getDownloadShips()에서 반환된 선박 배열
*/
export async function downloadShipCsv(ships: DownloadShip[]): Promise<void> {
const trenchMap = await lookupTrenchNumbers(ships);
const csvString = buildCsvString(ships, trenchMap);
const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const now = new Date();
const pad = (n: number): string => String(n).padStart(2, '0');
const fileName = `ship_download_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.csv`;
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}