diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 09ddfe5c..ce08c522 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -853,7 +853,9 @@ export function CanvasElement({ )} {/* 제목 */} {!element.type || element.type !== "chart" ? ( - {element.customTitle || element.title} + element.subtype === "map-summary-v2" && !element.customTitle ? null : ( + {element.customTitle || element.title} + ) ) : null}
diff --git a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx index 710abbe6..7ca9684b 100644 --- a/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/WidgetConfigSidebar.tsx @@ -152,7 +152,8 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge setCustomTitle(element.customTitle || ""); setShowHeader(element.showHeader !== false); setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }); - setDataSources(element.dataSources || []); + // dataSources는 element.dataSources 또는 chartConfig.dataSources에서 가져옴 + setDataSources(element.dataSources || element.chartConfig?.dataSources || []); setQueryResult(null); // 리스트 위젯 설정 초기화 @@ -297,10 +298,12 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge ...(needsDataSource(element.subtype) ? { dataSource, - // 다중 데이터 소스 위젯은 dataSources도 포함 + // 다중 데이터 소스 위젯은 dataSources도 포함 (빈 배열도 허용 - 연결 해제) ...(isMultiDataSourceWidget ? { - dataSources: dataSources.length > 0 ? dataSources : element.dataSources || [], + dataSources: dataSources, + // chartConfig에도 dataSources 포함 (일부 위젯은 chartConfig에서 읽음) + chartConfig: { ...chartConfig, dataSources: dataSources }, } : {}), } @@ -316,14 +319,14 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge element.subtype === "chart" || ["bar", "horizontal-bar", "pie", "line", "area", "stacked-bar", "donut", "combo"].includes(element.subtype) ? { - // 다중 데이터 소스 위젯은 chartConfig에 dataSources 포함 + // 다중 데이터 소스 위젯은 chartConfig에 dataSources 포함 (빈 배열도 허용 - 연결 해제) chartConfig: isMultiDataSourceWidget - ? { ...chartConfig, dataSources: dataSources.length > 0 ? dataSources : element.dataSources || [] } + ? { ...chartConfig, dataSources: dataSources } : chartConfig, - // 프론트엔드 호환성을 위해 dataSources도 element에 직접 포함 + // 프론트엔드 호환성을 위해 dataSources도 element에 직접 포함 (빈 배열도 허용 - 연결 해제) ...(isMultiDataSourceWidget ? { - dataSources: dataSources.length > 0 ? dataSources : element.dataSources || [], + dataSources: dataSources, } : {}), } @@ -520,7 +523,39 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge )} {/* 지도 설정 */} - {element.subtype === "map-summary-v2" && } + {element.subtype === "map-summary-v2" && ( + { + setElement((prev) => + prev + ? { + ...prev, + chartConfig: { + ...prev.chartConfig, + refreshInterval: interval, + }, + } + : prev + ); + }} + onMarkerTypeChange={(type) => { + setElement((prev) => + prev + ? { + ...prev, + chartConfig: { + ...prev.chartConfig, + markerType: type, + }, + } + : prev + ); + }} + /> + )} {/* 리스크 알림 설정 */} {element.subtype === "risk-alert-v2" && } @@ -534,7 +569,22 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge -
diff --git a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx index bbbf7d4d..c72cb18e 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiApiConfig.tsx @@ -530,31 +530,50 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M ))} - {/* 자동 새로고침 설정 */} + {/* 마커 polling 간격 설정 (MapTestWidgetV2 전용) */}
-
+ + {/* 마커 종류 선택 (MapTestWidgetV2 전용) */} +
+ + +

+ 지도에 표시할 마커의 모양을 선택합니다

@@ -892,6 +911,128 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M

)} + + {/* 지도 팝업 필드 설정 (MapTestWidgetV2 전용) */} + {availableColumns.length > 0 && ( +
+ + + {/* 기존 팝업 필드 목록 */} + {dataSource.popupFields && dataSource.popupFields.length > 0 && ( +
+ {dataSource.popupFields.map((field, index) => ( +
+
+ 필드 {index + 1} + +
+ + {/* 필드명 선택 */} +
+ + +
+ + {/* 라벨 입력 */} +
+ + { + const newFields = [...(dataSource.popupFields || [])]; + newFields[index].label = e.target.value; + onChange({ popupFields: newFields }); + }} + placeholder="예: 차량 번호" + className="h-8 w-full text-xs" + dir="ltr" + /> +
+ + {/* 포맷 선택 */} +
+ + +
+
+ ))} +
+ )} + + {/* 필드 추가 버튼 */} + + +

+ 마커 클릭 시 팝업에 표시할 필드를 선택하고 한글 라벨을 지정하세요 +

+
+ )} ); } diff --git a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx index 76986718..73b2ab4b 100644 --- a/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/MultiDatabaseConfig.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { ChartDataSource } from "@/components/admin/dashboard/types"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -8,7 +8,7 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Loader2, CheckCircle, XCircle } from "lucide-react"; +import { Loader2, CheckCircle, XCircle, Plus, Trash2 } from "lucide-react"; interface MultiDatabaseConfigProps { dataSource: ChartDataSource; @@ -45,13 +45,15 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult // ExternalDbConnectionAPI 사용 (인증 토큰 자동 포함) const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection"); const connections = await ExternalDbConnectionAPI.getConnections({ is_active: "Y" }); - + console.log("✅ 외부 DB 커넥션 로드 성공:", connections.length, "개"); - setExternalConnections(connections.map((conn: any) => ({ - id: String(conn.id), - name: conn.connection_name, - type: conn.db_type, - }))); + setExternalConnections( + connections.map((conn: any) => ({ + id: String(conn.id), + name: conn.connection_name, + type: conn.db_type, + })), + ); } catch (error) { console.error("❌ 외부 DB 커넥션 로드 실패:", error); setExternalConnections([]); @@ -73,27 +75,27 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult try { // 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 + dataSource.query, ); - + if (result.success && result.data) { const rows = Array.isArray(result.data.rows) ? result.data.rows : []; const rowCount = rows.length; - + // 컬럼 목록 및 타입 추출 if (rows.length > 0) { const columns = Object.keys(rows[0]); setAvailableColumns(columns); - + // 컬럼 타입 분석 const types: Record = {}; - columns.forEach(col => { + columns.forEach((col) => { const value = rows[0][col]; if (value === null || value === undefined) { types[col] = "unknown"; @@ -113,17 +115,17 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult }); setColumnTypes(types); setSampleData(rows.slice(0, 3)); - + console.log("📊 발견된 컬럼:", columns); console.log("📊 컬럼 타입:", types); } - + setTestResult({ success: true, message: "쿼리 실행 성공", rowCount, }); - + // 부모로 테스트 결과 전달 (차트 설정용) if (onTestResult && rows && rows.length > 0) { onTestResult(rows); @@ -134,15 +136,15 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult } else { // 현재 DB const result = await dashboardApi.executeQuery(dataSource.query); - + // 컬럼 목록 및 타입 추출 if (result.rows && result.rows.length > 0) { const columns = Object.keys(result.rows[0]); setAvailableColumns(columns); - + // 컬럼 타입 분석 const types: Record = {}; - columns.forEach(col => { + columns.forEach((col) => { const value = result.rows[0][col]; if (value === null || value === undefined) { types[col] = "unknown"; @@ -162,17 +164,17 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult }); setColumnTypes(types); setSampleData(result.rows.slice(0, 3)); - + console.log("📊 발견된 컬럼:", columns); console.log("📊 컬럼 타입:", types); } - + setTestResult({ success: true, message: "쿼리 실행 성공", rowCount: result.rowCount || 0, }); - + // 부모로 테스트 결과 전달 (차트 설정용) if (onTestResult && result.rows && result.rows.length > 0) { onTestResult(result.rows); @@ -194,25 +196,17 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult - onChange({ connectionType: value }) - } + onValueChange={(value: "current" | "external") => onChange({ connectionType: value })} >
- -
- -
@@ -222,12 +216,12 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult {/* 외부 DB 선택 */} {dataSource.connectionType === "external" && (
-