From cbdd9fef0fff07f2c221b6897d0a320d551e0f07 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 12 Nov 2025 15:46:24 +0900 Subject: [PATCH] =?UTF-8?q?http=20polling=20=EC=A3=BC=EA=B8=B0=EB=A5=BC=20?= =?UTF-8?q?5=EC=B4=88=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/widgets/MapTestWidgetV2.tsx | 862 ++++++++++-------- 1 file changed, 492 insertions(+), 370 deletions(-) diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx index 5eeeca12..1ea8685c 100644 --- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx +++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ "use client"; import React, { useEffect, useState, useCallback, useMemo } from "react"; @@ -11,12 +12,13 @@ import "leaflet/dist/leaflet.css"; // Leaflet 아이콘 경로 설정 (엑박 방지) if (typeof window !== "undefined") { - const L = require("leaflet"); - delete (L.Icon.Default.prototype as any)._getIconUrl; - L.Icon.Default.mergeOptions({ - iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png", - iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png", - shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png", + import("leaflet").then((L) => { + delete (L.Icon.Default.prototype as any)._getIconUrl; + L.Icon.Default.mergeOptions({ + iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png", + iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png", + shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png", + }); }); } @@ -46,6 +48,9 @@ interface MarkerData { description?: string; source?: string; // 어느 데이터 소스에서 왔는지 color?: string; // 마커 색상 + heading?: number; // 진행 방향 (0-360도, 0=북쪽) + prevLat?: number; // 이전 위도 (방향 계산용) + prevLng?: number; // 이전 경도 (방향 계산용) } interface PolygonData { @@ -61,6 +66,7 @@ interface PolygonData { export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const [markers, setMarkers] = useState([]); + const [prevMarkers, setPrevMarkers] = useState([]); // 이전 마커 위치 저장 const [polygons, setPolygons] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -75,10 +81,23 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { return element?.dataSources || element?.chartConfig?.dataSources; }, [element?.dataSources, element?.chartConfig?.dataSources]); + // 두 좌표 사이의 방향 계산 (0-360도, 0=북쪽) + const calculateHeading = useCallback((lat1: number, lng1: number, lat2: number, lng2: number): number => { + const dLng = (lng2 - lng1) * (Math.PI / 180); + const lat1Rad = lat1 * (Math.PI / 180); + const lat2Rad = lat2 * (Math.PI / 180); + + const y = Math.sin(dLng) * Math.cos(lat2Rad); + const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng); + + let heading = Math.atan2(y, x) * (180 / Math.PI); + heading = (heading + 360) % 360; // 0-360 범위로 정규화 + + return heading; + }, []); + // 다중 데이터 소스 로딩 const loadMultipleDataSources = useCallback(async () => { - const dataSourcesList = dataSources; - if (!dataSources || dataSources.length === 0) { // // console.log("⚠️ 데이터 소스가 없습니다."); return; @@ -94,38 +113,38 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { dataSources.map(async (source) => { try { // // console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); - + if (source.type === "api") { return await loadRestApiData(source); } else if (source.type === "database") { return await loadDatabaseData(source); } - + return { markers: [], polygons: [] }; } catch (err: any) { console.error(`❌ 데이터 소스 "${source.name || source.id}" 로딩 실패:`, err); return { markers: [], polygons: [] }; } - }) + }), ); // 성공한 데이터만 병합 const allMarkers: MarkerData[] = []; const allPolygons: PolygonData[] = []; - + results.forEach((result, index) => { // // console.log(`🔍 결과 ${index}:`, result); - + if (result.status === "fulfilled" && result.value) { const value = result.value as { markers: MarkerData[]; polygons: PolygonData[] }; // // console.log(`✅ 데이터 소스 ${index} 성공:`, value); - + // 마커 병합 if (value.markers && Array.isArray(value.markers)) { // // console.log(` → 마커 ${value.markers.length}개 추가`); allMarkers.push(...value.markers); } - + // 폴리곤 병합 if (value.polygons && Array.isArray(value.polygons)) { // // console.log(` → 폴리곤 ${value.polygons.length}개 추가`); @@ -139,8 +158,31 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // // console.log(`✅ 총 ${allMarkers.length}개의 마커, ${allPolygons.length}개의 폴리곤 로딩 완료`); // // console.log("📍 최종 마커 데이터:", allMarkers); // // console.log("🔷 최종 폴리곤 데이터:", allPolygons); - - setMarkers(allMarkers); + + // 이전 마커 위치와 비교하여 진행 방향 계산 + const markersWithHeading = allMarkers.map((marker) => { + const prevMarker = prevMarkers.find((pm) => pm.id === marker.id); + + if (prevMarker && (prevMarker.lat !== marker.lat || prevMarker.lng !== marker.lng)) { + // 이동했으면 방향 계산 + const heading = calculateHeading(prevMarker.lat, prevMarker.lng, marker.lat, marker.lng); + return { + ...marker, + heading, + prevLat: prevMarker.lat, + prevLng: prevMarker.lng, + }; + } + + // 이동하지 않았거나 이전 데이터가 없으면 기존 heading 유지 (또는 0) + return { + ...marker, + heading: marker.heading || prevMarker?.heading || 0, + }; + }); + + setPrevMarkers(markersWithHeading); // 다음 비교를 위해 현재 위치 저장 + setMarkers(markersWithHeading); setPolygons(allPolygons); setLastRefreshTime(new Date()); } catch (err: any) { @@ -149,7 +191,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } finally { setLoading(false); } - }, [dataSources]); + }, [dataSources, prevMarkers, calculateHeading]); // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { @@ -158,9 +200,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { }, [loadMultipleDataSources]); // REST API 데이터 로딩 - const loadRestApiData = async (source: ChartDataSource): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => { + const loadRestApiData = async ( + source: ChartDataSource, + ): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => { // // console.log(`🌐 REST API 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType); - + if (!source.endpoint) { throw new Error("API endpoint가 없습니다."); } @@ -205,16 +249,16 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } const result = await response.json(); - + if (!result.success) { throw new Error(result.message || "API 호출 실패"); } // 데이터 추출 및 파싱 let data = result.data; - + // 텍스트 형식 데이터 체크 (기상청 API 등) - if (data && typeof data === 'object' && data.text && typeof data.text === 'string') { + if (data && typeof data === "object" && data.text && typeof data.text === "string") { // // console.log("📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); const parsedData = parseTextData(data.text); if (parsedData.length > 0) { @@ -224,7 +268,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { return convertToMapData(mappedData, source.name || source.id || "API", source.mapDisplayType, source); } } - + // JSON Path로 데이터 추출 if (source.jsonPath) { const pathParts = source.jsonPath.split("."); @@ -234,18 +278,20 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } const rows = Array.isArray(data) ? data : [data]; - + // 컬럼 매핑 적용 const mappedRows = applyColumnMapping(rows, source.columnMapping); - + // 마커와 폴리곤으로 변환 (mapDisplayType + dataSource 전달) return convertToMapData(mappedRows, source.name || source.id || "API", source.mapDisplayType, source); }; // Database 데이터 로딩 - const loadDatabaseData = async (source: ChartDataSource): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => { + const loadDatabaseData = async ( + source: ChartDataSource, + ): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => { // // console.log(`💾 Database 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType); - + if (!source.query) { throw new Error("SQL 쿼리가 없습니다."); } @@ -257,9 +303,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection"); const externalResult = await ExternalDbConnectionAPI.executeQuery( parseInt(source.externalConnectionId), - source.query + source.query, ); - + if (!externalResult.success || !externalResult.data) { throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패"); } @@ -267,19 +313,19 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const resultData = externalResult.data as unknown as { rows: Record[]; }; - + rows = resultData.rows; } else { // 현재 DB const { dashboardApi } = await import("@/lib/api/dashboard"); const result = await dashboardApi.executeQuery(source.query); - + rows = result.rows; } - + // 컬럼 매핑 적용 const mappedRows = applyColumnMapping(rows, source.columnMapping); - + // 마커와 폴리곤으로 변환 (mapDisplayType + dataSource 전달) return convertToMapData(mappedRows, source.name || source.id || "Database", source.mapDisplayType, source); }; @@ -290,7 +336,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // // console.log(" 📄 XML 파싱 시작"); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); - + const records = xmlDoc.getElementsByTagName("record"); const results: any[] = []; @@ -318,56 +364,53 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const parseTextData = (text: string): any[] => { try { // // console.log(" 🔍 원본 텍스트 (처음 500자):", text.substring(0, 500)); - + // XML 형식 감지 if (text.trim().startsWith("")) { // // console.log(" 📄 XML 형식 데이터 감지"); return parseXmlData(text); } - - const lines = text.split('\n').filter(line => { + + const lines = text.split("\n").filter((line) => { const trimmed = line.trim(); - return trimmed && - !trimmed.startsWith('#') && - !trimmed.startsWith('=') && - !trimmed.startsWith('---'); + return trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("=") && !trimmed.startsWith("---"); }); - + // // console.log(` 📝 유효한 라인: ${lines.length}개`); - + if (lines.length === 0) return []; - + // CSV 형식으로 파싱 const result: any[] = []; - + for (let i = 0; i < lines.length; i++) { const line = lines[i]; - const values = line.split(',').map(v => v.trim().replace(/,=$/g, '')); - + const values = line.split(",").map((v) => v.trim().replace(/,=$/g, "")); + // // console.log(` 라인 ${i}:`, values); - + // 기상특보 형식: 지역코드, 지역명, 하위코드, 하위지역명, 발표시각, 특보종류, 등급, 발표상태, 설명 if (values.length >= 4) { const obj: any = { - code: values[0] || '', // 지역 코드 (예: L1070000) - region: values[1] || '', // 지역명 (예: 경상북도) - subCode: values[2] || '', // 하위 코드 (예: L1071600) - subRegion: values[3] || '', // 하위 지역명 (예: 영주시) - tmFc: values[4] || '', // 발표시각 - type: values[5] || '', // 특보종류 (강풍, 호우 등) - level: values[6] || '', // 등급 (주의, 경보) - status: values[7] || '', // 발표상태 - description: values.slice(8).join(', ').trim() || '', + code: values[0] || "", // 지역 코드 (예: L1070000) + region: values[1] || "", // 지역명 (예: 경상북도) + subCode: values[2] || "", // 하위 코드 (예: L1071600) + subRegion: values[3] || "", // 하위 지역명 (예: 영주시) + tmFc: values[4] || "", // 발표시각 + type: values[5] || "", // 특보종류 (강풍, 호우 등) + level: values[6] || "", // 등급 (주의, 경보) + status: values[7] || "", // 발표상태 + description: values.slice(8).join(", ").trim() || "", }; - + // 지역 이름 설정 (하위 지역명 우선, 없으면 상위 지역명) obj.name = obj.subRegion || obj.region || obj.code; - + result.push(obj); // console.log(` ✅ 파싱 성공:`, obj); } } - + // // console.log(" 📊 최종 파싱 결과:", result.length, "개"); return result; } catch (error) { @@ -378,15 +421,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 데이터를 마커와 폴리곤으로 변환 const convertToMapData = ( - rows: any[], - sourceName: string, + rows: any[], + sourceName: string, mapDisplayType?: "auto" | "marker" | "polygon", - dataSource?: ChartDataSource + dataSource?: ChartDataSource, ): { markers: MarkerData[]; polygons: PolygonData[] } => { // // console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행"); // // console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`); // // console.log(` 🎨 마커 색상:`, dataSource?.markerColor, `폴리곤 색상:`, dataSource?.polygonColor); - + if (rows.length === 0) return { markers: [], polygons: [] }; const markers: MarkerData[] = []; @@ -394,20 +437,20 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { rows.forEach((row, index) => { // // console.log(` 행 ${index}:`, row); - + // 텍스트 데이터 체크 (기상청 API 등) - if (row && typeof row === 'object' && row.text && typeof row.text === 'string') { + if (row && typeof row === "object" && row.text && typeof row.text === "string") { // // console.log(" 📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); const parsedData = parseTextData(row.text); // // console.log(` ✅ CSV 파싱 결과: ${parsedData.length}개 행`); - + // 파싱된 데이터를 재귀적으로 변환 (색상 정보 전달) const result = convertToMapData(parsedData, sourceName, mapDisplayType, dataSource); markers.push(...result.markers); polygons.push(...result.polygons); return; // 이 행은 처리 완료 } - + // 폴리곤 데이터 체크 (coordinates 필드가 배열인 경우 또는 강제 polygon 모드) if (row.coordinates && Array.isArray(row.coordinates) && row.coordinates.length > 0) { // // console.log(` → coordinates 발견:`, row.coordinates.length, "개"); @@ -437,7 +480,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { name: regionName, coordinates: MARITIME_ZONES[regionName] as [number, number][], status: row.status || row.level, - description: row.description || `${row.type || ''} ${row.level || ''}`.trim() || JSON.stringify(row, null, 2), + description: row.description || `${row.type || ""} ${row.level || ""}`.trim() || JSON.stringify(row, null, 2), source: sourceName, color: dataSource?.polygonColor || getColorByStatus(row.status || row.level), }); @@ -449,7 +492,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { let lng = row.lng || row.longitude || row.x || row.locationDataX; // 위도/경도가 없으면 지역 코드/지역명으로 변환 시도 - if ((lat === undefined || lng === undefined) && (row.code || row.areaCode || row.regionCode || row.tmFc || row.stnId)) { + if ( + (lat === undefined || lng === undefined) && + (row.code || row.areaCode || row.regionCode || row.tmFc || row.stnId) + ) { const regionCode = row.code || row.areaCode || row.regionCode || row.tmFc || row.stnId; // // console.log(` → 지역 코드 발견: ${regionCode}, 위도/경도 변환 시도`); const coords = getCoordinatesByRegionCode(regionCode); @@ -492,8 +538,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { return; // 폴리곤으로 처리했으므로 마커로는 추가하지 않음 } - // 위도/경도가 있고 marker 모드가 아니면 마커로 처리 - if (lat !== undefined && lng !== undefined && mapDisplayType !== "polygon") { + // 위도/경도가 있고 polygon 모드가 아니면 마커로 처리 + if (lat !== undefined && lng !== undefined && (mapDisplayType as string) !== "polygon") { // // console.log(` → 마커로 처리: (${lat}, ${lng})`); markers.push({ id: `${sourceName}-marker-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성 @@ -535,12 +581,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 상태에 따른 색상 반환 const getColorByStatus = (status?: string): string => { if (!status) return "#3b82f6"; // 기본 파란색 - + const statusLower = status.toLowerCase(); if (statusLower.includes("경보") || statusLower.includes("위험")) return "#ef4444"; // 빨강 if (statusLower.includes("주의")) return "#f59e0b"; // 주황 if (statusLower.includes("정상")) return "#10b981"; // 초록 - + return "#3b82f6"; // 기본 파란색 }; @@ -549,34 +595,34 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 기상청 지역 코드 매핑 (예시) const regionCodeMap: Record = { // 서울/경기 - "11": { lat: 37.5665, lng: 126.9780 }, // 서울 + "11": { lat: 37.5665, lng: 126.978 }, // 서울 "41": { lat: 37.4138, lng: 127.5183 }, // 경기 - + // 강원 "42": { lat: 37.8228, lng: 128.1555 }, // 강원 - + // 충청 "43": { lat: 36.6357, lng: 127.4913 }, // 충북 - "44": { lat: 36.5184, lng: 126.8000 }, // 충남 - + "44": { lat: 36.5184, lng: 126.8 }, // 충남 + // 전라 - "45": { lat: 35.7175, lng: 127.1530 }, // 전북 - "46": { lat: 34.8679, lng: 126.9910 }, // 전남 - + "45": { lat: 35.7175, lng: 127.153 }, // 전북 + "46": { lat: 34.8679, lng: 126.991 }, // 전남 + // 경상 "47": { lat: 36.4919, lng: 128.8889 }, // 경북 "48": { lat: 35.4606, lng: 128.2132 }, // 경남 - + // 제주 "50": { lat: 33.4996, lng: 126.5312 }, // 제주 - + // 광역시 "26": { lat: 35.1796, lng: 129.0756 }, // 부산 "27": { lat: 35.8714, lng: 128.6014 }, // 대구 "28": { lat: 35.1595, lng: 126.8526 }, // 광주 "29": { lat: 36.3504, lng: 127.3845 }, // 대전 "30": { lat: 35.5384, lng: 129.3114 }, // 울산 - "31": { lat: 36.8000, lng: 127.7000 }, // 세종 + "31": { lat: 36.8, lng: 127.7 }, // 세종 }; return regionCodeMap[code] || null; @@ -585,30 +631,130 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 해상 구역 폴리곤 좌표 (기상청 특보 구역 기준) const MARITIME_ZONES: Record> = { // 제주도 해역 - 제주도남부앞바다: [[33.25, 126.0], [33.25, 126.85], [33.0, 126.85], [33.0, 126.0]], - 제주도남쪽바깥먼바다: [[33.15, 125.7], [33.15, 127.3], [32.5, 127.3], [32.5, 125.7]], - 제주도동부앞바다: [[33.4, 126.7], [33.4, 127.25], [33.05, 127.25], [33.05, 126.7]], - 제주도남동쪽안쪽먼바다: [[33.3, 126.85], [33.3, 127.95], [32.65, 127.95], [32.65, 126.85]], - 제주도남서쪽안쪽먼바다: [[33.3, 125.35], [33.3, 126.45], [32.7, 126.45], [32.7, 125.35]], + 제주도남부앞바다: [ + [33.25, 126.0], + [33.25, 126.85], + [33.0, 126.85], + [33.0, 126.0], + ], + 제주도남쪽바깥먼바다: [ + [33.15, 125.7], + [33.15, 127.3], + [32.5, 127.3], + [32.5, 125.7], + ], + 제주도동부앞바다: [ + [33.4, 126.7], + [33.4, 127.25], + [33.05, 127.25], + [33.05, 126.7], + ], + 제주도남동쪽안쪽먼바다: [ + [33.3, 126.85], + [33.3, 127.95], + [32.65, 127.95], + [32.65, 126.85], + ], + 제주도남서쪽안쪽먼바다: [ + [33.3, 125.35], + [33.3, 126.45], + [32.7, 126.45], + [32.7, 125.35], + ], // 남해 해역 - 남해동부앞바다: [[34.65, 128.3], [34.65, 129.65], [33.95, 129.65], [33.95, 128.3]], - 남해동부안쪽먼바다: [[34.25, 127.95], [34.25, 129.75], [33.45, 129.75], [33.45, 127.95]], - 남해동부바깥먼바다: [[33.65, 127.95], [33.65, 130.35], [32.45, 130.35], [32.45, 127.95]], + 남해동부앞바다: [ + [34.65, 128.3], + [34.65, 129.65], + [33.95, 129.65], + [33.95, 128.3], + ], + 남해동부안쪽먼바다: [ + [34.25, 127.95], + [34.25, 129.75], + [33.45, 129.75], + [33.45, 127.95], + ], + 남해동부바깥먼바다: [ + [33.65, 127.95], + [33.65, 130.35], + [32.45, 130.35], + [32.45, 127.95], + ], // 동해 해역 - 경북북부앞바다: [[36.65, 129.2], [36.65, 130.1], [35.95, 130.1], [35.95, 129.2]], - 경북남부앞바다: [[36.15, 129.1], [36.15, 129.95], [35.45, 129.95], [35.45, 129.1]], - 동해남부남쪽안쪽먼바다: [[35.65, 129.35], [35.65, 130.65], [34.95, 130.65], [34.95, 129.35]], - 동해남부남쪽바깥먼바다: [[35.25, 129.45], [35.25, 131.15], [34.15, 131.15], [34.15, 129.45]], - 동해남부북쪽안쪽먼바다: [[36.6, 129.65], [36.6, 130.95], [35.85, 130.95], [35.85, 129.65]], - 동해남부북쪽바깥먼바다: [[36.65, 130.35], [36.65, 132.15], [35.85, 132.15], [35.85, 130.35]], + 경북북부앞바다: [ + [36.65, 129.2], + [36.65, 130.1], + [35.95, 130.1], + [35.95, 129.2], + ], + 경북남부앞바다: [ + [36.15, 129.1], + [36.15, 129.95], + [35.45, 129.95], + [35.45, 129.1], + ], + 동해남부남쪽안쪽먼바다: [ + [35.65, 129.35], + [35.65, 130.65], + [34.95, 130.65], + [34.95, 129.35], + ], + 동해남부남쪽바깥먼바다: [ + [35.25, 129.45], + [35.25, 131.15], + [34.15, 131.15], + [34.15, 129.45], + ], + 동해남부북쪽안쪽먼바다: [ + [36.6, 129.65], + [36.6, 130.95], + [35.85, 130.95], + [35.85, 129.65], + ], + 동해남부북쪽바깥먼바다: [ + [36.65, 130.35], + [36.65, 132.15], + [35.85, 132.15], + [35.85, 130.35], + ], // 강원 해역 - 강원북부앞바다: [[38.15, 128.4], [38.15, 129.55], [37.45, 129.55], [37.45, 128.4]], - 강원중부앞바다: [[37.65, 128.7], [37.65, 129.6], [36.95, 129.6], [36.95, 128.7]], - 강원남부앞바다: [[37.15, 128.9], [37.15, 129.85], [36.45, 129.85], [36.45, 128.9]], - 동해중부안쪽먼바다: [[38.55, 129.35], [38.55, 131.15], [37.25, 131.15], [37.25, 129.35]], - 동해중부바깥먼바다: [[38.6, 130.35], [38.6, 132.55], [37.65, 132.55], [37.65, 130.35]], + 강원북부앞바다: [ + [38.15, 128.4], + [38.15, 129.55], + [37.45, 129.55], + [37.45, 128.4], + ], + 강원중부앞바다: [ + [37.65, 128.7], + [37.65, 129.6], + [36.95, 129.6], + [36.95, 128.7], + ], + 강원남부앞바다: [ + [37.15, 128.9], + [37.15, 129.85], + [36.45, 129.85], + [36.45, 128.9], + ], + 동해중부안쪽먼바다: [ + [38.55, 129.35], + [38.55, 131.15], + [37.25, 131.15], + [37.25, 129.35], + ], + 동해중부바깥먼바다: [ + [38.6, 130.35], + [38.6, 132.55], + [37.65, 132.55], + [37.65, 130.35], + ], // 울릉도·독도 - "울릉도.독도": [[37.7, 130.7], [37.7, 132.0], [37.4, 132.0], [37.4, 130.7]], + "울릉도.독도": [ + [37.7, 130.7], + [37.7, 132.0], + [37.4, 132.0], + [37.4, 130.7], + ], }; // 지역명을 위도/경도로 변환 @@ -624,70 +770,70 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const regionNameMap: Record = { // 서울/경기 - "서울": { lat: 37.5665, lng: 126.9780 }, - "서울특별시": { lat: 37.5665, lng: 126.9780 }, - "경기": { lat: 37.4138, lng: 127.5183 }, - "경기도": { lat: 37.4138, lng: 127.5183 }, - "인천": { lat: 37.4563, lng: 126.7052 }, - "인천광역시": { lat: 37.4563, lng: 126.7052 }, - + 서울: { lat: 37.5665, lng: 126.978 }, + 서울특별시: { lat: 37.5665, lng: 126.978 }, + 경기: { lat: 37.4138, lng: 127.5183 }, + 경기도: { lat: 37.4138, lng: 127.5183 }, + 인천: { lat: 37.4563, lng: 126.7052 }, + 인천광역시: { lat: 37.4563, lng: 126.7052 }, + // 강원 - "강원": { lat: 37.8228, lng: 128.1555 }, - "강원도": { lat: 37.8228, lng: 128.1555 }, - "강원특별자치도": { lat: 37.8228, lng: 128.1555 }, - + 강원: { lat: 37.8228, lng: 128.1555 }, + 강원도: { lat: 37.8228, lng: 128.1555 }, + 강원특별자치도: { lat: 37.8228, lng: 128.1555 }, + // 충청 - "충북": { lat: 36.6357, lng: 127.4913 }, - "충청북도": { lat: 36.6357, lng: 127.4913 }, - "충남": { lat: 36.5184, lng: 126.8000 }, - "충청남도": { lat: 36.5184, lng: 126.8000 }, - "대전": { lat: 36.3504, lng: 127.3845 }, - "대전광역시": { lat: 36.3504, lng: 127.3845 }, - "세종": { lat: 36.8000, lng: 127.7000 }, - "세종특별자치시": { lat: 36.8000, lng: 127.7000 }, - + 충북: { lat: 36.6357, lng: 127.4913 }, + 충청북도: { lat: 36.6357, lng: 127.4913 }, + 충남: { lat: 36.5184, lng: 126.8 }, + 충청남도: { lat: 36.5184, lng: 126.8 }, + 대전: { lat: 36.3504, lng: 127.3845 }, + 대전광역시: { lat: 36.3504, lng: 127.3845 }, + 세종: { lat: 36.8, lng: 127.7 }, + 세종특별자치시: { lat: 36.8, lng: 127.7 }, + // 전라 - "전북": { lat: 35.7175, lng: 127.1530 }, - "전북특별자치도": { lat: 35.7175, lng: 127.1530 }, - "전라북도": { lat: 35.7175, lng: 127.1530 }, - "전남": { lat: 34.8679, lng: 126.9910 }, - "전라남도": { lat: 34.8679, lng: 126.9910 }, - "광주": { lat: 35.1595, lng: 126.8526 }, - "광주광역시": { lat: 35.1595, lng: 126.8526 }, - + 전북: { lat: 35.7175, lng: 127.153 }, + 전북특별자치도: { lat: 35.7175, lng: 127.153 }, + 전라북도: { lat: 35.7175, lng: 127.153 }, + 전남: { lat: 34.8679, lng: 126.991 }, + 전라남도: { lat: 34.8679, lng: 126.991 }, + 광주: { lat: 35.1595, lng: 126.8526 }, + 광주광역시: { lat: 35.1595, lng: 126.8526 }, + // 경상 - "경북": { lat: 36.4919, lng: 128.8889 }, - "경상북도": { lat: 36.4919, lng: 128.8889 }, - "포항": { lat: 36.0190, lng: 129.3435 }, - "포항시": { lat: 36.0190, lng: 129.3435 }, - "경주": { lat: 35.8562, lng: 129.2247 }, - "경주시": { lat: 35.8562, lng: 129.2247 }, - "안동": { lat: 36.5684, lng: 128.7294 }, - "안동시": { lat: 36.5684, lng: 128.7294 }, - "영주": { lat: 36.8056, lng: 128.6239 }, - "영주시": { lat: 36.8056, lng: 128.6239 }, - "경남": { lat: 35.4606, lng: 128.2132 }, - "경상남도": { lat: 35.4606, lng: 128.2132 }, - "창원": { lat: 35.2280, lng: 128.6811 }, - "창원시": { lat: 35.2280, lng: 128.6811 }, - "진주": { lat: 35.1800, lng: 128.1076 }, - "진주시": { lat: 35.1800, lng: 128.1076 }, - "부산": { lat: 35.1796, lng: 129.0756 }, - "부산광역시": { lat: 35.1796, lng: 129.0756 }, - "대구": { lat: 35.8714, lng: 128.6014 }, - "대구광역시": { lat: 35.8714, lng: 128.6014 }, - "울산": { lat: 35.5384, lng: 129.3114 }, - "울산광역시": { lat: 35.5384, lng: 129.3114 }, - + 경북: { lat: 36.4919, lng: 128.8889 }, + 경상북도: { lat: 36.4919, lng: 128.8889 }, + 포항: { lat: 36.019, lng: 129.3435 }, + 포항시: { lat: 36.019, lng: 129.3435 }, + 경주: { lat: 35.8562, lng: 129.2247 }, + 경주시: { lat: 35.8562, lng: 129.2247 }, + 안동: { lat: 36.5684, lng: 128.7294 }, + 안동시: { lat: 36.5684, lng: 128.7294 }, + 영주: { lat: 36.8056, lng: 128.6239 }, + 영주시: { lat: 36.8056, lng: 128.6239 }, + 경남: { lat: 35.4606, lng: 128.2132 }, + 경상남도: { lat: 35.4606, lng: 128.2132 }, + 창원: { lat: 35.228, lng: 128.6811 }, + 창원시: { lat: 35.228, lng: 128.6811 }, + 진주: { lat: 35.18, lng: 128.1076 }, + 진주시: { lat: 35.18, lng: 128.1076 }, + 부산: { lat: 35.1796, lng: 129.0756 }, + 부산광역시: { lat: 35.1796, lng: 129.0756 }, + 대구: { lat: 35.8714, lng: 128.6014 }, + 대구광역시: { lat: 35.8714, lng: 128.6014 }, + 울산: { lat: 35.5384, lng: 129.3114 }, + 울산광역시: { lat: 35.5384, lng: 129.3114 }, + // 제주 - "제주": { lat: 33.4996, lng: 126.5312 }, - "제주도": { lat: 33.4996, lng: 126.5312 }, - "제주특별자치도": { lat: 33.4996, lng: 126.5312 }, - + 제주: { lat: 33.4996, lng: 126.5312 }, + 제주도: { lat: 33.4996, lng: 126.5312 }, + 제주특별자치도: { lat: 33.4996, lng: 126.5312 }, + // 울릉도/독도 - "울릉도": { lat: 37.4845, lng: 130.9057 }, + 울릉도: { lat: 37.4845, lng: 130.9057 }, "울릉도.독도": { lat: 37.4845, lng: 130.9057 }, - "독도": { lat: 37.2433, lng: 131.8642 }, + 독도: { lat: 37.2433, lng: 131.8642 }, }; // 정확한 매칭 @@ -705,23 +851,18 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { return null; }; - // 데이터를 마커로 변환 (하위 호환성) + // 데이터를 마커로 변환 (하위 호환성 - 현재 미사용) + // eslint-disable-next-line @typescript-eslint/no-unused-vars const convertToMarkers = (rows: any[]): MarkerData[] => { if (rows.length === 0) return []; // 위도/경도 컬럼 찾기 const firstRow = rows[0]; const columns = Object.keys(firstRow); - - const latColumn = columns.find((col) => - /^(lat|latitude|위도|y)$/i.test(col) - ); - const lngColumn = columns.find((col) => - /^(lng|lon|longitude|경도|x)$/i.test(col) - ); - const nameColumn = columns.find((col) => - /^(name|title|이름|명칭|location)$/i.test(col) - ); + + const latColumn = columns.find((col) => /^(lat|latitude|위도|y)$/i.test(col)); + const lngColumn = columns.find((col) => /^(lng|lon|longitude|경도|x)$/i.test(col)); + const nameColumn = columns.find((col) => /^(name|title|이름|명칭|location)$/i.test(col)); if (!latColumn || !lngColumn) { console.warn("⚠️ 위도/경도 컬럼을 찾을 수 없습니다."); @@ -737,7 +878,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { return null; } - return { + const marker: MarkerData = { id: row.id || `marker-${index}`, lat, lng, @@ -747,6 +888,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { status: row.status, description: JSON.stringify(row, null, 2), }; + return marker; }) .filter((marker): marker is MarkerData => marker !== null); }; @@ -766,70 +908,60 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { loadGeoJsonData(); }, []); - // 초기 로드 + // 초기 로드 및 자동 새로고침 useEffect(() => { - const dataSources = element?.dataSources || element?.chartConfig?.dataSources; - // // console.log("🔄 useEffect 트리거! dataSources:", dataSources); - if (dataSources && dataSources.length > 0) { - loadMultipleDataSources(); - } else { - // // console.log("⚠️ dataSources가 없거나 비어있음"); + console.log("🔄 지도 위젯 초기화"); + + if (!dataSources || dataSources.length === 0) { + console.log("⚠️ dataSources가 없거나 비어있음"); setMarkers([]); setPolygons([]); + return; } - }, [dataSources, loadMultipleDataSources]); - // 자동 새로고침 - useEffect(() => { - if (!dataSources || dataSources.length === 0) return; + // 즉시 첫 로드 + console.log("📡 초기 데이터 로드"); + loadMultipleDataSources(); - // 모든 데이터 소스 중 가장 짧은 refreshInterval 찾기 - const intervals = dataSources - .map((ds) => ds.refreshInterval) - .filter((interval): interval is number => typeof interval === "number" && interval > 0); - - if (intervals.length === 0) return; - - const minInterval = Math.min(...intervals); - // // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // 5초마다 자동 새로고침 + const refreshInterval = 5; + console.log(`⏱️ 자동 새로고침 설정: ${refreshInterval}초마다`); const intervalId = setInterval(() => { - // // console.log("🔄 자동 새로고침 실행"); + console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); - }, minInterval * 1000); + }, refreshInterval * 1000); return () => { - // // console.log("⏹️ 자동 새로고침 정리"); + console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; - }, [dataSources, loadMultipleDataSources]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // 타일맵 URL (chartConfig에서 가져오기) - const tileMapUrl = element?.chartConfig?.tileMapUrl || - `https://api.vworld.kr/req/wmts/1.0.0/${VWORLD_API_KEY}/Base/{z}/{y}/{x}.png`; + const tileMapUrl = + element?.chartConfig?.tileMapUrl || `https://api.vworld.kr/req/wmts/1.0.0/${VWORLD_API_KEY}/Base/{z}/{y}/{x}.png`; // 지도 중심점 계산 - const center: [number, number] = markers.length > 0 - ? [ - markers.reduce((sum, m) => sum + m.lat, 0) / markers.length, - markers.reduce((sum, m) => sum + m.lng, 0) / markers.length, - ] - : [37.5665, 126.978]; // 기본: 서울 + const center: [number, number] = + markers.length > 0 + ? [ + markers.reduce((sum, m) => sum + m.lat, 0) / markers.length, + markers.reduce((sum, m) => sum + m.lng, 0) / markers.length, + ] + : [37.5665, 126.978]; // 기본: 서울 return ( -
+
{/* 헤더 */}
-

- {element?.customTitle || "지도"} -

-

+

{element?.customTitle || "지도"}

+

{element?.dataSources?.length || 0}개 데이터 소스 연결됨 {lastRefreshTime && ( - - • 마지막 업데이트: {lastRefreshTime.toLocaleTimeString("ko-KR")} - + • 마지막 업데이트: {lastRefreshTime.toLocaleTimeString("ko-KR")} )}

@@ -852,27 +984,16 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
{error ? (
-

{error}

+

{error}

) : !element?.dataSources || element.dataSources.length === 0 ? (
-

- 데이터 소스를 연결해주세요 -

+

데이터 소스를 연결해주세요

) : ( - - - + + + {/* 폴리곤 렌더링 */} {/* GeoJSON 렌더링 (육지 지역 경계선) */} {(() => { @@ -885,16 +1006,16 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { })()} {geoJsonData && polygons.length > 0 ? ( p.id))} // 폴리곤 변경 시 재렌더링 + key={JSON.stringify(polygons.map((p) => p.id))} // 폴리곤 변경 시 재렌더링 data={geoJsonData} style={(feature: any) => { const ctpName = feature?.properties?.CTP_KOR_NM; // 시/도명 (예: 경상북도) const sigName = feature?.properties?.SIG_KOR_NM; // 시/군/구명 (예: 군위군) - + // 폴리곤 매칭 (시/군/구명 우선, 없으면 시/도명) - const matchingPolygon = polygons.find(p => { + const matchingPolygon = polygons.find((p) => { if (!p.name) return false; - + // 정확한 매칭 if (p.name === sigName) { // console.log(`✅ 정확 매칭: ${p.name} === ${sigName}`); @@ -904,7 +1025,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // console.log(`✅ 정확 매칭: ${p.name} === ${ctpName}`); return true; } - + // 부분 매칭 (GeoJSON 지역명에 폴리곤 이름이 포함되는지) if (sigName && sigName.includes(p.name)) { // console.log(`✅ 부분 매칭: ${sigName} includes ${p.name}`); @@ -914,7 +1035,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // console.log(`✅ 부분 매칭: ${ctpName} includes ${p.name}`); return true; } - + // 역방향 매칭 (폴리곤 이름에 GeoJSON 지역명이 포함되는지) if (sigName && p.name.includes(sigName)) { // console.log(`✅ 역방향 매칭: ${p.name} includes ${sigName}`); @@ -924,7 +1045,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // console.log(`✅ 역방향 매칭: ${p.name} includes ${ctpName}`); return true; } - + return false; }); @@ -945,8 +1066,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { onEachFeature={(feature: any, layer: any) => { const ctpName = feature?.properties?.CTP_KOR_NM; const sigName = feature?.properties?.SIG_KOR_NM; - - const matchingPolygon = polygons.find(p => { + + const matchingPolygon = polygons.find((p) => { if (!p.name) return false; if (p.name === sigName || p.name === ctpName) return true; if (sigName && sigName.includes(p.name)) return true; @@ -960,9 +1081,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { layer.bindPopup(`
${matchingPolygon.name}
- ${matchingPolygon.source ? `
출처: ${matchingPolygon.source}
` : ''} - ${matchingPolygon.status ? `
상태: ${matchingPolygon.status}
` : ''} - ${matchingPolygon.description ? `
${matchingPolygon.description}
` : ''} + ${matchingPolygon.source ? `
출처: ${matchingPolygon.source}
` : ""} + ${matchingPolygon.status ? `
상태: ${matchingPolygon.status}
` : ""} + ${matchingPolygon.description ? `
${matchingPolygon.description}
` : ""}
`); } @@ -975,150 +1096,152 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { )} {/* 폴리곤 렌더링 (해상 구역만) */} - {polygons.filter(p => MARITIME_ZONES[p.name]).map((polygon) => ( - - -
-
{polygon.name}
- {polygon.source && ( -
- 출처: {polygon.source} -
- )} - {polygon.status && ( -
- 상태: {polygon.status} -
- )} - {polygon.description && ( -
-
{polygon.description}
-
- )} -
-
-
- ))} - - {/* 마커 렌더링 */} + {polygons + .filter((p) => MARITIME_ZONES[p.name]) + .map((polygon) => ( + + +
+
{polygon.name}
+ {polygon.source && ( +
출처: {polygon.source}
+ )} + {polygon.status &&
상태: {polygon.status}
} + {polygon.description && ( +
+
{polygon.description}
+
+ )} +
+
+
+ ))} + + {/* 마커 렌더링 (화살표 모양) */} {markers.map((marker) => { - // 커스텀 색상 아이콘 생성 - let customIcon; + // 화살표 아이콘 생성 (진행 방향으로 회전) + let arrowIcon: any; if (typeof window !== "undefined") { const L = require("leaflet"); - customIcon = L.divIcon({ - className: "custom-marker", + const heading = marker.heading || 0; + arrowIcon = L.divIcon({ + className: "custom-arrow-marker", html: `
+ transform: translate(-50%, -50%) rotate(${heading}deg); + filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); + "> + + + + + + + + +
`, - iconSize: [30, 30], - iconAnchor: [15, 15], + iconSize: [40, 40], + iconAnchor: [20, 20], }); } return ( - - -
- {/* 제목 */} -
-
{marker.name}
- {marker.source && ( -
- 📡 {marker.source} -
- )} -
+ + +
+ {/* 제목 */} +
+
{marker.name}
+ {marker.source &&
📡 {marker.source}
} +
- {/* 상세 정보 */} -
- {marker.description && ( -
-
상세 정보
-
- {(() => { - try { - const parsed = JSON.parse(marker.description); - return ( -
- {parsed.incidenteTypeCd === "1" && ( -
🚨 교통사고
- )} - {parsed.incidenteTypeCd === "2" && ( -
🚧 도로공사
- )} - {parsed.addressJibun && ( -
📍 {parsed.addressJibun}
- )} - {parsed.addressNew && parsed.addressNew !== parsed.addressJibun && ( -
📍 {parsed.addressNew}
- )} - {parsed.roadName && ( -
🛣️ {parsed.roadName}
- )} - {parsed.linkName && ( -
🔗 {parsed.linkName}
- )} - {parsed.incidentMsg && ( -
💬 {parsed.incidentMsg}
- )} - {parsed.eventContent && ( -
📝 {parsed.eventContent}
- )} - {parsed.startDate && ( -
🕐 {parsed.startDate}
- )} - {parsed.endDate && ( -
🕐 종료: {parsed.endDate}
- )} -
- ); - } catch { - return marker.description; - } - })()} + {/* 상세 정보 */} +
+ {marker.description && ( +
+
상세 정보
+
+ {(() => { + try { + const parsed = JSON.parse(marker.description); + return ( +
+ {parsed.incidenteTypeCd === "1" && ( +
🚨 교통사고
+ )} + {parsed.incidenteTypeCd === "2" && ( +
🚧 도로공사
+ )} + {parsed.addressJibun &&
📍 {parsed.addressJibun}
} + {parsed.addressNew && parsed.addressNew !== parsed.addressJibun && ( +
📍 {parsed.addressNew}
+ )} + {parsed.roadName &&
🛣️ {parsed.roadName}
} + {parsed.linkName &&
🔗 {parsed.linkName}
} + {parsed.incidentMsg && ( +
💬 {parsed.incidentMsg}
+ )} + {parsed.eventContent && ( +
📝 {parsed.eventContent}
+ )} + {parsed.startDate &&
🕐 {parsed.startDate}
} + {parsed.endDate &&
🕐 종료: {parsed.endDate}
} +
+ ); + } catch { + return marker.description; + } + })()} +
-
- )} + )} - {marker.status && ( -
- 상태: {marker.status} -
- )} + {marker.status && ( +
+ 상태: {marker.status} +
+ )} - {/* 좌표 */} -
- 📍 {marker.lat.toFixed(6)}, {marker.lng.toFixed(6)} + {/* 좌표 */} +
+ 📍 {marker.lat.toFixed(6)}, {marker.lng.toFixed(6)} +
-
- - + + ); })} @@ -1127,7 +1250,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { {/* 하단 정보 */} {(markers.length > 0 || polygons.length > 0) && ( -
+
{markers.length > 0 && `마커 ${markers.length}개`} {markers.length > 0 && polygons.length > 0 && " · "} {polygons.length > 0 && `영역 ${polygons.length}개`} @@ -1136,4 +1259,3 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
); } -