디벨롭
This commit is contained in:
parent
5b394473f4
commit
c52e77f37d
|
|
@ -78,6 +78,16 @@ const ChartTestWidget = dynamic(() => import("@/components/dashboard/widgets/Cha
|
|||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const ListTestWidget = dynamic(() => import("@/components/dashboard/widgets/ListTestWidget").then((mod) => ({ default: mod.ListTestWidget })), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
});
|
||||
|
||||
const CustomMetricTestWidget = dynamic(() => import("@/components/dashboard/widgets/CustomMetricTestWidget"), {
|
||||
ssr: false,
|
||||
loading: () => <div className="flex h-full items-center justify-center text-sm text-gray-500">로딩 중...</div>,
|
||||
});
|
||||
|
||||
// 범용 상태 요약 위젯 (차량, 배송 등 모든 상태 위젯 통합)
|
||||
const StatusSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/StatusSummaryWidget"), {
|
||||
ssr: false,
|
||||
|
|
@ -884,6 +894,16 @@ export function CanvasElement({
|
|||
<div className="widget-interactive-area h-full w-full">
|
||||
<ChartTestWidget element={element} />
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "list-test" ? (
|
||||
// 🧪 테스트용 리스트 위젯 (다중 데이터 소스)
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<ListTestWidget element={element} />
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "custom-metric-test" ? (
|
||||
// 🧪 테스트용 커스텀 메트릭 위젯 (다중 데이터 소스)
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
<CustomMetricTestWidget element={element} />
|
||||
</div>
|
||||
) : element.type === "widget" && element.subtype === "vehicle-map" ? (
|
||||
// 차량 위치 지도 위젯 렌더링 (구버전 - 호환용)
|
||||
<div className="widget-interactive-area h-full w-full">
|
||||
|
|
|
|||
|
|
@ -185,6 +185,8 @@ export function DashboardTopMenu({
|
|||
<SelectLabel>🧪 테스트 위젯 (다중 데이터 소스)</SelectLabel>
|
||||
<SelectItem value="map-test-v2">🧪 지도 테스트 V2</SelectItem>
|
||||
<SelectItem value="chart-test">🧪 차트 테스트</SelectItem>
|
||||
<SelectItem value="list-test">🧪 리스트 테스트</SelectItem>
|
||||
<SelectItem value="custom-metric-test">🧪 커스텀 메트릭 테스트</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>데이터 위젯</SelectLabel>
|
||||
|
|
|
|||
|
|
@ -222,7 +222,11 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
|
|||
(element.subtype === "clock" || element.subtype === "calendar" || isSelfContainedWidget);
|
||||
|
||||
// 다중 데이터 소스 테스트 위젯
|
||||
const isMultiDataSourceWidget = element.subtype === "map-test-v2" || element.subtype === "chart-test";
|
||||
const isMultiDataSourceWidget =
|
||||
element.subtype === "map-test-v2" ||
|
||||
element.subtype === "chart-test" ||
|
||||
element.subtype === "list-test" ||
|
||||
element.subtype === "custom-metric-test";
|
||||
|
||||
// 저장 가능 여부 확인
|
||||
const isPieChart = element.subtype === "pie" || element.subtype === "donut";
|
||||
|
|
|
|||
|
|
@ -4,10 +4,16 @@ import React, { useState } from "react";
|
|||
import { ChartDataSource } from "@/components/admin/dashboard/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { Plus, Trash2, Database, Globe } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import MultiApiConfig from "./MultiApiConfig";
|
||||
import MultiDatabaseConfig from "./MultiDatabaseConfig";
|
||||
|
||||
|
|
@ -25,18 +31,20 @@ export default function MultiDataSourceConfig({
|
|||
);
|
||||
const [previewData, setPreviewData] = useState<any[]>([]);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
const [showAddMenu, setShowAddMenu] = useState(false);
|
||||
|
||||
// 새 데이터 소스 추가
|
||||
const handleAddDataSource = () => {
|
||||
// 새 데이터 소스 추가 (타입 지정)
|
||||
const handleAddDataSource = (type: "api" | "database") => {
|
||||
const newId = Date.now().toString();
|
||||
const newSource: ChartDataSource = {
|
||||
id: newId,
|
||||
name: `데이터 소스 ${dataSources.length + 1}`,
|
||||
type: "api",
|
||||
name: `${type === "api" ? "REST API" : "Database"} ${dataSources.length + 1}`,
|
||||
type,
|
||||
};
|
||||
|
||||
onChange([...dataSources, newSource]);
|
||||
setActiveTab(newId);
|
||||
setShowAddMenu(false);
|
||||
};
|
||||
|
||||
// 데이터 소스 삭제
|
||||
|
|
|
|||
|
|
@ -65,28 +65,35 @@ export default function MultiDatabaseConfig({ dataSource, onChange }: MultiDatab
|
|||
setTestResult(null);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/dashboards/query", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
connectionType: dataSource.connectionType || "current",
|
||||
externalConnectionId: dataSource.externalConnectionId,
|
||||
query: dataSource.query,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
const rowCount = Array.isArray(result.data) ? result.data.length : 0;
|
||||
// dashboardApi 사용 (인증 토큰 자동 포함)
|
||||
const { dashboardApi } = await import("@/lib/api/dashboard");
|
||||
|
||||
if (dataSource.connectionType === "external" && dataSource.externalConnectionId) {
|
||||
// 외부 DB
|
||||
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
|
||||
const result = await ExternalDbConnectionAPI.executeQuery(
|
||||
parseInt(dataSource.externalConnectionId),
|
||||
dataSource.query
|
||||
);
|
||||
|
||||
if (result.success && result.data) {
|
||||
const rowCount = Array.isArray(result.data.rows) ? result.data.rows.length : 0;
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: "쿼리 실행 성공",
|
||||
rowCount,
|
||||
});
|
||||
} else {
|
||||
setTestResult({ success: false, message: result.message || "쿼리 실행 실패" });
|
||||
}
|
||||
} else {
|
||||
// 현재 DB
|
||||
const result = await dashboardApi.executeQuery(dataSource.query);
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: "쿼리 실행 성공",
|
||||
rowCount,
|
||||
rowCount: result.rowCount || 0,
|
||||
});
|
||||
} else {
|
||||
setTestResult({ success: false, message: result.message || "쿼리 실행 실패" });
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTestResult({ success: false, message: error.message || "네트워크 오류" });
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ export type ElementSubtype =
|
|||
| "map-test" // 🧪 지도 테스트 위젯 (REST API 지원)
|
||||
| "map-test-v2" // 🧪 지도 테스트 V2 (다중 데이터 소스)
|
||||
| "chart-test" // 🧪 차트 테스트 (다중 데이터 소스)
|
||||
| "list-test" // 🧪 리스트 테스트 (다중 데이터 소스)
|
||||
| "custom-metric-test" // 🧪 커스텀 메트릭 테스트 (다중 데이터 소스)
|
||||
| "delivery-status"
|
||||
| "status-summary" // 범용 상태 카드 (통합)
|
||||
// | "list-summary" // 범용 목록 카드 (다른 분 작업 중 - 임시 주석)
|
||||
|
|
@ -153,6 +155,9 @@ export interface ChartDataSource {
|
|||
}
|
||||
|
||||
export interface ChartConfig {
|
||||
// 다중 데이터 소스 (테스트 위젯용)
|
||||
dataSources?: ChartDataSource[]; // 여러 데이터 소스 (REST API + Database 혼합 가능)
|
||||
|
||||
// 축 매핑
|
||||
xAxis?: string; // X축 필드명
|
||||
yAxis?: string | string[]; // Y축 필드명 (다중 가능)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ const MapSummaryWidget = dynamic(() => import("./widgets/MapSummaryWidget"), { s
|
|||
const MapTestWidget = dynamic(() => import("./widgets/MapTestWidget"), { ssr: false });
|
||||
const MapTestWidgetV2 = dynamic(() => import("./widgets/MapTestWidgetV2"), { ssr: false });
|
||||
const ChartTestWidget = dynamic(() => import("./widgets/ChartTestWidget"), { ssr: false });
|
||||
const ListTestWidget = dynamic(() => import("./widgets/ListTestWidget").then((mod) => ({ default: mod.ListTestWidget })), { ssr: false });
|
||||
const CustomMetricTestWidget = dynamic(() => import("./widgets/CustomMetricTestWidget"), { ssr: false });
|
||||
const StatusSummaryWidget = dynamic(() => import("./widgets/StatusSummaryWidget"), { ssr: false });
|
||||
const RiskAlertWidget = dynamic(() => import("./widgets/RiskAlertWidget"), { ssr: false });
|
||||
const WeatherWidget = dynamic(() => import("./widgets/WeatherWidget"), { ssr: false });
|
||||
|
|
@ -85,6 +87,10 @@ function renderWidget(element: DashboardElement) {
|
|||
return <MapTestWidgetV2 element={element} />;
|
||||
case "chart-test":
|
||||
return <ChartTestWidget element={element} />;
|
||||
case "list-test":
|
||||
return <ListTestWidget element={element} />;
|
||||
case "custom-metric-test":
|
||||
return <CustomMetricTestWidget element={element} />;
|
||||
case "risk-alert":
|
||||
return <RiskAlertWidget element={element} />;
|
||||
case "calendar":
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
|
|||
|
||||
// 다중 데이터 소스 로딩
|
||||
const loadMultipleDataSources = useCallback(async () => {
|
||||
const dataSources = element?.dataSources;
|
||||
// dataSources는 element.dataSources 또는 chartConfig.dataSources에서 로드
|
||||
const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
|
||||
|
||||
if (!dataSources || dataSources.length === 0) {
|
||||
console.log("⚠️ 데이터 소스가 없습니다.");
|
||||
|
|
@ -174,10 +175,11 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (element?.dataSources && element.dataSources.length > 0) {
|
||||
const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
|
||||
if (dataSources && dataSources.length > 0) {
|
||||
loadMultipleDataSources();
|
||||
}
|
||||
}, [element?.dataSources, loadMultipleDataSources]);
|
||||
}, [element?.dataSources, element?.chartConfig?.dataSources, loadMultipleDataSources]);
|
||||
|
||||
const chartType = element?.subtype || "line";
|
||||
const chartConfig = element?.chartConfig || {};
|
||||
|
|
@ -265,7 +267,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
|
|||
{element?.customTitle || "차트 테스트 (다중 데이터 소스)"}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{element?.dataSources?.length || 0}개 데이터 소스 연결됨
|
||||
{(element?.dataSources || element?.chartConfig?.dataSources)?.length || 0}개 데이터 소스 연결됨
|
||||
</p>
|
||||
</div>
|
||||
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
|
|
@ -276,7 +278,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
|
|||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
) : !element?.dataSources || element.dataSources.length === 0 ? (
|
||||
) : !(element?.dataSources || element?.chartConfig?.dataSources) || (element?.dataSources || element?.chartConfig?.dataSources)?.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
데이터 소스를 연결해주세요
|
||||
|
|
|
|||
|
|
@ -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<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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<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}"에서 데이터를 찾을 수 없습니다`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.isArray(processedData) ? processedData : [processedData];
|
||||
};
|
||||
|
||||
// Database 데이터 로딩
|
||||
const loadDatabaseData = async (source: ChartDataSource): Promise<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 {
|
||||
rows: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
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 (
|
||||
<div
|
||||
key={index}
|
||||
className={`rounded-lg border ${color.border} ${color.bg} p-4 shadow-sm transition-all hover:shadow-md`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs font-medium text-muted-foreground">{metric.label}</p>
|
||||
<p className={`mt-1 text-2xl font-bold ${color.text}`}>
|
||||
{formattedValue}
|
||||
{metric.unit && <span className="ml-1 text-sm">{metric.unit}</span>}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col rounded-lg border bg-card shadow-sm">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{element?.customTitle || "커스텀 메트릭 (다중 데이터 소스)"}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(element?.dataSources || element?.chartConfig?.dataSources)?.length || 0}개 데이터 소스 연결됨
|
||||
</p>
|
||||
</div>
|
||||
{loading && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
</div>
|
||||
|
||||
{/* 컨텐츠 */}
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{error ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
) : !(element?.dataSources || element?.chartConfig?.dataSources) || (element?.dataSources || element?.chartConfig?.dataSources)?.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
데이터 소스를 연결해주세요
|
||||
</p>
|
||||
</div>
|
||||
) : metricConfig.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
메트릭을 설정해주세요
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{metrics.map((metric, index) => renderMetricCard(metric, index))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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<string, any>[];
|
||||
totalRows: number;
|
||||
executionTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 리스트 테스트 위젯 (다중 데이터 소스 지원)
|
||||
* - 여러 REST API 연결 가능
|
||||
* - 여러 Database 연결 가능
|
||||
* - REST API + Database 혼합 가능
|
||||
* - 데이터 자동 병합
|
||||
*/
|
||||
export function ListTestWidget({ element }: ListTestWidgetProps) {
|
||||
const [data, setData] = useState<QueryResult | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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<string>();
|
||||
const allRows: Record<string, any>[] = [];
|
||||
|
||||
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<string, unknown>[];
|
||||
};
|
||||
|
||||
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 = () => (
|
||||
<div className="overflow-auto">
|
||||
<Table>
|
||||
{config.showHeader && (
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{data?.columns.map((col) => (
|
||||
<TableHead key={col} className="whitespace-nowrap">
|
||||
{col}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
)}
|
||||
<TableBody>
|
||||
{paginatedRows.map((row, idx) => (
|
||||
<TableRow key={idx} className={config.stripedRows && idx % 2 === 0 ? "bg-muted/50" : ""}>
|
||||
{data?.columns.map((col) => (
|
||||
<TableCell key={col} className="whitespace-nowrap">
|
||||
{String(row[col] ?? "")}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 카드 뷰
|
||||
const renderCards = () => (
|
||||
<div className={`grid gap-4 grid-cols-1 md:grid-cols-${config.cardColumns || 3}`}>
|
||||
{paginatedRows.map((row, idx) => (
|
||||
<Card key={idx} className="p-4">
|
||||
{data?.columns.map((col) => (
|
||||
<div key={col} className="mb-2">
|
||||
<span className="font-semibold">{col}: </span>
|
||||
<span>{String(row[col] ?? "")}</span>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col rounded-lg border bg-card shadow-sm">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{element?.customTitle || "리스트 테스트 (다중 데이터 소스)"}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{(element?.dataSources || element?.chartConfig?.dataSources)?.length || 0}개 데이터 소스 연결됨
|
||||
</p>
|
||||
</div>
|
||||
{isLoading && <Loader2 className="h-4 w-4 animate-spin" />}
|
||||
</div>
|
||||
|
||||
{/* 컨텐츠 */}
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{error ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
) : !(element?.dataSources || element?.chartConfig?.dataSources) || (element?.dataSources || element?.chartConfig?.dataSources)?.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
데이터 소스를 연결해주세요
|
||||
</p>
|
||||
</div>
|
||||
) : !data || data.rows.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
데이터가 없습니다
|
||||
</p>
|
||||
</div>
|
||||
) : config.viewMode === "card" ? (
|
||||
renderCards()
|
||||
) : (
|
||||
renderTable()
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
{config.enablePagination && data && data.rows.length > 0 && totalPages > 1 && (
|
||||
<div className="flex items-center justify-between border-t p-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
총 {data.totalRows}개 항목 (페이지 {currentPage}/{totalPages})
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
이전
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
다음
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -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<string, unknown>[];
|
||||
};
|
||||
|
||||
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) {
|
|||
<GeoJSON
|
||||
data={geoJsonData}
|
||||
style={(feature: 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) {
|
||||
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(`
|
||||
|
|
|
|||
Loading…
Reference in New Issue