diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx
index dd3d08ce..363e2cba 100644
--- a/frontend/components/admin/dashboard/CanvasElement.tsx
+++ b/frontend/components/admin/dashboard/CanvasElement.tsx
@@ -78,6 +78,16 @@ const ChartTestWidget = dynamic(() => import("@/components/dashboard/widgets/Cha
loading: () =>
로딩 중...
,
});
+const ListTestWidget = dynamic(() => import("@/components/dashboard/widgets/ListTestWidget").then((mod) => ({ default: mod.ListTestWidget })), {
+ ssr: false,
+ loading: () => 로딩 중...
,
+});
+
+const CustomMetricTestWidget = dynamic(() => import("@/components/dashboard/widgets/CustomMetricTestWidget"), {
+ ssr: false,
+ loading: () => 로딩 중...
,
+});
+
// 범용 상태 요약 위젯 (차량, 배송 등 모든 상태 위젯 통합)
const StatusSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/StatusSummaryWidget"), {
ssr: false,
@@ -884,6 +894,16 @@ export function CanvasElement({
데이터 소스를 연결해주세요
diff --git a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx
new file mode 100644
index 00000000..b0f9122c
--- /dev/null
+++ b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx
@@ -0,0 +1,283 @@
+"use client";
+
+import React, { useState, useEffect, useCallback } from "react";
+import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/types";
+import { Loader2 } from "lucide-react";
+
+interface CustomMetricTestWidgetProps {
+ element: DashboardElement;
+}
+
+// 집계 함수 실행
+const calculateMetric = (rows: any[], field: string, aggregation: string): number => {
+ if (rows.length === 0) return 0;
+
+ switch (aggregation) {
+ case "count":
+ return rows.length;
+ case "sum": {
+ return rows.reduce((sum, row) => sum + (parseFloat(row[field]) || 0), 0);
+ }
+ case "avg": {
+ const sum = rows.reduce((s, row) => s + (parseFloat(row[field]) || 0), 0);
+ return rows.length > 0 ? sum / rows.length : 0;
+ }
+ case "min": {
+ return Math.min(...rows.map((row) => parseFloat(row[field]) || 0));
+ }
+ case "max": {
+ return Math.max(...rows.map((row) => parseFloat(row[field]) || 0));
+ }
+ default:
+ return 0;
+ }
+};
+
+// 색상 스타일 매핑
+const colorMap = {
+ indigo: { bg: "bg-indigo-50", text: "text-indigo-600", border: "border-indigo-200" },
+ green: { bg: "bg-green-50", text: "text-green-600", border: "border-green-200" },
+ blue: { bg: "bg-blue-50", text: "text-blue-600", border: "border-blue-200" },
+ purple: { bg: "bg-purple-50", text: "text-purple-600", border: "border-purple-200" },
+ orange: { bg: "bg-orange-50", text: "text-orange-600", border: "border-orange-200" },
+ gray: { bg: "bg-gray-50", text: "text-gray-600", border: "border-gray-200" },
+};
+
+/**
+ * 커스텀 메트릭 테스트 위젯 (다중 데이터 소스 지원)
+ * - 여러 REST API 연결 가능
+ * - 여러 Database 연결 가능
+ * - REST API + Database 혼합 가능
+ * - 데이터 자동 병합 후 집계
+ */
+export default function CustomMetricTestWidget({ element }: CustomMetricTestWidgetProps) {
+ const [metrics, setMetrics] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ console.log("🧪 CustomMetricTestWidget 렌더링!", element);
+
+ const metricConfig = element?.customMetricConfig?.metrics || [];
+
+ // 다중 데이터 소스 로딩
+ const loadMultipleDataSources = useCallback(async () => {
+ const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
+
+ if (!dataSources || dataSources.length === 0) {
+ console.log("⚠️ 데이터 소스가 없습니다.");
+ return;
+ }
+
+ console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
+ setLoading(true);
+ setError(null);
+
+ try {
+ // 모든 데이터 소스를 병렬로 로딩
+ const results = await Promise.allSettled(
+ 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 [];
+ } catch (err: any) {
+ console.error(`❌ 데이터 소스 "${source.name || source.id}" 로딩 실패:`, err);
+ return [];
+ }
+ })
+ );
+
+ // 성공한 데이터만 병합
+ const allRows: any[] = [];
+ results.forEach((result) => {
+ if (result.status === "fulfilled" && Array.isArray(result.value)) {
+ allRows.push(...result.value);
+ }
+ });
+
+ console.log(`✅ 총 ${allRows.length}개의 행 로딩 완료`);
+
+ // 메트릭 계산
+ const calculatedMetrics = metricConfig.map((metric) => ({
+ ...metric,
+ value: calculateMetric(allRows, metric.field, metric.aggregation),
+ }));
+
+ setMetrics(calculatedMetrics);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "데이터 로딩 실패");
+ } finally {
+ setLoading(false);
+ }
+ }, [element?.dataSources, element?.chartConfig?.dataSources, metricConfig]);
+
+ // REST API 데이터 로딩
+ const loadRestApiData = async (source: ChartDataSource): Promise => {
+ if (!source.endpoint) {
+ throw new Error("API endpoint가 없습니다.");
+ }
+
+ const params = new URLSearchParams();
+ if (source.queryParams) {
+ Object.entries(source.queryParams).forEach(([key, value]) => {
+ if (key && value) {
+ params.append(key, String(value));
+ }
+ });
+ }
+
+ const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ url: source.endpoint,
+ method: "GET",
+ headers: source.headers || {},
+ queryParams: Object.fromEntries(params),
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.message || "외부 API 호출 실패");
+ }
+
+ let processedData = result.data;
+
+ // JSON Path 처리
+ if (source.jsonPath) {
+ const paths = source.jsonPath.split(".");
+ for (const path of paths) {
+ if (processedData && typeof processedData === "object" && path in processedData) {
+ processedData = processedData[path];
+ } else {
+ throw new Error(`JSON Path "${source.jsonPath}"에서 데이터를 찾을 수 없습니다`);
+ }
+ }
+ }
+
+ return Array.isArray(processedData) ? processedData : [processedData];
+ };
+
+ // Database 데이터 로딩
+ const loadDatabaseData = async (source: ChartDataSource): Promise => {
+ if (!source.query) {
+ throw new Error("SQL 쿼리가 없습니다.");
+ }
+
+ if (source.connectionType === "external" && source.externalConnectionId) {
+ // 외부 DB
+ const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
+ const externalResult = await ExternalDbConnectionAPI.executeQuery(
+ parseInt(source.externalConnectionId),
+ source.query,
+ );
+
+ if (!externalResult.success || !externalResult.data) {
+ throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패");
+ }
+
+ const resultData = externalResult.data as unknown as {
+ rows: Record[];
+ };
+
+ return resultData.rows;
+ } else {
+ // 현재 DB
+ const { dashboardApi } = await import("@/lib/api/dashboard");
+ const result = await dashboardApi.executeQuery(source.query);
+
+ return result.rows;
+ }
+ };
+
+ // 초기 로드
+ useEffect(() => {
+ const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
+ if (dataSources && dataSources.length > 0 && metricConfig.length > 0) {
+ loadMultipleDataSources();
+ }
+ }, [element?.dataSources, element?.chartConfig?.dataSources, loadMultipleDataSources, metricConfig]);
+
+ // 메트릭 카드 렌더링
+ const renderMetricCard = (metric: any, index: number) => {
+ const color = colorMap[metric.color as keyof typeof colorMap] || colorMap.gray;
+ const formattedValue = metric.value.toLocaleString(undefined, {
+ minimumFractionDigits: metric.decimals || 0,
+ maximumFractionDigits: metric.decimals || 0,
+ });
+
+ return (
+
+
+
+
{metric.label}
+
+ {formattedValue}
+ {metric.unit && {metric.unit}}
+
+
+
+
+ );
+ };
+
+ return (
+
+ {/* 헤더 */}
+
+
+
+ {element?.customTitle || "커스텀 메트릭 (다중 데이터 소스)"}
+
+
+ {(element?.dataSources || element?.chartConfig?.dataSources)?.length || 0}개 데이터 소스 연결됨
+
+
+ {loading &&
}
+
+
+ {/* 컨텐츠 */}
+
+ {error ? (
+
+ ) : !(element?.dataSources || element?.chartConfig?.dataSources) || (element?.dataSources || element?.chartConfig?.dataSources)?.length === 0 ? (
+
+ ) : metricConfig.length === 0 ? (
+
+ ) : (
+
+ {metrics.map((metric, index) => renderMetricCard(metric, index))}
+
+ )}
+
+
+ );
+}
+
diff --git a/frontend/components/dashboard/widgets/ListTestWidget.tsx b/frontend/components/dashboard/widgets/ListTestWidget.tsx
new file mode 100644
index 00000000..b5dceead
--- /dev/null
+++ b/frontend/components/dashboard/widgets/ListTestWidget.tsx
@@ -0,0 +1,353 @@
+"use client";
+
+import React, { useState, useEffect, useCallback } from "react";
+import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/types";
+import { Button } from "@/components/ui/button";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
+import { Card } from "@/components/ui/card";
+import { Loader2 } from "lucide-react";
+
+interface ListTestWidgetProps {
+ element: DashboardElement;
+}
+
+interface QueryResult {
+ columns: string[];
+ rows: Record[];
+ totalRows: number;
+ executionTime: number;
+}
+
+/**
+ * 리스트 테스트 위젯 (다중 데이터 소스 지원)
+ * - 여러 REST API 연결 가능
+ * - 여러 Database 연결 가능
+ * - REST API + Database 혼합 가능
+ * - 데이터 자동 병합
+ */
+export function ListTestWidget({ element }: ListTestWidgetProps) {
+ const [data, setData] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+
+ console.log("🧪 ListTestWidget 렌더링!", element);
+
+ const config = element.listConfig || {
+ columnMode: "auto",
+ viewMode: "table",
+ columns: [],
+ pageSize: 10,
+ enablePagination: true,
+ showHeader: true,
+ stripedRows: true,
+ compactMode: false,
+ cardColumns: 3,
+ };
+
+ // 다중 데이터 소스 로딩
+ const loadMultipleDataSources = useCallback(async () => {
+ const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
+
+ if (!dataSources || dataSources.length === 0) {
+ console.log("⚠️ 데이터 소스가 없습니다.");
+ return;
+ }
+
+ console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ // 모든 데이터 소스를 병렬로 로딩
+ const results = await Promise.allSettled(
+ 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 { columns: [], rows: [] };
+ } catch (err: any) {
+ console.error(`❌ 데이터 소스 "${source.name || source.id}" 로딩 실패:`, err);
+ return { columns: [], rows: [] };
+ }
+ })
+ );
+
+ // 성공한 데이터만 병합
+ const allColumns = new Set();
+ const allRows: Record[] = [];
+
+ results.forEach((result, index) => {
+ if (result.status === "fulfilled") {
+ const { columns, rows } = result.value;
+
+ // 컬럼 수집
+ columns.forEach((col: string) => allColumns.add(col));
+
+ // 행 병합 (소스 정보 추가)
+ const sourceName = dataSources[index].name || dataSources[index].id || `소스 ${index + 1}`;
+ rows.forEach((row: any) => {
+ allRows.push({
+ ...row,
+ _source: sourceName,
+ });
+ });
+ }
+ });
+
+ const finalColumns = Array.from(allColumns);
+
+ // _source 컬럼을 맨 앞으로
+ const sortedColumns = finalColumns.includes("_source")
+ ? ["_source", ...finalColumns.filter((c) => c !== "_source")]
+ : finalColumns;
+
+ setData({
+ columns: sortedColumns,
+ rows: allRows,
+ totalRows: allRows.length,
+ executionTime: 0,
+ });
+
+ console.log(`✅ 총 ${allRows.length}개의 행 로딩 완료`);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "데이터 로딩 실패");
+ } finally {
+ setIsLoading(false);
+ }
+ }, [element?.dataSources, element?.chartConfig?.dataSources]);
+
+ // REST API 데이터 로딩
+ const loadRestApiData = async (source: ChartDataSource): Promise<{ columns: string[]; rows: any[] }> => {
+ if (!source.endpoint) {
+ throw new Error("API endpoint가 없습니다.");
+ }
+
+ const params = new URLSearchParams();
+ if (source.queryParams) {
+ Object.entries(source.queryParams).forEach(([key, value]) => {
+ if (key && value) {
+ params.append(key, String(value));
+ }
+ });
+ }
+
+ const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ url: source.endpoint,
+ method: "GET",
+ headers: source.headers || {},
+ queryParams: Object.fromEntries(params),
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const result = await response.json();
+
+ if (!result.success) {
+ throw new Error(result.message || "외부 API 호출 실패");
+ }
+
+ let processedData = result.data;
+
+ // JSON Path 처리
+ if (source.jsonPath) {
+ const paths = source.jsonPath.split(".");
+ for (const path of paths) {
+ if (processedData && typeof processedData === "object" && path in processedData) {
+ processedData = processedData[path];
+ } else {
+ throw new Error(`JSON Path "${source.jsonPath}"에서 데이터를 찾을 수 없습니다`);
+ }
+ }
+ }
+
+ const rows = Array.isArray(processedData) ? processedData : [processedData];
+ const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
+
+ return { columns, rows };
+ };
+
+ // Database 데이터 로딩
+ const loadDatabaseData = async (source: ChartDataSource): Promise<{ columns: string[]; rows: any[] }> => {
+ if (!source.query) {
+ throw new Error("SQL 쿼리가 없습니다.");
+ }
+
+ if (source.connectionType === "external" && source.externalConnectionId) {
+ // 외부 DB
+ const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
+ const externalResult = await ExternalDbConnectionAPI.executeQuery(
+ parseInt(source.externalConnectionId),
+ source.query,
+ );
+
+ if (!externalResult.success || !externalResult.data) {
+ throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패");
+ }
+
+ const resultData = externalResult.data as unknown as {
+ columns: string[];
+ rows: Record[];
+ };
+
+ return {
+ columns: resultData.columns,
+ rows: resultData.rows,
+ };
+ } else {
+ // 현재 DB
+ const { dashboardApi } = await import("@/lib/api/dashboard");
+ const result = await dashboardApi.executeQuery(source.query);
+
+ return {
+ columns: result.columns,
+ rows: result.rows,
+ };
+ }
+ };
+
+ // 초기 로드
+ useEffect(() => {
+ const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
+ if (dataSources && dataSources.length > 0) {
+ loadMultipleDataSources();
+ }
+ }, [element?.dataSources, element?.chartConfig?.dataSources, loadMultipleDataSources]);
+
+ // 페이지네이션
+ const pageSize = config.pageSize || 10;
+ const totalPages = data ? Math.ceil(data.totalRows / pageSize) : 0;
+ const startIndex = (currentPage - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedRows = data?.rows.slice(startIndex, endIndex) || [];
+
+ // 테이블 뷰
+ const renderTable = () => (
+
+
+ {config.showHeader && (
+
+
+ {data?.columns.map((col) => (
+
+ {col}
+
+ ))}
+
+
+ )}
+
+ {paginatedRows.map((row, idx) => (
+
+ {data?.columns.map((col) => (
+
+ {String(row[col] ?? "")}
+
+ ))}
+
+ ))}
+
+
+
+ );
+
+ // 카드 뷰
+ const renderCards = () => (
+
+ {paginatedRows.map((row, idx) => (
+
+ {data?.columns.map((col) => (
+
+ {col}:
+ {String(row[col] ?? "")}
+
+ ))}
+
+ ))}
+
+ );
+
+ return (
+
+ {/* 헤더 */}
+
+
+
+ {element?.customTitle || "리스트 테스트 (다중 데이터 소스)"}
+
+
+ {(element?.dataSources || element?.chartConfig?.dataSources)?.length || 0}개 데이터 소스 연결됨
+
+
+ {isLoading &&
}
+
+
+ {/* 컨텐츠 */}
+
+ {error ? (
+
+ ) : !(element?.dataSources || element?.chartConfig?.dataSources) || (element?.dataSources || element?.chartConfig?.dataSources)?.length === 0 ? (
+
+ ) : !data || data.rows.length === 0 ? (
+
+ ) : config.viewMode === "card" ? (
+ renderCards()
+ ) : (
+ renderTable()
+ )}
+
+
+ {/* 페이지네이션 */}
+ {config.enablePagination && data && data.rows.length > 0 && totalPages > 1 && (
+
+
+ 총 {data.totalRows}개 항목 (페이지 {currentPage}/{totalPages})
+
+
+
+
+
+
+ )}
+
+ );
+}
+
diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx
index b56eeccd..500f379f 100644
--- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx
+++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useEffect, useState, useCallback } from "react";
+import React, { useEffect, useState, useCallback, useMemo } from "react";
import dynamic from "next/dynamic";
import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/types";
import { Loader2 } from "lucide-react";
@@ -64,10 +64,14 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
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]);
+
// 다중 데이터 소스 로딩
const loadMultipleDataSources = useCallback(async () => {
- // dataSources는 element.dataSources 또는 chartConfig.dataSources에서 로드
- const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
+ const dataSourcesList = dataSources;
if (!dataSources || dataSources.length === 0) {
console.log("⚠️ 데이터 소스가 없습니다.");
@@ -138,10 +142,12 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
} finally {
setLoading(false);
}
- }, [element?.dataSources]);
+ }, [dataSources]);
// REST API 데이터 로딩
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가 없습니다.");
}
@@ -200,7 +206,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const parsedData = parseTextData(data.text);
if (parsedData.length > 0) {
console.log(`✅ CSV 파싱 성공: ${parsedData.length}개 행`);
- return convertToMapData(parsedData, source.name || source.id || "API");
+ return convertToMapData(parsedData, source.name || source.id || "API", source.mapDisplayType);
}
}
@@ -220,34 +226,38 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// Database 데이터 로딩
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 쿼리가 없습니다.");
}
- const response = await fetch("/api/dashboards/query", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- credentials: "include",
- body: JSON.stringify({
- connectionType: source.connectionType || "current",
- externalConnectionId: source.externalConnectionId,
- query: source.query,
- }),
- });
+ let rows: any[] = [];
- if (!response.ok) {
- throw new Error(`데이터베이스 쿼리 실패: ${response.status}`);
+ if (source.connectionType === "external" && source.externalConnectionId) {
+ // 외부 DB
+ const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
+ const externalResult = await ExternalDbConnectionAPI.executeQuery(
+ parseInt(source.externalConnectionId),
+ source.query
+ );
+
+ if (!externalResult.success || !externalResult.data) {
+ throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패");
+ }
+
+ 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 result = await response.json();
-
- if (!result.success) {
- throw new Error(result.message || "쿼리 실패");
- }
-
- const rows = result.data || [];
// 마커와 폴리곤으로 변환 (mapDisplayType 전달)
return convertToMapData(rows, source.name || source.id || "Database", source.mapDisplayType);
@@ -311,7 +321,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 데이터를 마커와 폴리곤으로 변환
const convertToMapData = (rows: any[], sourceName: string, mapDisplayType?: "auto" | "marker" | "polygon"): { markers: MarkerData[]; polygons: PolygonData[] } => {
- console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행, 표시 방식:", mapDisplayType || "auto");
+ console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행");
+ console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`);
if (rows.length === 0) return { markers: [], polygons: [] };
@@ -398,7 +409,28 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
}
}
- if (lat !== undefined && lng !== undefined) {
+ // mapDisplayType이 "polygon"이면 무조건 폴리곤으로 처리
+ if (mapDisplayType === "polygon") {
+ const regionName = row.name || row.subRegion || row.region || row.area;
+ if (regionName) {
+ console.log(` 🔷 강제 폴리곤 모드: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`);
+ polygons.push({
+ id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`,
+ name: regionName,
+ coordinates: [], // GeoJSON에서 좌표를 가져올 것
+ status: row.status || row.level,
+ description: row.description || JSON.stringify(row, null, 2),
+ source: sourceName,
+ color: getColorByStatus(row.status || row.level),
+ });
+ } else {
+ console.log(` ⚠️ 강제 폴리곤 모드지만 지역명 없음 - 스킵`);
+ }
+ return; // 폴리곤으로 처리했으므로 마커로는 추가하지 않음
+ }
+
+ // 위도/경도가 있고 marker 모드가 아니면 마커로 처리
+ if (lat !== undefined && lng !== undefined && mapDisplayType !== "polygon") {
console.log(` → 마커로 처리: (${lat}, ${lng})`);
markers.push({
id: `${sourceName}-marker-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성
@@ -681,7 +713,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
setMarkers([]);
setPolygons([]);
}
- }, [JSON.stringify(element?.dataSources || element?.chartConfig?.dataSources), loadMultipleDataSources]);
+ }, [dataSources, loadMultipleDataSources]);
// 타일맵 URL (chartConfig에서 가져오기)
const tileMapUrl = element?.chartConfig?.tileMapUrl ||
@@ -741,12 +773,45 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
{
- const regionName = feature?.properties?.CTP_KOR_NM || feature?.properties?.SIG_KOR_NM;
- const matchingPolygon = polygons.find(p =>
- p.name === regionName ||
- p.name?.includes(regionName) ||
- regionName?.includes(p.name)
- );
+ const ctpName = feature?.properties?.CTP_KOR_NM; // 시/도명 (예: 경상북도)
+ const sigName = feature?.properties?.SIG_KOR_NM; // 시/군/구명 (예: 군위군)
+
+ // 폴리곤 매칭 (시/군/구명 우선, 없으면 시/도명)
+ const matchingPolygon = polygons.find(p => {
+ if (!p.name) return false;
+
+ // 정확한 매칭
+ if (p.name === sigName) {
+ console.log(`✅ 정확 매칭: ${p.name} === ${sigName}`);
+ return true;
+ }
+ if (p.name === ctpName) {
+ console.log(`✅ 정확 매칭: ${p.name} === ${ctpName}`);
+ return true;
+ }
+
+ // 부분 매칭 (GeoJSON 지역명에 폴리곤 이름이 포함되는지)
+ if (sigName && sigName.includes(p.name)) {
+ console.log(`✅ 부분 매칭: ${sigName} includes ${p.name}`);
+ return true;
+ }
+ if (ctpName && ctpName.includes(p.name)) {
+ console.log(`✅ 부분 매칭: ${ctpName} includes ${p.name}`);
+ return true;
+ }
+
+ // 역방향 매칭 (폴리곤 이름에 GeoJSON 지역명이 포함되는지)
+ if (sigName && p.name.includes(sigName)) {
+ console.log(`✅ 역방향 매칭: ${p.name} includes ${sigName}`);
+ return true;
+ }
+ if (ctpName && p.name.includes(ctpName)) {
+ console.log(`✅ 역방향 매칭: ${p.name} includes ${ctpName}`);
+ return true;
+ }
+
+ return false;
+ });
if (matchingPolygon) {
return {
@@ -763,12 +828,18 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
};
}}
onEachFeature={(feature: any, layer: any) => {
- const regionName = feature?.properties?.CTP_KOR_NM || feature?.properties?.SIG_KOR_NM;
- const matchingPolygon = polygons.find(p =>
- p.name === regionName ||
- p.name?.includes(regionName) ||
- regionName?.includes(p.name)
- );
+ const ctpName = feature?.properties?.CTP_KOR_NM;
+ const sigName = feature?.properties?.SIG_KOR_NM;
+
+ 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;
+ if (ctpName && ctpName.includes(p.name)) return true;
+ if (sigName && p.name.includes(sigName)) return true;
+ if (ctpName && p.name.includes(ctpName)) return true;
+ return false;
+ });
if (matchingPolygon) {
layer.bindPopup(`