http polling 주기를 5초로 변경
This commit is contained in:
parent
379a3852b6
commit
cbdd9fef0f
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||||
|
|
@ -11,13 +12,14 @@ import "leaflet/dist/leaflet.css";
|
||||||
|
|
||||||
// Leaflet 아이콘 경로 설정 (엑박 방지)
|
// Leaflet 아이콘 경로 설정 (엑박 방지)
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const L = require("leaflet");
|
import("leaflet").then((L) => {
|
||||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||||
L.Icon.Default.mergeOptions({
|
L.Icon.Default.mergeOptions({
|
||||||
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
|
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",
|
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",
|
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaflet 동적 import (SSR 방지)
|
// Leaflet 동적 import (SSR 방지)
|
||||||
|
|
@ -46,6 +48,9 @@ interface MarkerData {
|
||||||
description?: string;
|
description?: string;
|
||||||
source?: string; // 어느 데이터 소스에서 왔는지
|
source?: string; // 어느 데이터 소스에서 왔는지
|
||||||
color?: string; // 마커 색상
|
color?: string; // 마커 색상
|
||||||
|
heading?: number; // 진행 방향 (0-360도, 0=북쪽)
|
||||||
|
prevLat?: number; // 이전 위도 (방향 계산용)
|
||||||
|
prevLng?: number; // 이전 경도 (방향 계산용)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PolygonData {
|
interface PolygonData {
|
||||||
|
|
@ -61,6 +66,7 @@ interface PolygonData {
|
||||||
|
|
||||||
export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
const [markers, setMarkers] = useState<MarkerData[]>([]);
|
const [markers, setMarkers] = useState<MarkerData[]>([]);
|
||||||
|
const [prevMarkers, setPrevMarkers] = useState<MarkerData[]>([]); // 이전 마커 위치 저장
|
||||||
const [polygons, setPolygons] = useState<PolygonData[]>([]);
|
const [polygons, setPolygons] = useState<PolygonData[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
@ -75,10 +81,23 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
return element?.dataSources || element?.chartConfig?.dataSources;
|
return element?.dataSources || element?.chartConfig?.dataSources;
|
||||||
}, [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 loadMultipleDataSources = useCallback(async () => {
|
||||||
const dataSourcesList = dataSources;
|
|
||||||
|
|
||||||
if (!dataSources || dataSources.length === 0) {
|
if (!dataSources || dataSources.length === 0) {
|
||||||
// // console.log("⚠️ 데이터 소스가 없습니다.");
|
// // console.log("⚠️ 데이터 소스가 없습니다.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -106,7 +125,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
console.error(`❌ 데이터 소스 "${source.name || source.id}" 로딩 실패:`, err);
|
console.error(`❌ 데이터 소스 "${source.name || source.id}" 로딩 실패:`, err);
|
||||||
return { markers: [], polygons: [] };
|
return { markers: [], polygons: [] };
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 성공한 데이터만 병합
|
// 성공한 데이터만 병합
|
||||||
|
|
@ -140,7 +159,30 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
// // console.log("📍 최종 마커 데이터:", allMarkers);
|
// // console.log("📍 최종 마커 데이터:", allMarkers);
|
||||||
// // console.log("🔷 최종 폴리곤 데이터:", allPolygons);
|
// // 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);
|
setPolygons(allPolygons);
|
||||||
setLastRefreshTime(new Date());
|
setLastRefreshTime(new Date());
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -149,7 +191,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [dataSources]);
|
}, [dataSources, prevMarkers, calculateHeading]);
|
||||||
|
|
||||||
// 수동 새로고침 핸들러
|
// 수동 새로고침 핸들러
|
||||||
const handleManualRefresh = useCallback(() => {
|
const handleManualRefresh = useCallback(() => {
|
||||||
|
|
@ -158,7 +200,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
}, [loadMultipleDataSources]);
|
}, [loadMultipleDataSources]);
|
||||||
|
|
||||||
// REST API 데이터 로딩
|
// 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);
|
// // console.log(`🌐 REST API 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType);
|
||||||
|
|
||||||
if (!source.endpoint) {
|
if (!source.endpoint) {
|
||||||
|
|
@ -214,7 +258,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
let data = result.data;
|
let data = result.data;
|
||||||
|
|
||||||
// 텍스트 형식 데이터 체크 (기상청 API 등)
|
// 텍스트 형식 데이터 체크 (기상청 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 파싱 시도");
|
// // console.log("📄 텍스트 형식 데이터 감지, CSV 파싱 시도");
|
||||||
const parsedData = parseTextData(data.text);
|
const parsedData = parseTextData(data.text);
|
||||||
if (parsedData.length > 0) {
|
if (parsedData.length > 0) {
|
||||||
|
|
@ -243,7 +287,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Database 데이터 로딩
|
// 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);
|
// // console.log(`💾 Database 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType);
|
||||||
|
|
||||||
if (!source.query) {
|
if (!source.query) {
|
||||||
|
|
@ -257,7 +303,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
|
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
|
||||||
const externalResult = await ExternalDbConnectionAPI.executeQuery(
|
const externalResult = await ExternalDbConnectionAPI.executeQuery(
|
||||||
parseInt(source.externalConnectionId),
|
parseInt(source.externalConnectionId),
|
||||||
source.query
|
source.query,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!externalResult.success || !externalResult.data) {
|
if (!externalResult.success || !externalResult.data) {
|
||||||
|
|
@ -325,12 +371,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
return parseXmlData(text);
|
return parseXmlData(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = text.split('\n').filter(line => {
|
const lines = text.split("\n").filter((line) => {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
return trimmed &&
|
return trimmed && !trimmed.startsWith("#") && !trimmed.startsWith("=") && !trimmed.startsWith("---");
|
||||||
!trimmed.startsWith('#') &&
|
|
||||||
!trimmed.startsWith('=') &&
|
|
||||||
!trimmed.startsWith('---');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// // console.log(` 📝 유효한 라인: ${lines.length}개`);
|
// // console.log(` 📝 유효한 라인: ${lines.length}개`);
|
||||||
|
|
@ -342,22 +385,22 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[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);
|
// // console.log(` 라인 ${i}:`, values);
|
||||||
|
|
||||||
// 기상특보 형식: 지역코드, 지역명, 하위코드, 하위지역명, 발표시각, 특보종류, 등급, 발표상태, 설명
|
// 기상특보 형식: 지역코드, 지역명, 하위코드, 하위지역명, 발표시각, 특보종류, 등급, 발표상태, 설명
|
||||||
if (values.length >= 4) {
|
if (values.length >= 4) {
|
||||||
const obj: any = {
|
const obj: any = {
|
||||||
code: values[0] || '', // 지역 코드 (예: L1070000)
|
code: values[0] || "", // 지역 코드 (예: L1070000)
|
||||||
region: values[1] || '', // 지역명 (예: 경상북도)
|
region: values[1] || "", // 지역명 (예: 경상북도)
|
||||||
subCode: values[2] || '', // 하위 코드 (예: L1071600)
|
subCode: values[2] || "", // 하위 코드 (예: L1071600)
|
||||||
subRegion: values[3] || '', // 하위 지역명 (예: 영주시)
|
subRegion: values[3] || "", // 하위 지역명 (예: 영주시)
|
||||||
tmFc: values[4] || '', // 발표시각
|
tmFc: values[4] || "", // 발표시각
|
||||||
type: values[5] || '', // 특보종류 (강풍, 호우 등)
|
type: values[5] || "", // 특보종류 (강풍, 호우 등)
|
||||||
level: values[6] || '', // 등급 (주의, 경보)
|
level: values[6] || "", // 등급 (주의, 경보)
|
||||||
status: values[7] || '', // 발표상태
|
status: values[7] || "", // 발표상태
|
||||||
description: values.slice(8).join(', ').trim() || '',
|
description: values.slice(8).join(", ").trim() || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 지역 이름 설정 (하위 지역명 우선, 없으면 상위 지역명)
|
// 지역 이름 설정 (하위 지역명 우선, 없으면 상위 지역명)
|
||||||
|
|
@ -381,7 +424,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
rows: any[],
|
rows: any[],
|
||||||
sourceName: string,
|
sourceName: string,
|
||||||
mapDisplayType?: "auto" | "marker" | "polygon",
|
mapDisplayType?: "auto" | "marker" | "polygon",
|
||||||
dataSource?: ChartDataSource
|
dataSource?: ChartDataSource,
|
||||||
): { markers: MarkerData[]; polygons: PolygonData[] } => {
|
): { markers: MarkerData[]; polygons: PolygonData[] } => {
|
||||||
// // console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행");
|
// // console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행");
|
||||||
// // console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`);
|
// // console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`);
|
||||||
|
|
@ -396,7 +439,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
// // console.log(` 행 ${index}:`, row);
|
// // console.log(` 행 ${index}:`, row);
|
||||||
|
|
||||||
// 텍스트 데이터 체크 (기상청 API 등)
|
// 텍스트 데이터 체크 (기상청 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 파싱 시도");
|
// // console.log(" 📄 텍스트 형식 데이터 감지, CSV 파싱 시도");
|
||||||
const parsedData = parseTextData(row.text);
|
const parsedData = parseTextData(row.text);
|
||||||
// // console.log(` ✅ CSV 파싱 결과: ${parsedData.length}개 행`);
|
// // console.log(` ✅ CSV 파싱 결과: ${parsedData.length}개 행`);
|
||||||
|
|
@ -437,7 +480,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
name: regionName,
|
name: regionName,
|
||||||
coordinates: MARITIME_ZONES[regionName] as [number, number][],
|
coordinates: MARITIME_ZONES[regionName] as [number, number][],
|
||||||
status: row.status || row.level,
|
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,
|
source: sourceName,
|
||||||
color: dataSource?.polygonColor || getColorByStatus(row.status || row.level),
|
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;
|
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;
|
const regionCode = row.code || row.areaCode || row.regionCode || row.tmFc || row.stnId;
|
||||||
// // console.log(` → 지역 코드 발견: ${regionCode}, 위도/경도 변환 시도`);
|
// // console.log(` → 지역 코드 발견: ${regionCode}, 위도/경도 변환 시도`);
|
||||||
const coords = getCoordinatesByRegionCode(regionCode);
|
const coords = getCoordinatesByRegionCode(regionCode);
|
||||||
|
|
@ -492,8 +538,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
return; // 폴리곤으로 처리했으므로 마커로는 추가하지 않음
|
return; // 폴리곤으로 처리했으므로 마커로는 추가하지 않음
|
||||||
}
|
}
|
||||||
|
|
||||||
// 위도/경도가 있고 marker 모드가 아니면 마커로 처리
|
// 위도/경도가 있고 polygon 모드가 아니면 마커로 처리
|
||||||
if (lat !== undefined && lng !== undefined && mapDisplayType !== "polygon") {
|
if (lat !== undefined && lng !== undefined && (mapDisplayType as string) !== "polygon") {
|
||||||
// // console.log(` → 마커로 처리: (${lat}, ${lng})`);
|
// // console.log(` → 마커로 처리: (${lat}, ${lng})`);
|
||||||
markers.push({
|
markers.push({
|
||||||
id: `${sourceName}-marker-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성
|
id: `${sourceName}-marker-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성
|
||||||
|
|
@ -549,7 +595,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
// 기상청 지역 코드 매핑 (예시)
|
// 기상청 지역 코드 매핑 (예시)
|
||||||
const regionCodeMap: Record<string, { lat: number; lng: number }> = {
|
const regionCodeMap: Record<string, { lat: number; lng: number }> = {
|
||||||
// 서울/경기
|
// 서울/경기
|
||||||
"11": { lat: 37.5665, lng: 126.9780 }, // 서울
|
"11": { lat: 37.5665, lng: 126.978 }, // 서울
|
||||||
"41": { lat: 37.4138, lng: 127.5183 }, // 경기
|
"41": { lat: 37.4138, lng: 127.5183 }, // 경기
|
||||||
|
|
||||||
// 강원
|
// 강원
|
||||||
|
|
@ -557,11 +603,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
|
|
||||||
// 충청
|
// 충청
|
||||||
"43": { lat: 36.6357, lng: 127.4913 }, // 충북
|
"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 }, // 전북
|
"45": { lat: 35.7175, lng: 127.153 }, // 전북
|
||||||
"46": { lat: 34.8679, lng: 126.9910 }, // 전남
|
"46": { lat: 34.8679, lng: 126.991 }, // 전남
|
||||||
|
|
||||||
// 경상
|
// 경상
|
||||||
"47": { lat: 36.4919, lng: 128.8889 }, // 경북
|
"47": { lat: 36.4919, lng: 128.8889 }, // 경북
|
||||||
|
|
@ -576,7 +622,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
"28": { lat: 35.1595, lng: 126.8526 }, // 광주
|
"28": { lat: 35.1595, lng: 126.8526 }, // 광주
|
||||||
"29": { lat: 36.3504, lng: 127.3845 }, // 대전
|
"29": { lat: 36.3504, lng: 127.3845 }, // 대전
|
||||||
"30": { lat: 35.5384, lng: 129.3114 }, // 울산
|
"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;
|
return regionCodeMap[code] || null;
|
||||||
|
|
@ -585,30 +631,130 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
// 해상 구역 폴리곤 좌표 (기상청 특보 구역 기준)
|
// 해상 구역 폴리곤 좌표 (기상청 특보 구역 기준)
|
||||||
const MARITIME_ZONES: Record<string, Array<[number, number]>> = {
|
const MARITIME_ZONES: Record<string, Array<[number, number]>> = {
|
||||||
// 제주도 해역
|
// 제주도 해역
|
||||||
제주도남부앞바다: [[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.25, 126.0],
|
||||||
제주도동부앞바다: [[33.4, 126.7], [33.4, 127.25], [33.05, 127.25], [33.05, 126.7]],
|
[33.25, 126.85],
|
||||||
제주도남동쪽안쪽먼바다: [[33.3, 126.85], [33.3, 127.95], [32.65, 127.95], [32.65, 126.85]],
|
[33.0, 126.85],
|
||||||
제주도남서쪽안쪽먼바다: [[33.3, 125.35], [33.3, 126.45], [32.7, 126.45], [32.7, 125.35]],
|
[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]],
|
[34.65, 128.3],
|
||||||
남해동부바깥먼바다: [[33.65, 127.95], [33.65, 130.35], [32.45, 130.35], [32.45, 127.95]],
|
[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]],
|
[36.65, 129.2],
|
||||||
동해남부남쪽안쪽먼바다: [[35.65, 129.35], [35.65, 130.65], [34.95, 130.65], [34.95, 129.35]],
|
[36.65, 130.1],
|
||||||
동해남부남쪽바깥먼바다: [[35.25, 129.45], [35.25, 131.15], [34.15, 131.15], [34.15, 129.45]],
|
[35.95, 130.1],
|
||||||
동해남부북쪽안쪽먼바다: [[36.6, 129.65], [36.6, 130.95], [35.85, 130.95], [35.85, 129.65]],
|
[35.95, 129.2],
|
||||||
동해남부북쪽바깥먼바다: [[36.65, 130.35], [36.65, 132.15], [35.85, 132.15], [35.85, 130.35]],
|
],
|
||||||
|
경북남부앞바다: [
|
||||||
|
[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]],
|
[38.15, 128.4],
|
||||||
강원남부앞바다: [[37.15, 128.9], [37.15, 129.85], [36.45, 129.85], [36.45, 128.9]],
|
[38.15, 129.55],
|
||||||
동해중부안쪽먼바다: [[38.55, 129.35], [38.55, 131.15], [37.25, 131.15], [37.25, 129.35]],
|
[37.45, 129.55],
|
||||||
동해중부바깥먼바다: [[38.6, 130.35], [38.6, 132.55], [37.65, 132.55], [37.65, 130.35]],
|
[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<string, { lat: number; lng: number }> = {
|
const regionNameMap: Record<string, { lat: number; lng: number }> = {
|
||||||
// 서울/경기
|
// 서울/경기
|
||||||
"서울": { lat: 37.5665, lng: 126.9780 },
|
서울: { lat: 37.5665, lng: 126.978 },
|
||||||
"서울특별시": { lat: 37.5665, lng: 126.9780 },
|
서울특별시: { lat: 37.5665, lng: 126.978 },
|
||||||
"경기": { lat: 37.4138, lng: 127.5183 },
|
경기: { lat: 37.4138, lng: 127.5183 },
|
||||||
"경기도": { 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.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.6357, lng: 127.4913 },
|
충청북도: { lat: 36.6357, lng: 127.4913 },
|
||||||
"충남": { lat: 36.5184, lng: 126.8000 },
|
충남: { lat: 36.5184, lng: 126.8 },
|
||||||
"충청남도": { lat: 36.5184, lng: 126.8000 },
|
충청남도: { lat: 36.5184, lng: 126.8 },
|
||||||
"대전": { lat: 36.3504, lng: 127.3845 },
|
대전: { lat: 36.3504, lng: 127.3845 },
|
||||||
"대전광역시": { lat: 36.3504, lng: 127.3845 },
|
대전광역시: { lat: 36.3504, lng: 127.3845 },
|
||||||
"세종": { lat: 36.8000, lng: 127.7000 },
|
세종: { lat: 36.8, lng: 127.7 },
|
||||||
"세종특별자치시": { lat: 36.8000, lng: 127.7000 },
|
세종특별자치시: { lat: 36.8, lng: 127.7 },
|
||||||
|
|
||||||
// 전라
|
// 전라
|
||||||
"전북": { lat: 35.7175, lng: 127.1530 },
|
전북: { lat: 35.7175, lng: 127.153 },
|
||||||
"전북특별자치도": { lat: 35.7175, lng: 127.1530 },
|
전북특별자치도: { lat: 35.7175, lng: 127.153 },
|
||||||
"전라북도": { lat: 35.7175, lng: 127.1530 },
|
전라북도: { lat: 35.7175, lng: 127.153 },
|
||||||
"전남": { lat: 34.8679, lng: 126.9910 },
|
전남: { lat: 34.8679, lng: 126.991 },
|
||||||
"전라남도": { lat: 34.8679, lng: 126.9910 },
|
전라남도: { lat: 34.8679, lng: 126.991 },
|
||||||
"광주": { lat: 35.1595, lng: 126.8526 },
|
광주: { lat: 35.1595, lng: 126.8526 },
|
||||||
"광주광역시": { 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.4919, lng: 128.8889 },
|
경상북도: { lat: 36.4919, lng: 128.8889 },
|
||||||
"포항": { lat: 36.0190, lng: 129.3435 },
|
포항: { lat: 36.019, lng: 129.3435 },
|
||||||
"포항시": { lat: 36.0190, lng: 129.3435 },
|
포항시: { lat: 36.019, lng: 129.3435 },
|
||||||
"경주": { lat: 35.8562, lng: 129.2247 },
|
경주: { lat: 35.8562, lng: 129.2247 },
|
||||||
"경주시": { 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.5684, lng: 128.7294 },
|
안동시: { lat: 36.5684, lng: 128.7294 },
|
||||||
"영주": { lat: 36.8056, lng: 128.6239 },
|
영주: { lat: 36.8056, lng: 128.6239 },
|
||||||
"영주시": { 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.4606, lng: 128.2132 },
|
경상남도: { lat: 35.4606, lng: 128.2132 },
|
||||||
"창원": { lat: 35.2280, lng: 128.6811 },
|
창원: { lat: 35.228, lng: 128.6811 },
|
||||||
"창원시": { lat: 35.2280, lng: 128.6811 },
|
창원시: { lat: 35.228, lng: 128.6811 },
|
||||||
"진주": { lat: 35.1800, lng: 128.1076 },
|
진주: { lat: 35.18, lng: 128.1076 },
|
||||||
"진주시": { lat: 35.1800, lng: 128.1076 },
|
진주시: { lat: 35.18, lng: 128.1076 },
|
||||||
"부산": { lat: 35.1796, lng: 129.0756 },
|
부산: { lat: 35.1796, lng: 129.0756 },
|
||||||
"부산광역시": { 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.8714, lng: 128.6014 },
|
대구광역시: { lat: 35.8714, lng: 128.6014 },
|
||||||
"울산": { lat: 35.5384, lng: 129.3114 },
|
울산: { lat: 35.5384, lng: 129.3114 },
|
||||||
"울산광역시": { 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.4845, lng: 130.9057 },
|
||||||
"독도": { lat: 37.2433, lng: 131.8642 },
|
독도: { lat: 37.2433, lng: 131.8642 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// 정확한 매칭
|
// 정확한 매칭
|
||||||
|
|
@ -705,7 +851,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 데이터를 마커로 변환 (하위 호환성)
|
// 데이터를 마커로 변환 (하위 호환성 - 현재 미사용)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const convertToMarkers = (rows: any[]): MarkerData[] => {
|
const convertToMarkers = (rows: any[]): MarkerData[] => {
|
||||||
if (rows.length === 0) return [];
|
if (rows.length === 0) return [];
|
||||||
|
|
||||||
|
|
@ -713,15 +860,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
const firstRow = rows[0];
|
const firstRow = rows[0];
|
||||||
const columns = Object.keys(firstRow);
|
const columns = Object.keys(firstRow);
|
||||||
|
|
||||||
const latColumn = columns.find((col) =>
|
const latColumn = columns.find((col) => /^(lat|latitude|위도|y)$/i.test(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 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) {
|
if (!latColumn || !lngColumn) {
|
||||||
console.warn("⚠️ 위도/경도 컬럼을 찾을 수 없습니다.");
|
console.warn("⚠️ 위도/경도 컬럼을 찾을 수 없습니다.");
|
||||||
|
|
@ -737,7 +878,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const marker: MarkerData = {
|
||||||
id: row.id || `marker-${index}`,
|
id: row.id || `marker-${index}`,
|
||||||
lat,
|
lat,
|
||||||
lng,
|
lng,
|
||||||
|
|
@ -747,6 +888,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
status: row.status,
|
status: row.status,
|
||||||
description: JSON.stringify(row, null, 2),
|
description: JSON.stringify(row, null, 2),
|
||||||
};
|
};
|
||||||
|
return marker;
|
||||||
})
|
})
|
||||||
.filter((marker): marker is MarkerData => marker !== null);
|
.filter((marker): marker is MarkerData => marker !== null);
|
||||||
};
|
};
|
||||||
|
|
@ -766,50 +908,44 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
loadGeoJsonData();
|
loadGeoJsonData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 초기 로드
|
// 초기 로드 및 자동 새로고침
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
|
console.log("🔄 지도 위젯 초기화");
|
||||||
// // console.log("🔄 useEffect 트리거! dataSources:", dataSources);
|
|
||||||
if (dataSources && dataSources.length > 0) {
|
if (!dataSources || dataSources.length === 0) {
|
||||||
loadMultipleDataSources();
|
console.log("⚠️ dataSources가 없거나 비어있음");
|
||||||
} else {
|
|
||||||
// // console.log("⚠️ dataSources가 없거나 비어있음");
|
|
||||||
setMarkers([]);
|
setMarkers([]);
|
||||||
setPolygons([]);
|
setPolygons([]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [dataSources, loadMultipleDataSources]);
|
|
||||||
|
|
||||||
// 자동 새로고침
|
// 즉시 첫 로드
|
||||||
useEffect(() => {
|
console.log("📡 초기 데이터 로드");
|
||||||
if (!dataSources || dataSources.length === 0) return;
|
loadMultipleDataSources();
|
||||||
|
|
||||||
// 모든 데이터 소스 중 가장 짧은 refreshInterval 찾기
|
// 5초마다 자동 새로고침
|
||||||
const intervals = dataSources
|
const refreshInterval = 5;
|
||||||
.map((ds) => ds.refreshInterval)
|
console.log(`⏱️ 자동 새로고침 설정: ${refreshInterval}초마다`);
|
||||||
.filter((interval): interval is number => typeof interval === "number" && interval > 0);
|
|
||||||
|
|
||||||
if (intervals.length === 0) return;
|
|
||||||
|
|
||||||
const minInterval = Math.min(...intervals);
|
|
||||||
// // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
|
|
||||||
|
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
// // console.log("🔄 자동 새로고침 실행");
|
console.log("🔄 자동 새로고침 실행");
|
||||||
loadMultipleDataSources();
|
loadMultipleDataSources();
|
||||||
}, minInterval * 1000);
|
}, refreshInterval * 1000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// // console.log("⏹️ 자동 새로고침 정리");
|
console.log("⏹️ 자동 새로고침 정리");
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
};
|
};
|
||||||
}, [dataSources, loadMultipleDataSources]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 타일맵 URL (chartConfig에서 가져오기)
|
// 타일맵 URL (chartConfig에서 가져오기)
|
||||||
const tileMapUrl = element?.chartConfig?.tileMapUrl ||
|
const tileMapUrl =
|
||||||
`https://api.vworld.kr/req/wmts/1.0.0/${VWORLD_API_KEY}/Base/{z}/{y}/{x}.png`;
|
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
|
const center: [number, number] =
|
||||||
|
markers.length > 0
|
||||||
? [
|
? [
|
||||||
markers.reduce((sum, m) => sum + m.lat, 0) / markers.length,
|
markers.reduce((sum, m) => sum + m.lat, 0) / markers.length,
|
||||||
markers.reduce((sum, m) => sum + m.lng, 0) / markers.length,
|
markers.reduce((sum, m) => sum + m.lng, 0) / markers.length,
|
||||||
|
|
@ -817,19 +953,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
: [37.5665, 126.978]; // 기본: 서울
|
: [37.5665, 126.978]; // 기본: 서울
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col bg-background">
|
<div className="bg-background flex h-full w-full flex-col">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between border-b p-4">
|
<div className="flex items-center justify-between border-b p-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold">
|
<h3 className="text-lg font-semibold">{element?.customTitle || "지도"}</h3>
|
||||||
{element?.customTitle || "지도"}
|
<p className="text-muted-foreground text-xs">
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{element?.dataSources?.length || 0}개 데이터 소스 연결됨
|
{element?.dataSources?.length || 0}개 데이터 소스 연결됨
|
||||||
{lastRefreshTime && (
|
{lastRefreshTime && (
|
||||||
<span className="ml-2">
|
<span className="ml-2">• 마지막 업데이트: {lastRefreshTime.toLocaleTimeString("ko-KR")}</span>
|
||||||
• 마지막 업데이트: {lastRefreshTime.toLocaleTimeString("ko-KR")}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -852,26 +984,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<p className="text-sm text-destructive">{error}</p>
|
<p className="text-destructive text-sm">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
) : !element?.dataSources || element.dataSources.length === 0 ? (
|
) : !element?.dataSources || element.dataSources.length === 0 ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-muted-foreground text-sm">데이터 소스를 연결해주세요</p>
|
||||||
데이터 소스를 연결해주세요
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<MapContainer
|
<MapContainer center={center} zoom={13} style={{ width: "100%", height: "100%" }} className="z-0">
|
||||||
center={center}
|
<TileLayer url={tileMapUrl} attribution="© VWorld" maxZoom={19} />
|
||||||
zoom={13}
|
|
||||||
style={{ width: "100%", height: "100%" }}
|
|
||||||
className="z-0"
|
|
||||||
>
|
|
||||||
<TileLayer
|
|
||||||
url={tileMapUrl}
|
|
||||||
attribution='© VWorld'
|
|
||||||
maxZoom={19}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 폴리곤 렌더링 */}
|
{/* 폴리곤 렌더링 */}
|
||||||
{/* GeoJSON 렌더링 (육지 지역 경계선) */}
|
{/* GeoJSON 렌더링 (육지 지역 경계선) */}
|
||||||
|
|
@ -885,14 +1006,14 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
})()}
|
})()}
|
||||||
{geoJsonData && polygons.length > 0 ? (
|
{geoJsonData && polygons.length > 0 ? (
|
||||||
<GeoJSON
|
<GeoJSON
|
||||||
key={JSON.stringify(polygons.map(p => p.id))} // 폴리곤 변경 시 재렌더링
|
key={JSON.stringify(polygons.map((p) => p.id))} // 폴리곤 변경 시 재렌더링
|
||||||
data={geoJsonData}
|
data={geoJsonData}
|
||||||
style={(feature: any) => {
|
style={(feature: any) => {
|
||||||
const ctpName = feature?.properties?.CTP_KOR_NM; // 시/도명 (예: 경상북도)
|
const ctpName = feature?.properties?.CTP_KOR_NM; // 시/도명 (예: 경상북도)
|
||||||
const sigName = feature?.properties?.SIG_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) return false;
|
||||||
|
|
||||||
// 정확한 매칭
|
// 정확한 매칭
|
||||||
|
|
@ -946,7 +1067,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
const ctpName = feature?.properties?.CTP_KOR_NM;
|
const ctpName = feature?.properties?.CTP_KOR_NM;
|
||||||
const sigName = feature?.properties?.SIG_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) return false;
|
||||||
if (p.name === sigName || p.name === ctpName) return true;
|
if (p.name === sigName || p.name === ctpName) return true;
|
||||||
if (sigName && sigName.includes(p.name)) return true;
|
if (sigName && sigName.includes(p.name)) return true;
|
||||||
|
|
@ -960,9 +1081,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
layer.bindPopup(`
|
layer.bindPopup(`
|
||||||
<div class="min-w-[200px]">
|
<div class="min-w-[200px]">
|
||||||
<div class="mb-2 font-semibold">${matchingPolygon.name}</div>
|
<div class="mb-2 font-semibold">${matchingPolygon.name}</div>
|
||||||
${matchingPolygon.source ? `<div class="mb-1 text-xs text-muted-foreground">출처: ${matchingPolygon.source}</div>` : ''}
|
${matchingPolygon.source ? `<div class="mb-1 text-xs text-muted-foreground">출처: ${matchingPolygon.source}</div>` : ""}
|
||||||
${matchingPolygon.status ? `<div class="mb-1 text-xs">상태: ${matchingPolygon.status}</div>` : ''}
|
${matchingPolygon.status ? `<div class="mb-1 text-xs">상태: ${matchingPolygon.status}</div>` : ""}
|
||||||
${matchingPolygon.description ? `<div class="mt-2 max-h-[200px] overflow-auto text-xs"><pre class="whitespace-pre-wrap">${matchingPolygon.description}</pre></div>` : ''}
|
${matchingPolygon.description ? `<div class="mt-2 max-h-[200px] overflow-auto text-xs"><pre class="whitespace-pre-wrap">${matchingPolygon.description}</pre></div>` : ""}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
@ -975,7 +1096,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 폴리곤 렌더링 (해상 구역만) */}
|
{/* 폴리곤 렌더링 (해상 구역만) */}
|
||||||
{polygons.filter(p => MARITIME_ZONES[p.name]).map((polygon) => (
|
{polygons
|
||||||
|
.filter((p) => MARITIME_ZONES[p.name])
|
||||||
|
.map((polygon) => (
|
||||||
<Polygon
|
<Polygon
|
||||||
key={polygon.id}
|
key={polygon.id}
|
||||||
positions={polygon.coordinates}
|
positions={polygon.coordinates}
|
||||||
|
|
@ -990,15 +1113,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
<div className="min-w-[200px]">
|
<div className="min-w-[200px]">
|
||||||
<div className="mb-2 font-semibold">{polygon.name}</div>
|
<div className="mb-2 font-semibold">{polygon.name}</div>
|
||||||
{polygon.source && (
|
{polygon.source && (
|
||||||
<div className="mb-1 text-xs text-muted-foreground">
|
<div className="text-muted-foreground mb-1 text-xs">출처: {polygon.source}</div>
|
||||||
출처: {polygon.source}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{polygon.status && (
|
|
||||||
<div className="mb-1 text-xs">
|
|
||||||
상태: {polygon.status}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
{polygon.status && <div className="mb-1 text-xs">상태: {polygon.status}</div>}
|
||||||
{polygon.description && (
|
{polygon.description && (
|
||||||
<div className="mt-2 max-h-[200px] overflow-auto text-xs">
|
<div className="mt-2 max-h-[200px] overflow-auto text-xs">
|
||||||
<pre className="whitespace-pre-wrap">{polygon.description}</pre>
|
<pre className="whitespace-pre-wrap">{polygon.description}</pre>
|
||||||
|
|
@ -1009,92 +1126,98 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
</Polygon>
|
</Polygon>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* 마커 렌더링 */}
|
{/* 마커 렌더링 (화살표 모양) */}
|
||||||
{markers.map((marker) => {
|
{markers.map((marker) => {
|
||||||
// 커스텀 색상 아이콘 생성
|
// 화살표 아이콘 생성 (진행 방향으로 회전)
|
||||||
let customIcon;
|
let arrowIcon: any;
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const L = require("leaflet");
|
const L = require("leaflet");
|
||||||
customIcon = L.divIcon({
|
const heading = marker.heading || 0;
|
||||||
className: "custom-marker",
|
arrowIcon = L.divIcon({
|
||||||
|
className: "custom-arrow-marker",
|
||||||
html: `
|
html: `
|
||||||
<div style="
|
<div style="
|
||||||
width: 30px;
|
width: 40px;
|
||||||
height: 30px;
|
height: 40px;
|
||||||
background-color: ${marker.color || "#3b82f6"};
|
|
||||||
border: 3px solid white;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%) rotate(${heading}deg);
|
||||||
"></div>
|
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
|
||||||
|
">
|
||||||
|
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<!-- 화살표 몸통 -->
|
||||||
|
<polygon
|
||||||
|
points="20,5 25,15 23,15 23,25 17,25 17,15 15,15"
|
||||||
|
fill="${marker.color || "#3b82f6"}"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<!-- 화살촉 -->
|
||||||
|
<polygon
|
||||||
|
points="20,2 28,12 12,12"
|
||||||
|
fill="${marker.color || "#3b82f6"}"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
<!-- 중심점 -->
|
||||||
|
<circle
|
||||||
|
cx="20"
|
||||||
|
cy="30"
|
||||||
|
r="3"
|
||||||
|
fill="white"
|
||||||
|
stroke="${marker.color || "#3b82f6"}"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
`,
|
`,
|
||||||
iconSize: [30, 30],
|
iconSize: [40, 40],
|
||||||
iconAnchor: [15, 15],
|
iconAnchor: [20, 20],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker key={marker.id} position={[marker.lat, marker.lng]} icon={arrowIcon}>
|
||||||
key={marker.id}
|
|
||||||
position={[marker.lat, marker.lng]}
|
|
||||||
icon={customIcon}
|
|
||||||
>
|
|
||||||
<Popup maxWidth={350}>
|
<Popup maxWidth={350}>
|
||||||
<div className="min-w-[250px] max-w-[350px]">
|
<div className="max-w-[350px] min-w-[250px]">
|
||||||
{/* 제목 */}
|
{/* 제목 */}
|
||||||
<div className="mb-2 border-b pb-2">
|
<div className="mb-2 border-b pb-2">
|
||||||
<div className="text-base font-bold">{marker.name}</div>
|
<div className="text-base font-bold">{marker.name}</div>
|
||||||
{marker.source && (
|
{marker.source && <div className="text-muted-foreground mt-1 text-xs">📡 {marker.source}</div>}
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
|
||||||
📡 {marker.source}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 상세 정보 */}
|
{/* 상세 정보 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{marker.description && (
|
{marker.description && (
|
||||||
<div className="rounded bg-muted p-2">
|
<div className="bg-muted rounded p-2">
|
||||||
<div className="mb-1 text-xs font-semibold text-foreground">상세 정보</div>
|
<div className="text-foreground mb-1 text-xs font-semibold">상세 정보</div>
|
||||||
<div className="text-xs text-muted-foreground whitespace-pre-wrap">
|
<div className="text-muted-foreground text-xs whitespace-pre-wrap">
|
||||||
{(() => {
|
{(() => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(marker.description);
|
const parsed = JSON.parse(marker.description);
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{parsed.incidenteTypeCd === "1" && (
|
{parsed.incidenteTypeCd === "1" && (
|
||||||
<div className="font-semibold text-destructive">🚨 교통사고</div>
|
<div className="text-destructive font-semibold">🚨 교통사고</div>
|
||||||
)}
|
)}
|
||||||
{parsed.incidenteTypeCd === "2" && (
|
{parsed.incidenteTypeCd === "2" && (
|
||||||
<div className="font-semibold text-warning">🚧 도로공사</div>
|
<div className="text-warning font-semibold">🚧 도로공사</div>
|
||||||
)}
|
|
||||||
{parsed.addressJibun && (
|
|
||||||
<div>📍 {parsed.addressJibun}</div>
|
|
||||||
)}
|
)}
|
||||||
|
{parsed.addressJibun && <div>📍 {parsed.addressJibun}</div>}
|
||||||
{parsed.addressNew && parsed.addressNew !== parsed.addressJibun && (
|
{parsed.addressNew && parsed.addressNew !== parsed.addressJibun && (
|
||||||
<div>📍 {parsed.addressNew}</div>
|
<div>📍 {parsed.addressNew}</div>
|
||||||
)}
|
)}
|
||||||
{parsed.roadName && (
|
{parsed.roadName && <div>🛣️ {parsed.roadName}</div>}
|
||||||
<div>🛣️ {parsed.roadName}</div>
|
{parsed.linkName && <div>🔗 {parsed.linkName}</div>}
|
||||||
)}
|
|
||||||
{parsed.linkName && (
|
|
||||||
<div>🔗 {parsed.linkName}</div>
|
|
||||||
)}
|
|
||||||
{parsed.incidentMsg && (
|
{parsed.incidentMsg && (
|
||||||
<div className="mt-2 border-t pt-2">💬 {parsed.incidentMsg}</div>
|
<div className="mt-2 border-t pt-2">💬 {parsed.incidentMsg}</div>
|
||||||
)}
|
)}
|
||||||
{parsed.eventContent && (
|
{parsed.eventContent && (
|
||||||
<div className="mt-2 border-t pt-2">📝 {parsed.eventContent}</div>
|
<div className="mt-2 border-t pt-2">📝 {parsed.eventContent}</div>
|
||||||
)}
|
)}
|
||||||
{parsed.startDate && (
|
{parsed.startDate && <div className="text-[10px]">🕐 {parsed.startDate}</div>}
|
||||||
<div className="text-[10px]">🕐 {parsed.startDate}</div>
|
{parsed.endDate && <div className="text-[10px]">🕐 종료: {parsed.endDate}</div>}
|
||||||
)}
|
|
||||||
{parsed.endDate && (
|
|
||||||
<div className="text-[10px]">🕐 종료: {parsed.endDate}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -1112,7 +1235,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 좌표 */}
|
{/* 좌표 */}
|
||||||
<div className="border-t pt-2 text-[10px] text-muted-foreground">
|
<div className="text-muted-foreground border-t pt-2 text-[10px]">
|
||||||
📍 {marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
|
📍 {marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1127,7 +1250,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
|
|
||||||
{/* 하단 정보 */}
|
{/* 하단 정보 */}
|
||||||
{(markers.length > 0 || polygons.length > 0) && (
|
{(markers.length > 0 || polygons.length > 0) && (
|
||||||
<div className="border-t p-2 text-xs text-muted-foreground">
|
<div className="text-muted-foreground border-t p-2 text-xs">
|
||||||
{markers.length > 0 && `마커 ${markers.length}개`}
|
{markers.length > 0 && `마커 ${markers.length}개`}
|
||||||
{markers.length > 0 && polygons.length > 0 && " · "}
|
{markers.length > 0 && polygons.length > 0 && " · "}
|
||||||
{polygons.length > 0 && `영역 ${polygons.length}개`}
|
{polygons.length > 0 && `영역 ${polygons.length}개`}
|
||||||
|
|
@ -1136,4 +1259,3 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue