From 8edd5e4ca6bcddcd8a6df6d15b0538a261fffc67 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 29 Oct 2025 11:52:18 +0900 Subject: [PATCH 01/12] =?UTF-8?q?api=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/charts/ChartRenderer.tsx | 3 ++- .../admin/dashboard/data-sources/ApiConfig.tsx | 3 ++- .../admin/dashboard/data-sources/MultiApiConfig.tsx | 3 ++- .../admin/dashboard/widgets/ListWidget.tsx | 3 ++- .../components/dashboard/widgets/ChartTestWidget.tsx | 3 ++- .../dashboard/widgets/CustomMetricTestWidget.tsx | 12 ++++++++---- .../components/dashboard/widgets/ListTestWidget.tsx | 3 ++- .../dashboard/widgets/MapSummaryWidget.tsx | 3 ++- .../components/dashboard/widgets/MapTestWidgetV2.tsx | 3 ++- .../dashboard/widgets/RiskAlertTestWidget.tsx | 3 ++- 10 files changed, 26 insertions(+), 13 deletions(-) diff --git a/frontend/components/admin/dashboard/charts/ChartRenderer.tsx b/frontend/components/admin/dashboard/charts/ChartRenderer.tsx index 3cd5afbe..ddb8e919 100644 --- a/frontend/components/admin/dashboard/charts/ChartRenderer.tsx +++ b/frontend/components/admin/dashboard/charts/ChartRenderer.tsx @@ -7,6 +7,7 @@ import { transformQueryResultToChartData } from "../utils/chartDataTransform"; import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection"; import { dashboardApi } from "@/lib/api/dashboard"; import { applyQueryFilters } from "../utils/queryHelpers"; +import { getApiUrl } from "@/lib/utils/apiUrl"; interface ChartRendererProps { element: DashboardElement; @@ -84,7 +85,7 @@ export function ChartRenderer({ element, data, width, height = 200 }: ChartRende }); } - const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", { + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx b/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx index a8b2b74c..65905ea3 100644 --- a/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/ApiConfig.tsx @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"; import { Plus, X, Play, AlertCircle } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ExternalDbConnectionAPI, ExternalApiConnection } from "@/lib/api/externalDbConnection"; +import { getApiUrl } from "@/lib/utils/apiUrl"; // 개별 API 소스 인터페이스 interface ApiSource { @@ -254,7 +255,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps }); // 백엔드 프록시를 통한 외부 API 호출 (CORS 우회) - const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", { + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx index 0a2d9dd4..05288c96 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx @@ -8,6 +8,7 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Plus, Trash2, Loader2, CheckCircle, XCircle } from "lucide-react"; import { ExternalDbConnectionAPI, ExternalApiConnection } from "@/lib/api/externalDbConnection"; +import { getApiUrl } from "@/lib/utils/apiUrl"; interface MultiApiConfigProps { dataSource: ChartDataSource; @@ -220,7 +221,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M } }); - const response = await fetch("/api/dashboards/fetch-external-api", { + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", diff --git a/frontend/components/admin/dashboard/widgets/ListWidget.tsx b/frontend/components/admin/dashboard/widgets/ListWidget.tsx index ab721c4b..f529c0a9 100644 --- a/frontend/components/admin/dashboard/widgets/ListWidget.tsx +++ b/frontend/components/admin/dashboard/widgets/ListWidget.tsx @@ -5,6 +5,7 @@ import { DashboardElement, QueryResult, ListWidgetConfig } from "../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 { getApiUrl } from "@/lib/utils/apiUrl"; interface ListWidgetProps { element: DashboardElement; @@ -58,7 +59,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { }); } - const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", { + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/components/dashboard/widgets/ChartTestWidget.tsx b/frontend/components/dashboard/widgets/ChartTestWidget.tsx index 412e4961..22d7f2db 100644 --- a/frontend/components/dashboard/widgets/ChartTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ChartTestWidget.tsx @@ -5,6 +5,7 @@ import { DashboardElement, ChartDataSource, ChartData } from "@/components/admin import { Button } from "@/components/ui/button"; import { Loader2, RefreshCw } from "lucide-react"; import { applyColumnMapping } from "@/lib/utils/columnMapping"; +import { getApiUrl } from "@/lib/utils/apiUrl"; import { Chart } from "@/components/admin/dashboard/charts/Chart"; interface ChartTestWidgetProps { @@ -127,7 +128,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { }); } - const response = await fetch("/api/dashboards/fetch-external-api", { + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", diff --git a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx index 81dbc292..48a8c161 100644 --- a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx @@ -5,6 +5,7 @@ import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/ import { Button } from "@/components/ui/button"; import { Loader2, RefreshCw } from "lucide-react"; import { applyColumnMapping } from "@/lib/utils/columnMapping"; +import { getApiUrl } from "@/lib/utils/apiUrl"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; @@ -241,8 +242,8 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg }); // 🆕 자동 집계 로직: 집계 컬럼 이름으로 판단 (count, 개수, sum, avg 등) - const isAggregated = numericColumns.some((col) => - /count|개수|sum|합계|avg|평균|min|최소|max|최대|total|전체/i.test(col) + const isAggregated = numericColumns.some((col) => + /count|개수|sum|합계|avg|평균|min|최소|max|최대|total|전체/i.test(col), ); if (isAggregated && numericColumns.length > 0) { @@ -553,7 +554,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg console.log("🌐 API 호출:", source.endpoint, "파라미터:", Object.fromEntries(params)); - const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", { + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json", @@ -746,7 +747,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg return (
{/* 콘텐츠 영역 - 스크롤 가능하도록 개선 */} -
+
{/* 그룹별 카드 (활성화 시) */} {isGroupByMode && groupedCards.map((card, index) => { diff --git a/frontend/components/dashboard/widgets/ListTestWidget.tsx b/frontend/components/dashboard/widgets/ListTestWidget.tsx index 3b9d7256..d10ee6be 100644 --- a/frontend/components/dashboard/widgets/ListTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ListTestWidget.tsx @@ -7,6 +7,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { Card } from "@/components/ui/card"; import { Loader2, RefreshCw } from "lucide-react"; import { applyColumnMapping } from "@/lib/utils/columnMapping"; +import { getApiUrl } from "@/lib/utils/apiUrl"; interface ListTestWidgetProps { element: DashboardElement; @@ -155,7 +156,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { }); } - const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", { + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/components/dashboard/widgets/MapSummaryWidget.tsx b/frontend/components/dashboard/widgets/MapSummaryWidget.tsx index 3ad09305..227cee80 100644 --- a/frontend/components/dashboard/widgets/MapSummaryWidget.tsx +++ b/frontend/components/dashboard/widgets/MapSummaryWidget.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from "react"; import dynamic from "next/dynamic"; import { DashboardElement } from "@/components/admin/dashboard/types"; +import { getApiUrl } from "@/lib/utils/apiUrl"; import "leaflet/dist/leaflet.css"; // Leaflet 아이콘 경로 설정 (엑박 방지) @@ -105,7 +106,7 @@ export default function MapSummaryWidget({ element }: MapSummaryWidgetProps) { setTableName(extractedTableName); const token = localStorage.getItem("authToken"); - const response = await fetch("/api/dashboards/execute-query", { + const response = await fetch(getApiUrl("/api/dashboards/execute-query"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx index 767c4d01..cec70b7a 100644 --- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx +++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx @@ -6,6 +6,7 @@ import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/ import { Button } from "@/components/ui/button"; import { Loader2, RefreshCw } from "lucide-react"; import { applyColumnMapping } from "@/lib/utils/columnMapping"; +import { getApiUrl } from "@/lib/utils/apiUrl"; import "leaflet/dist/leaflet.css"; // Leaflet 아이콘 경로 설정 (엑박 방지) @@ -185,7 +186,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } // 백엔드 프록시를 통해 API 호출 - const response = await fetch("/api/dashboards/fetch-external-api", { + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx index 71f5d6b7..6ac1765a 100644 --- a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx +++ b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { RefreshCw, AlertTriangle, Cloud, Construction, Database as DatabaseIcon } from "lucide-react"; import { DashboardElement, ChartDataSource } from "@/components/admin/dashboard/types"; +import { getApiUrl } from "@/lib/utils/apiUrl"; type AlertType = "accident" | "weather" | "construction" | "system" | "security" | "other"; @@ -138,7 +139,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp console.log("🔍 원본 source.queryParams:", source.queryParams); console.log("🔍 원본 source.headers:", source.headers); - const response = await fetch("http://localhost:8080/api/dashboards/fetch-external-api", { + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ From 398c47618b669b77178a37e0771d465fed5e1ea7 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Wed, 29 Oct 2025 13:50:08 +0900 Subject: [PATCH 02/12] =?UTF-8?q?fix:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EC=B5=9C=EC=A2=85=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/DashboardTopMenu.tsx | 2 +- .../admin/dashboard/MultiChartConfigPanel.tsx | 13 +- .../dashboard/widgets/ChartTestWidget.tsx | 74 +++++---- .../widgets/CustomMetricTestWidget.tsx | 107 ++++++------- .../dashboard/widgets/ListTestWidget.tsx | 27 ++-- .../dashboard/widgets/MapTestWidgetV2.tsx | 128 ++++++++-------- .../dashboard/widgets/RiskAlertTestWidget.tsx | 141 +++++++++++++----- 7 files changed, 291 insertions(+), 201 deletions(-) diff --git a/frontend/components/admin/dashboard/DashboardTopMenu.tsx b/frontend/components/admin/dashboard/DashboardTopMenu.tsx index 12d73f98..1993e8ae 100644 --- a/frontend/components/admin/dashboard/DashboardTopMenu.tsx +++ b/frontend/components/admin/dashboard/DashboardTopMenu.tsx @@ -185,7 +185,7 @@ export function DashboardTopMenu({ 데이터 위젯 지도 - {/* 차트 */} {/* 주석 처리: 2025-10-29, 시기상조 */} + 테스트용 차트 위젯 리스트 통계 카드 리스크/알림 diff --git a/frontend/components/admin/dashboard/MultiChartConfigPanel.tsx b/frontend/components/admin/dashboard/MultiChartConfigPanel.tsx index 9a53f04d..52240720 100644 --- a/frontend/components/admin/dashboard/MultiChartConfigPanel.tsx +++ b/frontend/components/admin/dashboard/MultiChartConfigPanel.tsx @@ -157,11 +157,14 @@ export function MultiChartConfigPanel({ - 라인 차트 - 바 차트 - 영역 차트 - 파이 차트 - 도넛 차트 + 📈 라인 차트 + 📊 바 차트 + 📊 수평 바 차트 + 📊 누적 바 차트 + 📉 영역 차트 + 🥧 파이 차트 + 🍩 도넛 차트 + 🎨 콤보 차트
diff --git a/frontend/components/dashboard/widgets/ChartTestWidget.tsx b/frontend/components/dashboard/widgets/ChartTestWidget.tsx index 22d7f2db..ce8c119c 100644 --- a/frontend/components/dashboard/widgets/ChartTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ChartTestWidget.tsx @@ -22,7 +22,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { const containerRef = useRef(null); const [containerSize, setContainerSize] = useState({ width: 600, height: 400 }); - console.log("🧪 ChartTestWidget 렌더링 (D3 기반)!", element); + // console.log("🧪 ChartTestWidget 렌더링 (D3 기반)!", element); const dataSources = useMemo(() => { return element?.dataSources || element?.chartConfig?.dataSources; @@ -48,11 +48,11 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { const dataSources = element?.dataSources || element?.chartConfig?.dataSources; if (!dataSources || dataSources.length === 0) { - console.log("⚠️ 데이터 소스가 없습니다."); + // console.log("⚠️ 데이터 소스가 없습니다."); return; } - console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); + // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); setLoading(true); setError(null); @@ -60,7 +60,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { const results = await Promise.allSettled( dataSources.map(async (source) => { try { - console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); + // console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); if (source.type === "api") { return await loadRestApiData(source); @@ -87,7 +87,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { } }); - console.log(`✅ 총 ${allData.length}개의 데이터 로딩 완료`); + // console.log(`✅ 총 ${allData.length}개의 데이터 로딩 완료`); setData(allData); setLastRefreshTime(new Date()); } catch (err: any) { @@ -100,7 +100,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { // 수동 새로고침 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // console.log("🔄 수동 새로고침 버튼 클릭"); loadMultipleDataSources(); }, [loadMultipleDataSources]); @@ -212,15 +212,15 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadMultipleDataSources]); @@ -241,14 +241,20 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { // 병합 모드: 여러 데이터 소스를 하나로 합침 if (mergeMode && dataSourceConfigs.length > 1) { + // console.log("🔀 병합 모드 활성화"); + + // 모든 데이터 소스의 X축 필드 수집 (첫 번째 데이터 소스의 X축 사용) const baseConfig = dataSourceConfigs[0]; const xAxisField = baseConfig.xAxis; - const yAxisField = baseConfig.yAxis[0]; + + // console.log("📊 X축 필드:", xAxisField); - // X축 값 수집 - dataSourceConfigs.forEach((dsConfig) => { + // X축 값 수집 (모든 데이터 소스에서) + dataSourceConfigs.forEach((dsConfig, idx) => { const sourceName = dataSources?.find((ds) => ds.id === dsConfig.dataSourceId)?.name; const sourceData = data.filter((item) => item._source === sourceName); + // console.log(` 소스 ${idx + 1} (${sourceName}): ${sourceData.length}개 행`); + sourceData.forEach((item) => { if (item[xAxisField] !== undefined) { labels.add(String(item[xAxisField])); @@ -256,26 +262,36 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) { }); }); - // 데이터 병합 - const mergedData: number[] = []; - labels.forEach((label) => { - let totalValue = 0; - dataSourceConfigs.forEach((dsConfig) => { - const sourceName = dataSources?.find((ds) => ds.id === dsConfig.dataSourceId)?.name; - const sourceData = data.filter((item) => item._source === sourceName); - const matchingItem = sourceData.find((item) => String(item[xAxisField]) === label); - if (matchingItem && yAxisField) { - totalValue += parseFloat(matchingItem[yAxisField]) || 0; - } + // console.log("📍 수집된 X축 라벨:", Array.from(labels)); + + // 각 데이터 소스별로 데이터셋 생성 (병합하지 않고 각각 표시) + dataSourceConfigs.forEach((dsConfig, idx) => { + const sourceName = dataSources?.find((ds) => ds.id === dsConfig.dataSourceId)?.name || `소스 ${idx + 1}`; + const sourceData = data.filter((item) => item._source === sourceName); + + // 각 Y축 필드마다 데이터셋 생성 + (dsConfig.yAxis || []).forEach((yAxisField, yIdx) => { + const datasetData: number[] = []; + + labels.forEach((label) => { + const matchingItem = sourceData.find((item) => String(item[xAxisField]) === label); + const value = matchingItem && yAxisField ? parseFloat(matchingItem[yAxisField]) || 0 : 0; + datasetData.push(value); + }); + + // console.log(` 📈 ${sourceName} - ${yAxisField}: [${datasetData.join(", ")}]`); + + datasets.push({ + label: `${sourceName} - ${yAxisField}`, + data: datasetData, + backgroundColor: COLORS[(idx * 2 + yIdx) % COLORS.length], + borderColor: COLORS[(idx * 2 + yIdx) % COLORS.length], + type: dsConfig.chartType || chartType, // 데이터 소스별 차트 타입 + }); }); - mergedData.push(totalValue); }); - datasets.push({ - label: yAxisField, - data: mergedData, - color: COLORS[0], - }); + // console.log("✅ 병합 모드 데이터셋 생성 완료:", datasets.length, "개"); } else { // 일반 모드: 각 데이터 소스를 별도로 표시 dataSourceConfigs.forEach((dsConfig, index) => { diff --git a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx index 48a8c161..f2e6ebd7 100644 --- a/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx +++ b/frontend/components/dashboard/widgets/CustomMetricTestWidget.tsx @@ -64,7 +64,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const [selectedMetric, setSelectedMetric] = useState(null); const [isDetailOpen, setIsDetailOpen] = useState(false); - console.log("🧪 CustomMetricTestWidget 렌더링!", element); + // console.log("🧪 CustomMetricTestWidget 렌더링!", element); const dataSources = useMemo(() => { return element?.dataSources || element?.chartConfig?.dataSources; @@ -101,10 +101,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const { dashboardApi } = await import("@/lib/api/dashboard"); const result = await dashboardApi.executeQuery(groupByDS.query); - if (result.success && result.data?.rows) { - const rows = result.data.rows; + if (result && result.rows) { + const rows = result.rows; if (rows.length > 0) { - const columns = result.data.columns || Object.keys(rows[0]); + const columns = result.columns || Object.keys(rows[0]); const labelColumn = columns[0]; const valueColumn = columns[1]; @@ -121,12 +121,17 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg else if (dataSourceType === "api") { if (!groupByDS.endpoint) return; - const { dashboardApi } = await import("@/lib/api/dashboard"); - const result = await dashboardApi.fetchExternalApi({ - method: "GET", - url: groupByDS.endpoint, - headers: (groupByDS as any).headers || {}, + // REST API 호출 (백엔드 프록시 사용) + const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: groupByDS.endpoint, + method: "GET", + headers: (groupByDS as any).headers || {}, + }), }); + const result = await response.json(); if (result.success && result.data) { let rows: any[] = []; @@ -163,11 +168,11 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const dataSources = element?.dataSources || element?.chartConfig?.dataSources; if (!dataSources || dataSources.length === 0) { - console.log("⚠️ 데이터 소스가 없습니다."); + // console.log("⚠️ 데이터 소스가 없습니다."); return; } - console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); + // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); setLoading(true); setError(null); @@ -176,7 +181,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const results = await Promise.allSettled( dataSources.map(async (source, sourceIndex) => { try { - console.log(`📡 데이터 소스 ${sourceIndex + 1} "${source.name || source.id}" 로딩 중...`); + // console.log(`📡 데이터 소스 ${sourceIndex + 1} "${source.name || source.id}" 로딩 중...`); let rows: any[] = []; if (source.type === "api") { @@ -185,7 +190,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg rows = await loadDatabaseData(source); } - console.log(`✅ 데이터 소스 ${sourceIndex + 1}: ${rows.length}개 행`); + // console.log(`✅ 데이터 소스 ${sourceIndex + 1}: ${rows.length}개 행`); return { sourceName: source.name || `데이터 소스 ${sourceIndex + 1}`, @@ -203,7 +208,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg }), ); - console.log(`✅ 총 ${results.length}개의 데이터 소스 로딩 완료`); + // console.log(`✅ 총 ${results.length}개의 데이터 소스 로딩 완료`); // 각 데이터 소스별로 메트릭 생성 const allMetrics: any[] = []; @@ -235,11 +240,11 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg return typeof value === "string" || !numericColumns.includes(col); }); - console.log(`📊 [${sourceName}] 컬럼 분석:`, { - 전체: columns, - 숫자: numericColumns, - 문자열: stringColumns, - }); + // console.log(`📊 [${sourceName}] 컬럼 분석:`, { + // 전체: columns, + // 숫자: numericColumns, + // 문자열: stringColumns, + // }); // 🆕 자동 집계 로직: 집계 컬럼 이름으로 판단 (count, 개수, sum, avg 등) const isAggregated = numericColumns.some((col) => @@ -248,7 +253,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (isAggregated && numericColumns.length > 0) { // 집계 컬럼이 있으면 이미 집계된 데이터로 판단 (GROUP BY 결과) - console.log(`✅ [${sourceName}] 집계된 데이터로 판단 (집계 컬럼 발견: ${numericColumns.join(", ")})`); + // console.log(`✅ [${sourceName}] 집계된 데이터로 판단 (집계 컬럼 발견: ${numericColumns.join(", ")})`); rows.forEach((row, index) => { // 라벨: 첫 번째 문자열 컬럼 @@ -259,7 +264,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const valueField = numericColumns[0]; const value = Number(row[valueField]) || 0; - console.log(` [${sourceName}] 메트릭: ${label} = ${value}`); + // console.log(` [${sourceName}] 메트릭: ${label} = ${value}`); allMetrics.push({ label: label, @@ -273,11 +278,11 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg }); } else { // 숫자 컬럼이 없으면 자동 집계 (마지막 컬럼 기준) - console.log(`✅ [${sourceName}] 자동 집계 모드 (숫자 컬럼 없음)`); + // console.log(`✅ [${sourceName}] 자동 집계 모드 (숫자 컬럼 없음)`); // 마지막 컬럼을 집계 기준으로 사용 const aggregateField = columns[columns.length - 1]; - console.log(` [${sourceName}] 집계 기준 컬럼: ${aggregateField}`); + // console.log(` [${sourceName}] 집계 기준 컬럼: ${aggregateField}`); // 해당 컬럼의 값별로 카운트 const countMap = new Map(); @@ -288,7 +293,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 카운트 결과를 메트릭으로 변환 countMap.forEach((count, label) => { - console.log(` [${sourceName}] 자동 집계: ${label} = ${count}개`); + // console.log(` [${sourceName}] 자동 집계: ${label} = ${count}개`); allMetrics.push({ label: label, @@ -314,20 +319,20 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } // 🆕 숫자 컬럼이 없을 때의 기존 로직은 주석 처리 - if (false) { + /* if (false && result.status === "fulfilled") { // 숫자 컬럼이 없으면 각 컬럼별 고유값 개수 표시 - console.log(`📊 [${sourceName}] 문자열 데이터, 각 컬럼별 고유값 개수 표시`); + // console.log(`📊 [${sourceName}] 문자열 데이터, 각 컬럼별 고유값 개수 표시`); // 데이터 소스에서 선택된 컬럼 가져오기 const dataSourceConfig = (element?.dataSources || element?.chartConfig?.dataSources)?.find( - (ds) => ds.name === sourceName || ds.id === result.value.sourceIndex.toString(), + (ds) => ds.name === sourceName || ds.id === result.value?.sourceIndex.toString(), ); const selectedColumns = dataSourceConfig?.selectedColumns || []; // 선택된 컬럼이 있으면 해당 컬럼만, 없으면 전체 컬럼 표시 const columnsToShow = selectedColumns.length > 0 ? selectedColumns : columns; - console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow); + // console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow); columnsToShow.forEach((col) => { // 해당 컬럼이 실제로 존재하는지 확인 @@ -340,7 +345,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const uniqueValues = new Set(rows.map((row) => row[col])); const uniqueCount = uniqueValues.size; - console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`); + // console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`); allMetrics.push({ label: `${sourceName} - ${col} (고유값)`, @@ -363,24 +368,24 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg sourceName: sourceName, rawData: rows, // 원본 데이터 저장 }); - } + } */ } else { // 행이 많으면 각 컬럼별 고유값 개수 + 총 개수 표시 - console.log(`📊 [${sourceName}] 일반 데이터 (행 많음), 컬럼별 통계 표시`); + // console.log(`📊 [${sourceName}] 일반 데이터 (행 많음), 컬럼별 통계 표시`); const firstRow = rows[0]; const columns = Object.keys(firstRow); // 데이터 소스에서 선택된 컬럼 가져오기 const dataSourceConfig = (element?.dataSources || element?.chartConfig?.dataSources)?.find( - (ds) => ds.name === sourceName || ds.id === result.value.sourceIndex.toString(), + (ds) => ds.name === sourceName || (result.status === "fulfilled" && ds.id === result.value?.sourceIndex.toString()), ); const selectedColumns = dataSourceConfig?.selectedColumns || []; // 선택된 컬럼이 있으면 해당 컬럼만, 없으면 전체 컬럼 표시 const columnsToShow = selectedColumns.length > 0 ? selectedColumns : columns; - console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow); + // console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow); // 각 컬럼별 고유값 개수 columnsToShow.forEach((col) => { @@ -393,7 +398,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg const uniqueValues = new Set(rows.map((row) => row[col])); const uniqueCount = uniqueValues.size; - console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`); + // console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`); allMetrics.push({ label: `${sourceName} - ${col} (고유값)`, @@ -419,7 +424,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } }); - console.log(`✅ 총 ${allMetrics.length}개의 메트릭 생성 완료`); + // console.log(`✅ 총 ${allMetrics.length}개의 메트릭 생성 완료`); setMetrics(allMetrics); setLastRefreshTime(new Date()); } catch (err) { @@ -460,13 +465,13 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // console.log("🔄 수동 새로고침 버튼 클릭"); loadAllData(); }, [loadAllData]); // XML 데이터 파싱 const parseXmlData = (xmlText: string): any[] => { - console.log("🔍 XML 파싱 시작"); + // console.log("🔍 XML 파싱 시작"); try { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); @@ -486,7 +491,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg result.push(obj); } - console.log(`✅ XML 파싱 완료: ${result.length}개 레코드`); + // console.log(`✅ XML 파싱 완료: ${result.length}개 레코드`); return result; } catch (error) { console.error("❌ XML 파싱 실패:", error); @@ -496,16 +501,16 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 텍스트/CSV 데이터 파싱 const parseTextData = (text: string): any[] => { - console.log("🔍 텍스트 파싱 시작 (처음 500자):", text.substring(0, 500)); + // console.log("🔍 텍스트 파싱 시작 (처음 500자):", text.substring(0, 500)); // XML 감지 if (text.trim().startsWith("")) { - console.log("📄 XML 형식 감지"); + // console.log("📄 XML 형식 감지"); return parseXmlData(text); } // CSV 파싱 - console.log("📄 CSV 형식으로 파싱 시도"); + // console.log("📄 CSV 형식으로 파싱 시도"); const lines = text.trim().split("\n"); if (lines.length === 0) return []; @@ -523,7 +528,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg result.push(obj); } - console.log(`✅ CSV 파싱 완료: ${result.length}개 행`); + // console.log(`✅ CSV 파싱 완료: ${result.length}개 행`); return result; }; @@ -552,7 +557,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } } - console.log("🌐 API 호출:", source.endpoint, "파라미터:", Object.fromEntries(params)); + // console.log("🌐 API 호출:", source.endpoint, "파라미터:", Object.fromEntries(params)); const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", @@ -578,7 +583,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } const result = await response.json(); - console.log("✅ API 응답:", result); + // console.log("✅ API 응답:", result); if (!result.success) { console.error("❌ API 실패:", result); @@ -589,10 +594,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg // 텍스트/XML 데이터 처리 if (typeof processedData === "string") { - console.log("📄 텍스트 형식 데이터 감지"); + // console.log("📄 텍스트 형식 데이터 감지"); processedData = parseTextData(processedData); } else if (processedData && typeof processedData === "object" && processedData.text) { - console.log("📄 래핑된 텍스트 데이터 감지"); + // console.log("📄 래핑된 텍스트 데이터 감지"); processedData = parseTextData(processedData.text); } @@ -608,12 +613,12 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg } } else if (!Array.isArray(processedData) && typeof processedData === "object") { // JSON Path 없으면 자동으로 배열 찾기 - console.log("🔍 JSON Path 없음, 자동으로 배열 찾기 시도"); + // console.log("🔍 JSON Path 없음, 자동으로 배열 찾기 시도"); const arrayKeys = ["data", "items", "result", "records", "rows", "list"]; for (const key of arrayKeys) { if (Array.isArray(processedData[key])) { - console.log(`✅ 배열 발견: ${key}`); + // console.log(`✅ 배열 발견: ${key}`); processedData = processedData[key]; break; } @@ -681,15 +686,15 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // console.log("🔄 자동 새로고침 실행"); loadAllData(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadAllData]); diff --git a/frontend/components/dashboard/widgets/ListTestWidget.tsx b/frontend/components/dashboard/widgets/ListTestWidget.tsx index d10ee6be..64248532 100644 --- a/frontend/components/dashboard/widgets/ListTestWidget.tsx +++ b/frontend/components/dashboard/widgets/ListTestWidget.tsx @@ -34,13 +34,13 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const [currentPage, setCurrentPage] = useState(1); const [lastRefreshTime, setLastRefreshTime] = useState(null); - // console.log("🧪 ListTestWidget 렌더링!", element); + // // console.log("🧪 ListTestWidget 렌더링!", element); const dataSources = useMemo(() => { return element?.dataSources || element?.chartConfig?.dataSources; }, [element?.dataSources, element?.chartConfig?.dataSources]); - // console.log("📊 dataSources 확인:", { + // // console.log("📊 dataSources 확인:", { // hasDataSources: !!dataSources, // dataSourcesLength: dataSources?.length || 0, // dataSources: dataSources, @@ -61,11 +61,11 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { // 다중 데이터 소스 로딩 const loadMultipleDataSources = useCallback(async () => { if (!dataSources || dataSources.length === 0) { - console.log("⚠️ 데이터 소스가 없습니다."); + // console.log("⚠️ 데이터 소스가 없습니다."); return; } - console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); + // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); setIsLoading(true); setError(null); @@ -74,7 +74,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const results = await Promise.allSettled( dataSources.map(async (source) => { try { - console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); + // console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); if (source.type === "api") { return await loadRestApiData(source); @@ -127,7 +127,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { }); setLastRefreshTime(new Date()); - console.log(`✅ 총 ${allRows.length}개의 행 로딩 완료`); + // console.log(`✅ 총 ${allRows.length}개의 행 로딩 완료`); } catch (err) { setError(err instanceof Error ? err.message : "데이터 로딩 실패"); } finally { @@ -137,7 +137,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // console.log("🔄 수동 새로고침 버튼 클릭"); loadMultipleDataSources(); }, [loadMultipleDataSources]); @@ -161,6 +161,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { headers: { "Content-Type": "application/json", }, + credentials: "include", body: JSON.stringify({ url: source.endpoint, method: "GET", @@ -180,7 +181,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { } const result = await response.json(); - console.log("✅ API 응답:", result); + // console.log("✅ API 응답:", result); if (!result.success) { console.error("❌ API 실패:", result); @@ -247,7 +248,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const { dashboardApi } = await import("@/lib/api/dashboard"); const result = await dashboardApi.executeQuery(source.query); - // console.log("💾 내부 DB 쿼리 결과:", { + // // console.log("💾 내부 DB 쿼리 결과:", { // hasRows: !!result.rows, // rowCount: result.rows?.length || 0, // hasColumns: !!result.columns, @@ -260,7 +261,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { const mappedRows = applyColumnMapping(result.rows, source.columnMapping); const columns = mappedRows.length > 0 ? Object.keys(mappedRows[0]) : result.columns; - // console.log("✅ 매핑 후:", { + // // console.log("✅ 매핑 후:", { // columns, // rowCount: mappedRows.length, // firstMappedRow: mappedRows[0], @@ -291,15 +292,15 @@ export function ListTestWidget({ element }: ListTestWidgetProps) { if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadMultipleDataSources]); diff --git a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx index cec70b7a..5eeeca12 100644 --- a/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx +++ b/frontend/components/dashboard/widgets/MapTestWidgetV2.tsx @@ -67,8 +67,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const [geoJsonData, setGeoJsonData] = useState(null); const [lastRefreshTime, setLastRefreshTime] = useState(null); - console.log("🧪 MapTestWidgetV2 렌더링!", element); - console.log("📍 마커:", markers.length, "🔷 폴리곤:", polygons.length); + // // console.log("🧪 MapTestWidgetV2 렌더링!", element); + // // console.log("📍 마커:", markers.length, "🔷 폴리곤:", polygons.length); // dataSources를 useMemo로 추출 (circular reference 방지) const dataSources = useMemo(() => { @@ -80,11 +80,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const dataSourcesList = dataSources; if (!dataSources || dataSources.length === 0) { - console.log("⚠️ 데이터 소스가 없습니다."); + // // console.log("⚠️ 데이터 소스가 없습니다."); return; } - console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); + // // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`); setLoading(true); setError(null); @@ -93,7 +93,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const results = await Promise.allSettled( dataSources.map(async (source) => { try { - console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); + // // console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`); if (source.type === "api") { return await loadRestApiData(source); @@ -114,21 +114,21 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const allPolygons: PolygonData[] = []; results.forEach((result, index) => { - console.log(`🔍 결과 ${index}:`, result); + // // console.log(`🔍 결과 ${index}:`, result); if (result.status === "fulfilled" && result.value) { const value = result.value as { markers: MarkerData[]; polygons: PolygonData[] }; - console.log(`✅ 데이터 소스 ${index} 성공:`, value); + // // console.log(`✅ 데이터 소스 ${index} 성공:`, value); // 마커 병합 if (value.markers && Array.isArray(value.markers)) { - console.log(` → 마커 ${value.markers.length}개 추가`); + // // console.log(` → 마커 ${value.markers.length}개 추가`); allMarkers.push(...value.markers); } // 폴리곤 병합 if (value.polygons && Array.isArray(value.polygons)) { - console.log(` → 폴리곤 ${value.polygons.length}개 추가`); + // // console.log(` → 폴리곤 ${value.polygons.length}개 추가`); allPolygons.push(...value.polygons); } } else if (result.status === "rejected") { @@ -136,9 +136,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { } }); - console.log(`✅ 총 ${allMarkers.length}개의 마커, ${allPolygons.length}개의 폴리곤 로딩 완료`); - console.log("📍 최종 마커 데이터:", allMarkers); - console.log("🔷 최종 폴리곤 데이터:", allPolygons); + // // console.log(`✅ 총 ${allMarkers.length}개의 마커, ${allPolygons.length}개의 폴리곤 로딩 완료`); + // // console.log("📍 최종 마커 데이터:", allMarkers); + // // console.log("🔷 최종 폴리곤 데이터:", allPolygons); setMarkers(allMarkers); setPolygons(allPolygons); @@ -153,13 +153,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // // console.log("🔄 수동 새로고침 버튼 클릭"); loadMultipleDataSources(); }, [loadMultipleDataSources]); // REST API 데이터 로딩 const loadRestApiData = async (source: ChartDataSource): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => { - console.log(`🌐 REST API 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType); + // // console.log(`🌐 REST API 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType); if (!source.endpoint) { throw new Error("API endpoint가 없습니다."); @@ -215,10 +215,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 텍스트 형식 데이터 체크 (기상청 API 등) if (data && typeof data === 'object' && data.text && typeof data.text === 'string') { - console.log("📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); + // // console.log("📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); const parsedData = parseTextData(data.text); if (parsedData.length > 0) { - console.log(`✅ CSV 파싱 성공: ${parsedData.length}개 행`); + // // console.log(`✅ CSV 파싱 성공: ${parsedData.length}개 행`); // 컬럼 매핑 적용 const mappedData = applyColumnMapping(parsedData, source.columnMapping); return convertToMapData(mappedData, source.name || source.id || "API", source.mapDisplayType, source); @@ -244,7 +244,7 @@ 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); + // // console.log(`💾 Database 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType); if (!source.query) { throw new Error("SQL 쿼리가 없습니다."); @@ -287,7 +287,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // XML 데이터 파싱 (UTIC API 등) const parseXmlData = (xmlText: string): any[] => { try { - console.log(" 📄 XML 파싱 시작"); + // // console.log(" 📄 XML 파싱 시작"); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlText, "text/xml"); @@ -306,7 +306,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { results.push(obj); } - console.log(` ✅ XML 파싱 완료: ${results.length}개 레코드`); + // // console.log(` ✅ XML 파싱 완료: ${results.length}개 레코드`); return results; } catch (error) { console.error(" ❌ XML 파싱 실패:", error); @@ -317,11 +317,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 텍스트 데이터 파싱 (CSV, 기상청 형식 등) const parseTextData = (text: string): any[] => { try { - console.log(" 🔍 원본 텍스트 (처음 500자):", text.substring(0, 500)); + // // console.log(" 🔍 원본 텍스트 (처음 500자):", text.substring(0, 500)); // XML 형식 감지 if (text.trim().startsWith("")) { - console.log(" 📄 XML 형식 데이터 감지"); + // // console.log(" 📄 XML 형식 데이터 감지"); return parseXmlData(text); } @@ -333,7 +333,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { !trimmed.startsWith('---'); }); - console.log(` 📝 유효한 라인: ${lines.length}개`); + // // console.log(` 📝 유효한 라인: ${lines.length}개`); if (lines.length === 0) return []; @@ -344,7 +344,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const line = lines[i]; const values = line.split(',').map(v => v.trim().replace(/,=$/g, '')); - console.log(` 라인 ${i}:`, values); + // // console.log(` 라인 ${i}:`, values); // 기상특보 형식: 지역코드, 지역명, 하위코드, 하위지역명, 발표시각, 특보종류, 등급, 발표상태, 설명 if (values.length >= 4) { @@ -364,11 +364,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { obj.name = obj.subRegion || obj.region || obj.code; result.push(obj); - console.log(` ✅ 파싱 성공:`, obj); + // console.log(` ✅ 파싱 성공:`, obj); } } - console.log(" 📊 최종 파싱 결과:", result.length, "개"); + // // console.log(" 📊 최종 파싱 결과:", result.length, "개"); return result; } catch (error) { console.error(" ❌ 텍스트 파싱 오류:", error); @@ -383,9 +383,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { mapDisplayType?: "auto" | "marker" | "polygon", dataSource?: ChartDataSource ): { markers: MarkerData[]; polygons: PolygonData[] } => { - console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행"); - console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`); - console.log(` 🎨 마커 색상:`, dataSource?.markerColor, `폴리곤 색상:`, dataSource?.polygonColor); + // // console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행"); + // // console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`); + // // console.log(` 🎨 마커 색상:`, dataSource?.markerColor, `폴리곤 색상:`, dataSource?.polygonColor); if (rows.length === 0) return { markers: [], polygons: [] }; @@ -393,13 +393,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { const polygons: PolygonData[] = []; rows.forEach((row, index) => { - console.log(` 행 ${index}:`, row); + // // console.log(` 행 ${index}:`, row); // 텍스트 데이터 체크 (기상청 API 등) if (row && typeof row === 'object' && row.text && typeof row.text === 'string') { - console.log(" 📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); + // // console.log(" 📄 텍스트 형식 데이터 감지, CSV 파싱 시도"); const parsedData = parseTextData(row.text); - console.log(` ✅ CSV 파싱 결과: ${parsedData.length}개 행`); + // // console.log(` ✅ CSV 파싱 결과: ${parsedData.length}개 행`); // 파싱된 데이터를 재귀적으로 변환 (색상 정보 전달) const result = convertToMapData(parsedData, sourceName, mapDisplayType, dataSource); @@ -410,11 +410,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 폴리곤 데이터 체크 (coordinates 필드가 배열인 경우 또는 강제 polygon 모드) if (row.coordinates && Array.isArray(row.coordinates) && row.coordinates.length > 0) { - console.log(` → coordinates 발견:`, row.coordinates.length, "개"); + // // console.log(` → coordinates 발견:`, row.coordinates.length, "개"); // coordinates가 [lat, lng] 배열의 배열인지 확인 const firstCoord = row.coordinates[0]; if (Array.isArray(firstCoord) && firstCoord.length === 2) { - console.log(` → 폴리곤으로 처리:`, row.name); + // console.log(` → 폴리곤으로 처리:`, row.name); polygons.push({ id: row.id || row.code || `polygon-${index}`, name: row.name || row.title || `영역 ${index + 1}`, @@ -431,7 +431,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 지역명으로 해상 구역 확인 (auto 또는 polygon 모드일 때만) const regionName = row.name || row.area || row.region || row.location || row.subRegion; if (regionName && MARITIME_ZONES[regionName] && mapDisplayType !== "marker") { - console.log(` → 해상 구역 발견: ${regionName}, 폴리곤으로 처리`); + // // console.log(` → 해상 구역 발견: ${regionName}, 폴리곤으로 처리`); polygons.push({ id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성 name: regionName, @@ -451,24 +451,24 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 위도/경도가 없으면 지역 코드/지역명으로 변환 시도 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}, 위도/경도 변환 시도`); + // // console.log(` → 지역 코드 발견: ${regionCode}, 위도/경도 변환 시도`); const coords = getCoordinatesByRegionCode(regionCode); if (coords) { lat = coords.lat; lng = coords.lng; - console.log(` → 변환 성공: (${lat}, ${lng})`); + // console.log(` → 변환 성공: (${lat}, ${lng})`); } } // 지역명으로도 시도 if ((lat === undefined || lng === undefined) && (row.name || row.area || row.region || row.location)) { const regionName = row.name || row.area || row.region || row.location; - console.log(` → 지역명 발견: ${regionName}, 위도/경도 변환 시도`); + // // console.log(` → 지역명 발견: ${regionName}, 위도/경도 변환 시도`); const coords = getCoordinatesByRegionName(regionName); if (coords) { lat = coords.lat; lng = coords.lng; - console.log(` → 변환 성공: (${lat}, ${lng})`); + // console.log(` → 변환 성공: (${lat}, ${lng})`); } } @@ -476,7 +476,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { if (mapDisplayType === "polygon") { const regionName = row.name || row.subRegion || row.region || row.area; if (regionName) { - console.log(` 🔷 강제 폴리곤 모드: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`); + // console.log(` 🔷 강제 폴리곤 모드: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`); polygons.push({ id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`, name: regionName, @@ -487,14 +487,14 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { color: dataSource?.polygonColor || getColorByStatus(row.status || row.level), }); } else { - console.log(` ⚠️ 강제 폴리곤 모드지만 지역명 없음 - 스킵`); + // console.log(` ⚠️ 강제 폴리곤 모드지만 지역명 없음 - 스킵`); } return; // 폴리곤으로 처리했으므로 마커로는 추가하지 않음 } // 위도/경도가 있고 marker 모드가 아니면 마커로 처리 if (lat !== undefined && lng !== undefined && mapDisplayType !== "polygon") { - console.log(` → 마커로 처리: (${lat}, ${lng})`); + // // console.log(` → 마커로 처리: (${lat}, ${lng})`); markers.push({ id: `${sourceName}-marker-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성 lat: Number(lat), @@ -511,7 +511,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 위도/경도가 없는 육지 지역 → 폴리곤으로 추가 (GeoJSON 매칭용) const regionName = row.name || row.subRegion || row.region || row.area; if (regionName) { - console.log(` 📍 위도/경도 없지만 지역명 있음: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`); + // console.log(` 📍 위도/경도 없지만 지역명 있음: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`); polygons.push({ id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`, name: regionName, @@ -522,13 +522,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { color: dataSource?.polygonColor || getColorByStatus(row.status || row.level), }); } else { - console.log(` ⚠️ 위도/경도 없고 지역명도 없음 - 스킵`); - console.log(` 데이터:`, row); + // console.log(` ⚠️ 위도/경도 없고 지역명도 없음 - 스킵`); + // console.log(` 데이터:`, row); } } }); - console.log(`✅ ${sourceName}: 마커 ${markers.length}개, 폴리곤 ${polygons.length}개 변환 완료`); + // // console.log(`✅ ${sourceName}: 마커 ${markers.length}개, 폴리곤 ${polygons.length}개 변환 완료`); return { markers, polygons }; }; @@ -757,7 +757,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { try { const response = await fetch("/geojson/korea-municipalities.json"); const data = await response.json(); - console.log("🗺️ GeoJSON 로드 완료:", data.features?.length, "개 시/군/구"); + // // console.log("🗺️ GeoJSON 로드 완료:", data.features?.length, "개 시/군/구"); setGeoJsonData(data); } catch (err) { console.error("❌ GeoJSON 로드 실패:", err); @@ -769,11 +769,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 초기 로드 useEffect(() => { const dataSources = element?.dataSources || element?.chartConfig?.dataSources; - console.log("🔄 useEffect 트리거! dataSources:", dataSources); + // // console.log("🔄 useEffect 트리거! dataSources:", dataSources); if (dataSources && dataSources.length > 0) { loadMultipleDataSources(); } else { - console.log("⚠️ dataSources가 없거나 비어있음"); + // // console.log("⚠️ dataSources가 없거나 비어있음"); setMarkers([]); setPolygons([]); } @@ -791,15 +791,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // // console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadMultipleDataSources]); @@ -876,11 +876,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { {/* 폴리곤 렌더링 */} {/* GeoJSON 렌더링 (육지 지역 경계선) */} {(() => { - console.log(`🗺️ GeoJSON 렌더링 조건 체크:`, { - geoJsonData: !!geoJsonData, - polygonsLength: polygons.length, - polygonNames: polygons.map(p => p.name), - }); + // console.log(`🗺️ GeoJSON 렌더링 조건 체크:`, { + // geoJsonData: !!geoJsonData, + // polygonsLength: polygons.length, + // polygonNames: polygons.map(p => p.name), + // }); return null; })()} {geoJsonData && polygons.length > 0 ? ( @@ -897,31 +897,31 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { // 정확한 매칭 if (p.name === sigName) { - console.log(`✅ 정확 매칭: ${p.name} === ${sigName}`); + // console.log(`✅ 정확 매칭: ${p.name} === ${sigName}`); return true; } if (p.name === ctpName) { - console.log(`✅ 정확 매칭: ${p.name} === ${ctpName}`); + // console.log(`✅ 정확 매칭: ${p.name} === ${ctpName}`); return true; } // 부분 매칭 (GeoJSON 지역명에 폴리곤 이름이 포함되는지) if (sigName && sigName.includes(p.name)) { - console.log(`✅ 부분 매칭: ${sigName} includes ${p.name}`); + // console.log(`✅ 부분 매칭: ${sigName} includes ${p.name}`); return true; } if (ctpName && ctpName.includes(p.name)) { - console.log(`✅ 부분 매칭: ${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}`); + // console.log(`✅ 역방향 매칭: ${p.name} includes ${sigName}`); return true; } if (ctpName && p.name.includes(ctpName)) { - console.log(`✅ 역방향 매칭: ${p.name} includes ${ctpName}`); + // console.log(`✅ 역방향 매칭: ${p.name} includes ${ctpName}`); return true; } @@ -969,7 +969,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) { }} /> ) : ( - <>{console.log(`⚠️ GeoJSON 렌더링 안 됨: geoJsonData=${!!geoJsonData}, polygons=${polygons.length}`)} + <> + {/* console.log(`⚠️ GeoJSON 렌더링 안 됨: geoJsonData=${!!geoJsonData}, polygons=${polygons.length}`) */} + )} {/* 폴리곤 렌더링 (해상 구역만) */} diff --git a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx index 6ac1765a..fd8b60fd 100644 --- a/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx +++ b/frontend/components/dashboard/widgets/RiskAlertTestWidget.tsx @@ -39,12 +39,12 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp const parseTextData = (text: string): any[] => { // XML 형식 감지 if (text.trim().startsWith("")) { - console.log("📄 XML 형식 데이터 감지"); + // console.log("📄 XML 형식 데이터 감지"); return parseXmlData(text); } // CSV 형식 (기상청 특보) - console.log("📄 CSV 형식 데이터 감지"); + // console.log("📄 CSV 형식 데이터 감지"); const lines = text.split("\n").filter((line) => { const trimmed = line.trim(); return trimmed && !trimmed.startsWith("#") && trimmed !== "="; @@ -98,7 +98,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp results.push(obj); } - console.log(`✅ XML 파싱 완료: ${results.length}개 레코드`); + // console.log(`✅ XML 파싱 완료: ${results.length}개 레코드`); return results; } catch (error) { console.error("❌ XML 파싱 실패:", error); @@ -107,14 +107,78 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp }; const loadRestApiData = useCallback(async (source: ChartDataSource) => { - if (!source.endpoint) { + // 🆕 외부 연결 ID가 있으면 먼저 외부 연결 정보를 가져옴 + let actualEndpoint = source.endpoint; + let actualQueryParams = source.queryParams; + let actualHeaders = source.headers; + + if (source.externalConnectionId) { + // console.log("🔗 외부 연결 ID 감지:", source.externalConnectionId); + try { + const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection"); + const connection = await ExternalDbConnectionAPI.getApiConnectionById(Number(source.externalConnectionId)); + + if (connection) { + // console.log("✅ 외부 연결 정보 가져옴:", connection); + + // 전체 엔드포인트 URL 생성 + actualEndpoint = connection.endpoint_path + ? `${connection.base_url}${connection.endpoint_path}` + : connection.base_url; + + // console.log("📍 실제 엔드포인트:", actualEndpoint); + + // 기본 헤더 적용 + const headers: any[] = []; + if (connection.default_headers && Object.keys(connection.default_headers).length > 0) { + Object.entries(connection.default_headers).forEach(([key, value]) => { + headers.push({ key, value }); + }); + } + + // 인증 정보 적용 + const queryParams: any[] = []; + if (connection.auth_type && connection.auth_type !== "none" && connection.auth_config) { + const authConfig = connection.auth_config; + + if (connection.auth_type === "api-key") { + if (authConfig.keyLocation === "header" && authConfig.keyName && authConfig.keyValue) { + headers.push({ key: authConfig.keyName, value: authConfig.keyValue }); + // console.log("🔑 API Key 헤더 추가:", authConfig.keyName); + } else if (authConfig.keyLocation === "query" && authConfig.keyName && authConfig.keyValue) { + const actualKeyName = authConfig.keyName === "apiKey" ? "key" : authConfig.keyName; + queryParams.push({ key: actualKeyName, value: authConfig.keyValue }); + // console.log("🔑 API Key 쿼리 파라미터 추가:", actualKeyName); + } + } else if (connection.auth_type === "bearer" && authConfig.token) { + headers.push({ key: "Authorization", value: `Bearer ${authConfig.token}` }); + // console.log("🔑 Bearer Token 헤더 추가"); + } else if (connection.auth_type === "basic" && authConfig.username && authConfig.password) { + const credentials = btoa(`${authConfig.username}:${authConfig.password}`); + headers.push({ key: "Authorization", value: `Basic ${credentials}` }); + // console.log("🔑 Basic Auth 헤더 추가"); + } + } + + actualHeaders = headers; + actualQueryParams = queryParams; + + // console.log("✅ 최종 헤더:", actualHeaders); + // console.log("✅ 최종 쿼리 파라미터:", actualQueryParams); + } + } catch (err) { + console.error("❌ 외부 연결 정보 가져오기 실패:", err); + } + } + + if (!actualEndpoint) { throw new Error("API endpoint가 없습니다."); } // 쿼리 파라미터 처리 const queryParamsObj: Record = {}; - if (source.queryParams && Array.isArray(source.queryParams)) { - source.queryParams.forEach((param) => { + if (actualQueryParams && Array.isArray(actualQueryParams)) { + actualQueryParams.forEach((param) => { if (param.key && param.value) { queryParamsObj[param.key] = param.value; } @@ -123,34 +187,33 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp // 헤더 처리 const headersObj: Record = {}; - if (source.headers && Array.isArray(source.headers)) { - source.headers.forEach((header) => { + if (actualHeaders && Array.isArray(actualHeaders)) { + actualHeaders.forEach((header) => { if (header.key && header.value) { headersObj[header.key] = header.value; } }); } - console.log("🌐 API 호출 준비:", { - endpoint: source.endpoint, - queryParams: queryParamsObj, - headers: headersObj, - }); - console.log("🔍 원본 source.queryParams:", source.queryParams); - console.log("🔍 원본 source.headers:", source.headers); + // console.log("🌐 API 호출 준비:", { + // endpoint: actualEndpoint, + // queryParams: queryParamsObj, + // headers: headersObj, + // }); const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), { method: "POST", headers: { "Content-Type": "application/json" }, + credentials: "include", body: JSON.stringify({ - url: source.endpoint, + url: actualEndpoint, method: "GET", headers: headersObj, queryParams: queryParamsObj, }), }); - console.log("🌐 API 응답 상태:", response.status); + // console.log("🌐 API 응답 상태:", response.status); if (!response.ok) { throw new Error(`HTTP ${response.status}`); @@ -163,22 +226,22 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp let apiData = result.data; - console.log("🔍 API 응답 데이터 타입:", typeof apiData); - console.log("🔍 API 응답 데이터 (처음 500자):", typeof apiData === "string" ? apiData.substring(0, 500) : JSON.stringify(apiData).substring(0, 500)); + // console.log("🔍 API 응답 데이터 타입:", typeof apiData); + // console.log("🔍 API 응답 데이터 (처음 500자):", typeof apiData === "string" ? apiData.substring(0, 500) : JSON.stringify(apiData).substring(0, 500)); // 백엔드가 {text: "XML..."} 형태로 감싼 경우 처리 if (apiData && typeof apiData === "object" && apiData.text && typeof apiData.text === "string") { - console.log("📦 백엔드가 text 필드로 감싼 데이터 감지"); + // console.log("📦 백엔드가 text 필드로 감싼 데이터 감지"); apiData = parseTextData(apiData.text); - console.log("✅ 파싱 성공:", apiData.length, "개 행"); + // console.log("✅ 파싱 성공:", apiData.length, "개 행"); } else if (typeof apiData === "string") { - console.log("📄 텍스트 형식 데이터 감지, 파싱 시도"); + // console.log("📄 텍스트 형식 데이터 감지, 파싱 시도"); apiData = parseTextData(apiData); - console.log("✅ 파싱 성공:", apiData.length, "개 행"); + // console.log("✅ 파싱 성공:", apiData.length, "개 행"); } else if (Array.isArray(apiData)) { - console.log("✅ 이미 배열 형태의 데이터입니다."); + // console.log("✅ 이미 배열 형태의 데이터입니다."); } else { - console.log("⚠️ 예상치 못한 데이터 형식입니다. 배열로 변환 시도."); + // console.log("⚠️ 예상치 못한 데이터 형식입니다. 배열로 변환 시도."); apiData = [apiData]; } @@ -220,7 +283,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp }, []); const convertToAlerts = useCallback((rows: any[], sourceName: string): Alert[] => { - console.log("🔄 convertToAlerts 호출:", rows.length, "개 행"); + // console.log("🔄 convertToAlerts 호출:", rows.length, "개 행"); return rows.map((row: any, index: number) => { // 타입 결정 (UTIC XML 기준) @@ -325,7 +388,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp source: sourceName, }; - console.log(` ✅ Alert ${index}:`, alert); + // console.log(` ✅ Alert ${index}:`, alert); return alert; }); }, []); @@ -338,19 +401,19 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp setLoading(true); setError(null); - console.log("🔄 RiskAlertTestWidget 데이터 로딩 시작:", dataSources.length, "개 소스"); + // console.log("🔄 RiskAlertTestWidget 데이터 로딩 시작:", dataSources.length, "개 소스"); try { const results = await Promise.allSettled( dataSources.map(async (source, index) => { - console.log(`📡 데이터 소스 ${index + 1} 로딩 중:`, source.name, source.type); + // console.log(`📡 데이터 소스 ${index + 1} 로딩 중:`, source.name, source.type); if (source.type === "api") { const alerts = await loadRestApiData(source); - console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림"); + // console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림"); return alerts; } else { const alerts = await loadDatabaseData(source); - console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림"); + // console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림"); return alerts; } }) @@ -359,14 +422,14 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp const allAlerts: Alert[] = []; results.forEach((result, index) => { if (result.status === "fulfilled") { - console.log(`✅ 결과 ${index + 1} 병합:`, result.value.length, "개 알림"); + // console.log(`✅ 결과 ${index + 1} 병합:`, result.value.length, "개 알림"); allAlerts.push(...result.value); } else { console.error(`❌ 결과 ${index + 1} 실패:`, result.reason); } }); - console.log("✅ 총", allAlerts.length, "개 알림 로딩 완료"); + // console.log("✅ 총", allAlerts.length, "개 알림 로딩 완료"); allAlerts.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); setAlerts(allAlerts); setLastRefreshTime(new Date()); @@ -380,7 +443,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp // 수동 새로고침 핸들러 const handleManualRefresh = useCallback(() => { - console.log("🔄 수동 새로고침 버튼 클릭"); + // console.log("🔄 수동 새로고침 버튼 클릭"); loadMultipleDataSources(); }, [loadMultipleDataSources]); @@ -403,15 +466,15 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp if (intervals.length === 0) return; const minInterval = Math.min(...intervals); - console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); + // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`); const intervalId = setInterval(() => { - console.log("🔄 자동 새로고침 실행"); + // console.log("🔄 자동 새로고침 실행"); loadMultipleDataSources(); }, minInterval * 1000); return () => { - console.log("⏹️ 자동 새로고침 정리"); + // console.log("⏹️ 자동 새로고침 정리"); clearInterval(intervalId); }; }, [dataSources, loadMultipleDataSources]); @@ -516,7 +579,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
{/* 컨텐츠 */} -
+
{/* 색상 선택 패널 */} {showColorPicker && ( -
+
onCanvasBackgroundColorChange(e.target.value)} - className="h-10 w-16 border border-gray-300 rounded cursor-pointer" + className="h-10 w-16 border border-border rounded cursor-pointer" /> onCanvasBackgroundColorChange(e.target.value)} placeholder="#ffffff" - className="flex-1 px-2 py-1 text-sm border border-gray-300 rounded" + className="flex-1 px-2 py-1 text-sm border border-border rounded" />
@@ -89,7 +89,7 @@ export function DashboardToolbar({ onClearCanvas, onSaveLayout, canvasBackground diff --git a/frontend/components/admin/dashboard/DashboardTopMenu.tsx b/frontend/components/admin/dashboard/DashboardTopMenu.tsx index 71a9615f..14a2283a 100644 --- a/frontend/components/admin/dashboard/DashboardTopMenu.tsx +++ b/frontend/components/admin/dashboard/DashboardTopMenu.tsx @@ -265,13 +265,13 @@ export function DashboardTopMenu({ }; return ( -
+
{/* 좌측: 대시보드 제목 */}
{dashboardTitle && (
- {dashboardTitle} - 편집 중 + {dashboardTitle} + 편집 중
)}
@@ -287,7 +287,7 @@ export function DashboardTopMenu({ /> )} -
+
{/* 배경색 선택 */} {onBackgroundColorChange && ( @@ -295,7 +295,7 @@ export function DashboardTopMenu({ @@ -349,7 +349,7 @@ export function DashboardTopMenu({ )} -
+
{/* 차트 선택 */} @@ -104,12 +104,12 @@ export function DateFilterPanel({ config, dateColumns, onChange }: DateFilterPan ))} -

감지된 날짜 컬럼: {dateColumns.join(", ")}

+

감지된 날짜 컬럼: {dateColumns.join(", ")}

{/* 빠른 선택 */}
- +
@@ -298,9 +298,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview {/* 진행 상황 표시 - 간단한 위젯과 헤더 전용 위젯은 표시 안 함 */} {!isSimpleWidget && !isHeaderOnlyWidget && ( -
+
-
+
단계 {currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정 및 차트 설정"}
@@ -356,9 +356,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview onConfigChange={handleChartConfigChange} /> ) : ( -
+
-
데이터를 가져온 후 지도 설정이 표시됩니다
+
데이터를 가져온 후 지도 설정이 표시됩니다
) @@ -373,9 +373,9 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview query={dataSource.query} /> ) : ( -
+
-
데이터를 가져온 후 차트 설정이 표시됩니다
+
데이터를 가져온 후 차트 설정이 표시됩니다
)} @@ -387,7 +387,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview )} {/* 모달 푸터 */} -
+
{queryResult && {queryResult.rows.length}개 데이터 로드됨}
diff --git a/frontend/components/admin/dashboard/ElementConfigSidebar.tsx b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx index b8d838f2..32a45a49 100644 --- a/frontend/components/admin/dashboard/ElementConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx @@ -291,31 +291,31 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem return (
{/* 헤더 */} -
+
- {element.title} + {element.title}
{/* 본문: 스크롤 가능 영역 */}
{/* 기본 설정 카드 */} -
-
기본 설정
+
+
기본 설정
{/* 커스텀 제목 입력 */}
@@ -325,20 +325,20 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem onChange={(e) => setCustomTitle(e.target.value)} onKeyDown={(e) => e.stopPropagation()} placeholder="위젯 제목" - className="focus:border-primary focus:ring-primary/20 h-8 w-full rounded border border-gray-200 bg-gray-50 px-2 text-xs placeholder:text-gray-400 focus:bg-white focus:ring-1 focus:outline-none" + className="focus:border-primary focus:ring-primary/20 h-8 w-full rounded border border-border bg-muted px-2 text-xs placeholder:text-muted-foreground focus:bg-background focus:ring-1 focus:outline-none" />
{/* 헤더 표시 옵션 */} -
@@ -346,7 +346,7 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem {/* 다중 데이터 소스 위젯 */} {isMultiDataSourceWidget && ( <> -
+
+
- +
-
+
타일맵 설정 (선택사항)
기본 VWorld 타일맵 사용 중
@@ -403,11 +403,11 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem {/* 차트 위젯: 차트 설정 */} {element.subtype === "chart" && ( -
+
- +
-
차트 설정
+
차트 설정
{testResults.size > 0 ? `${testResults.size}개 데이터 소스 • X축, Y축, 차트 타입 설정` @@ -439,24 +439,24 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem {/* 헤더 전용 위젯이 아닐 때만 데이터 소스 표시 */} {!isHeaderOnlyWidget && !isMultiDataSourceWidget && ( -
-
데이터 소스
+
+
데이터 소스
handleDataSourceTypeChange(value as "database" | "api")} className="w-full" > - + 데이터베이스 REST API @@ -552,9 +552,9 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem {/* 데이터 로드 상태 */} {queryResult && ( -
-
- +
+
+ {queryResult.rows.length}개 데이터 로드됨
@@ -564,10 +564,10 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
{/* 푸터: 적용 버튼 */} -
+
diff --git a/frontend/components/admin/dashboard/MapTestConfigPanel.tsx b/frontend/components/admin/dashboard/MapTestConfigPanel.tsx index b5be7a35..38d60e5b 100644 --- a/frontend/components/admin/dashboard/MapTestConfigPanel.tsx +++ b/frontend/components/admin/dashboard/MapTestConfigPanel.tsx @@ -123,9 +123,9 @@ export function MapTestConfigPanel({ config, queryResult, onConfigChange }: MapT
{/* 타일맵 URL 설정 (외부 커넥션 또는 직접 입력) */}
-