❓
알 수 없는 위젯 타입: {element.subtype}
@@ -212,7 +212,7 @@ export function DashboardViewer({
dataUrl: string,
format: "png" | "pdf",
canvasWidth: number,
- canvasHeight: number
+ canvasHeight: number,
) => {
if (format === "png") {
console.log("💾 PNG 다운로드 시작...");
@@ -227,7 +227,7 @@ export function DashboardViewer({
} else {
console.log("📄 PDF 생성 중...");
const jsPDF = (await import("jspdf")).default;
-
+
// dataUrl에서 이미지 크기 계산
const img = new Image();
img.src = dataUrl;
@@ -274,40 +274,41 @@ export function DashboardViewer({
console.log("📸 html-to-image 로딩 중...");
// html-to-image 동적 import
+ // @ts-expect-error - html-to-image 타입 선언 누락
const { toPng } = await import("html-to-image");
console.log("📸 캔버스 캡처 중...");
-
+
// 3D/WebGL 렌더링 완료 대기
console.log("⏳ 3D 렌더링 완료 대기 중...");
await new Promise((resolve) => setTimeout(resolve, 1000));
-
+
// WebGL 캔버스를 이미지로 변환 (Three.js 캔버스 보존)
console.log("🎨 WebGL 캔버스 처리 중...");
const webglCanvases = canvas.querySelectorAll("canvas");
const webglImages: { canvas: HTMLCanvasElement; dataUrl: string; rect: DOMRect }[] = [];
-
+
webglCanvases.forEach((webglCanvas) => {
try {
const rect = webglCanvas.getBoundingClientRect();
const dataUrl = webglCanvas.toDataURL("image/png");
webglImages.push({ canvas: webglCanvas, dataUrl, rect });
- console.log("✅ WebGL 캔버스 캡처:", {
- width: rect.width,
+ console.log("✅ WebGL 캔버스 캡처:", {
+ width: rect.width,
height: rect.height,
left: rect.left,
top: rect.top,
- bottom: rect.bottom
+ bottom: rect.bottom,
});
} catch (error) {
console.warn("⚠️ WebGL 캔버스 캡처 실패:", error);
}
});
-
+
// 캔버스의 실제 크기와 위치 가져오기
const rect = canvas.getBoundingClientRect();
const canvasWidth = canvas.scrollWidth;
-
+
// 실제 콘텐츠의 최하단 위치 계산
// 뷰어 모드에서는 모든 자식 요소를 확인
const children = canvas.querySelectorAll("*");
@@ -323,17 +324,17 @@ export function DashboardViewer({
maxBottom = relativeBottom;
}
});
-
+
// 실제 콘텐츠 높이 + 여유 공간 (50px)
// maxBottom이 0이면 기본 캔버스 높이 사용
const canvasHeight = maxBottom > 50 ? maxBottom + 50 : Math.max(canvas.scrollHeight, rect.height);
-
+
console.log("📐 캔버스 정보:", {
rect: { x: rect.x, y: rect.y, left: rect.left, top: rect.top, width: rect.width, height: rect.height },
scroll: { width: canvasWidth, height: canvas.scrollHeight },
calculated: { width: canvasWidth, height: canvasHeight },
maxBottom: maxBottom,
- webglCount: webglImages.length
+ webglCount: webglImages.length,
});
// html-to-image로 캔버스 캡처 (WebGL 제외)
@@ -344,8 +345,8 @@ export function DashboardViewer({
pixelRatio: 2, // 고해상도
cacheBust: true,
skipFonts: false,
- preferredFontFormat: 'woff2',
- filter: (node) => {
+ preferredFontFormat: "woff2",
+ filter: (node: Node) => {
// WebGL 캔버스는 제외 (나중에 수동으로 합성)
if (node instanceof HTMLCanvasElement) {
return false;
@@ -353,7 +354,7 @@ export function DashboardViewer({
return true;
},
});
-
+
// WebGL 캔버스를 이미지 위에 합성
if (webglImages.length > 0) {
console.log("🖼️ WebGL 이미지 합성 중...");
@@ -362,17 +363,17 @@ export function DashboardViewer({
await new Promise((resolve) => {
img.onload = resolve;
});
-
+
// 새 캔버스에 합성
const compositeCanvas = document.createElement("canvas");
compositeCanvas.width = img.width;
compositeCanvas.height = img.height;
const ctx = compositeCanvas.getContext("2d");
-
+
if (ctx) {
// 기본 이미지 그리기
ctx.drawImage(img, 0, 0);
-
+
// WebGL 이미지들을 위치에 맞게 그리기
for (const { dataUrl: webglDataUrl, rect: webglRect } of webglImages) {
const webglImg = new Image();
@@ -380,28 +381,28 @@ export function DashboardViewer({
await new Promise((resolve) => {
webglImg.onload = resolve;
});
-
+
// 상대 위치 계산 (pixelRatio 2 고려)
const relativeX = (webglRect.left - rect.left) * 2;
const relativeY = (webglRect.top - rect.top) * 2;
const width = webglRect.width * 2;
const height = webglRect.height * 2;
-
+
ctx.drawImage(webglImg, relativeX, relativeY, width, height);
console.log("✅ WebGL 이미지 합성 완료:", { x: relativeX, y: relativeY, width, height });
}
-
+
// 합성된 이미지를 dataUrl로 변환
const compositeDataUrl = compositeCanvas.toDataURL("image/png");
console.log("✅ 최종 합성 완료");
-
+
// 합성된 이미지로 다운로드
return await handleDownloadWithDataUrl(compositeDataUrl, format, canvasWidth, canvasHeight);
}
}
console.log("✅ 캡처 완료 (WebGL 없음)");
-
+
// WebGL이 없는 경우 기본 다운로드
await handleDownloadWithDataUrl(dataUrl, format, canvasWidth, canvasHeight);
} catch (error) {
@@ -409,7 +410,8 @@ export function DashboardViewer({
alert(`다운로드에 실패했습니다.\n\n에러: ${error instanceof Error ? error.message : String(error)}`);
}
},
- [backgroundColor, dashboardTitle],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [backgroundColor, dashboardTitle, handleDownloadWithDataUrl],
);
// 캔버스 설정 계산
@@ -528,11 +530,11 @@ export function DashboardViewer({
// 요소가 없는 경우
if (elements.length === 0) {
return (
-
+
📊
-
표시할 요소가 없습니다
-
대시보드 편집기에서 차트나 위젯을 추가해보세요
+
표시할 요소가 없습니다
+
대시보드 편집기에서 차트나 위젯을 추가해보세요
);
@@ -541,8 +543,8 @@ export function DashboardViewer({
return (
{/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
-
-
+
+
{/* 다운로드 버튼 */}
@@ -584,7 +586,7 @@ export function DashboardViewer({
{/* 태블릿 이하: 반응형 세로 정렬 */}
-
+
{/* 다운로드 버튼 */}
@@ -646,38 +648,21 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
// 태블릿 이하: 세로 스택 카드 스타일
return (
{element.showHeader !== false && (
-
{element.customTitle || element.title}
-
+ {/* map-summary-v2는 customTitle이 없으면 제목 숨김 */}
+ {element.subtype === "map-summary-v2" && !element.customTitle ? null : (
+
{element.customTitle || element.title}
+ )}
)}
{!isMounted ? (
) : element.type === "chart" ? (
@@ -686,10 +671,10 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
)}
{isLoading && (
-
+
-
-
업데이트 중...
+
+
업데이트 중...
)}
@@ -704,7 +689,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
return (
{element.showHeader !== false && (
-
{element.customTitle || element.title}
-
+ {/* map-summary-v2는 customTitle이 없으면 제목 숨김 */}
+ {element.subtype === "map-summary-v2" && !element.customTitle ? null : (
+
{element.customTitle || element.title}
+ )}
)}
{!isMounted ? (
) : element.type === "chart" ? (
{isLoading && (
-
+
-
-
업데이트 중...
+
+
업데이트 중...
)}
diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx
index 5eeeca12..dafc40fa 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,24 +66,35 @@ 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);
const [geoJsonData, setGeoJsonData] = useState(null);
const [lastRefreshTime, setLastRefreshTime] = useState(null);
- // // console.log("🧪 MapTestWidgetV2 렌더링!", element);
- // // console.log("📍 마커:", markers.length, "🔷 폴리곤:", polygons.length);
-
// dataSources를 useMemo로 추출 (circular reference 방지)
const dataSources = useMemo(() => {
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 +110,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 +155,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 +188,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
} finally {
setLoading(false);
}
- }, [dataSources]);
+ }, [dataSources, prevMarkers, calculateHeading]);
// 수동 새로고침 핸들러
const handleManualRefresh = useCallback(() => {
@@ -158,9 +197,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 +246,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 +265,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 +275,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 +300,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 +310,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 +333,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 +361,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 +418,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 +434,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 +477,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 +489,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 +535,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 +578,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 +592,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 +628,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 +767,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 +848,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 +875,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
return null;
}
- return {
+ const marker: MarkerData = {
id: row.id || `marker-${index}`,
lat,
lng,
@@ -747,6 +885,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 +905,59 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
loadGeoJsonData();
}, []);
- // 초기 로드
+ // 초기 로드 및 자동 새로고침 (마커 데이터만 polling)
useEffect(() => {
- const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
- // // console.log("🔄 useEffect 트리거! dataSources:", dataSources);
- if (dataSources && dataSources.length > 0) {
- loadMultipleDataSources();
- } else {
- // // console.log("⚠️ dataSources가 없거나 비어있음");
+ if (!dataSources || dataSources.length === 0) {
setMarkers([]);
setPolygons([]);
+ return;
}
- }, [dataSources, loadMultipleDataSources]);
- // 자동 새로고침
- useEffect(() => {
- if (!dataSources || dataSources.length === 0) return;
+ // 즉시 첫 로드 (마커 데이터)
+ loadMultipleDataSources();
- // 모든 데이터 소스 중 가장 짧은 refreshInterval 찾기
- const intervals = dataSources
- .map((ds) => ds.refreshInterval)
- .filter((interval): interval is number => typeof interval === "number" && interval > 0);
+ // 첫 번째 데이터 소스의 새로고침 간격 사용 (초)
+ const firstDataSource = dataSources[0];
+ const refreshInterval = firstDataSource?.refreshInterval ?? 5;
- if (intervals.length === 0) return;
-
- const minInterval = Math.min(...intervals);
- // // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
+ // 0이면 자동 새로고침 비활성화
+ if (refreshInterval === 0) {
+ return;
+ }
const intervalId = setInterval(() => {
- // // console.log("🔄 자동 새로고침 실행");
loadMultipleDataSources();
- }, minInterval * 1000);
+ }, refreshInterval * 1000);
return () => {
- // // console.log("⏹️ 자동 새로고침 정리");
clearInterval(intervalId);
};
- }, [dataSources, loadMultipleDataSources]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [dataSources]);
// 타일맵 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?.dataSources?.length || 0}개 데이터 소스 연결됨
+
{element?.customTitle || "지도"}
+
+ {dataSources?.length || 0}개 데이터 소스 연결됨
{lastRefreshTime && (
-
- • 마지막 업데이트: {lastRefreshTime.toLocaleTimeString("ko-KR")}
-
+ • 마지막 업데이트: {lastRefreshTime.toLocaleTimeString("ko-KR")}
)}
@@ -852,27 +980,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
{error ? (
- ) : !element?.dataSources || element.dataSources.length === 0 ? (
-
-
- 데이터 소스를 연결해주세요
-
+
{error}
) : (
-
-
-
+
+
+
{/* 폴리곤 렌더링 */}
{/* GeoJSON 렌더링 (육지 지역 경계선) */}
{(() => {
@@ -885,16 +998,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 +1017,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 +1027,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 +1037,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// console.log(`✅ 역방향 매칭: ${p.name} includes ${ctpName}`);
return true;
}
-
+
return false;
});
@@ -945,8 +1058,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 +1073,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 +1088,250 @@ 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;
+ // 첫 번째 데이터 소스의 마커 종류 가져오기
+ const firstDataSource = dataSources?.[0];
+ const markerType = firstDataSource?.markerType || "circle";
+
+ let markerIcon: any;
if (typeof window !== "undefined") {
const L = require("leaflet");
- customIcon = L.divIcon({
- className: "custom-marker",
- html: `
-
- `,
- iconSize: [30, 30],
- iconAnchor: [15, 15],
- });
+ const heading = marker.heading || 0;
+
+ if (markerType === "arrow") {
+ // 화살표 마커
+ markerIcon = L.divIcon({
+ className: "custom-arrow-marker",
+ html: `
+
+ `,
+ iconSize: [40, 40],
+ iconAnchor: [20, 20],
+ });
+ } else {
+ // 동그라미 마커 (기본)
+ markerIcon = L.divIcon({
+ className: "custom-circle-marker",
+ html: `
+
+
+
+ `,
+ iconSize: [32, 32],
+ iconAnchor: [16, 16],
+ });
+ }
}
return (
-
-
-
- {/* 제목 */}
-
-
{marker.name}
+
+
+
+ {/* 데이터 소스명만 표시 */}
{marker.source && (
-
- 📡 {marker.source}
+
)}
-
- {/* 상세 정보 */}
-
- {marker.description && (
-
-
상세 정보
-
- {(() => {
+ {/* 상세 정보 */}
+
+ {marker.description &&
+ (() => {
+ const firstDataSource = dataSources?.[0];
+ const popupFields = firstDataSource?.popupFields;
+
+ // popupFields가 설정되어 있으면 설정된 필드만 표시
+ if (popupFields && popupFields.length > 0) {
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}
- )}
+
+
상세 정보
+
+ {popupFields.map((field, idx) => {
+ const value = parsed[field.fieldName];
+ if (value === undefined || value === null) return null;
+
+ // 포맷팅 적용
+ let formattedValue = value;
+ if (field.format === "date" && value) {
+ formattedValue = new Date(value).toLocaleDateString("ko-KR");
+ } else if (field.format === "datetime" && value) {
+ formattedValue = new Date(value).toLocaleString("ko-KR");
+ } else if (field.format === "number" && typeof value === "number") {
+ formattedValue = value.toLocaleString();
+ } else if (
+ field.format === "url" &&
+ typeof value === "string" &&
+ value.startsWith("http")
+ ) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {field.label}:{" "}
+ {String(formattedValue)}
+
+ );
+ })}
+
+
+ );
+ } catch (error) {
+ return (
+
+
상세 정보
+
{marker.description}
);
- } catch {
- return marker.description;
}
- })()}
-
-
- )}
+ }
- {marker.status && (
-
- 상태: {marker.status}
-
- )}
+ // popupFields가 없으면 전체 데이터 표시 (기본 동작)
+ try {
+ const parsed = JSON.parse(marker.description);
+ return (
+
+
상세 정보
+
+ {Object.entries(parsed).map(([key, value], idx) => {
+ if (value === undefined || value === null) return null;
- {/* 좌표 */}
-
- 📍 {marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
+ // 좌표 필드는 제외 (하단에 별도 표시)
+ if (["lat", "lng", "latitude", "longitude", "x", "y"].includes(key)) return null;
+
+ return (
+
+ {key}:{" "}
+ {String(value)}
+
+ );
+ })}
+
+
+ );
+ } catch (error) {
+ return (
+
+
상세 정보
+
{marker.description}
+
+ );
+ }
+ })()}
+
+ {/* 좌표 */}
+
+ {marker.lat.toFixed(6)}, {marker.lng.toFixed(6)}
+
-
-
-
+
+
);
})}
@@ -1127,7 +1340,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 +1349,3 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
);
}
-