"use client"; import React, { useState, useCallback, useEffect } from "react"; import { DashboardElement, ChartDataSource, ElementSubtype, QueryResult, ListWidgetConfig, ChartConfig, CustomMetricConfig, } from "./types"; import { X } from "lucide-react"; import { cn } from "@/lib/utils"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { DatabaseConfig } from "./data-sources/DatabaseConfig"; import { ApiConfig } from "./data-sources/ApiConfig"; import { QueryEditor } from "./QueryEditor"; import { ListWidgetSection } from "./widget-sections/ListWidgetSection"; import { ChartConfigSection } from "./widget-sections/ChartConfigSection"; import { CustomMetricSection } from "./widget-sections/CustomMetricSection"; import { MapConfigSection } from "./widget-sections/MapConfigSection"; import { RiskAlertSection } from "./widget-sections/RiskAlertSection"; import MultiDataSourceConfig from "@/components/admin/dashboard/data-sources/MultiDataSourceConfig"; interface WidgetConfigSidebarProps { element: DashboardElement | null; isOpen: boolean; onClose: () => void; onApply: (element: DashboardElement) => void; } // 위젯 분류 헬퍼 함수 const needsDataSource = (subtype: ElementSubtype): boolean => { // 차트 타입들 const chartTypes = ["bar", "horizontal-bar", "pie", "line", "area", "stacked-bar", "donut", "combo"]; const dataWidgets = [ "list-v2", "custom-metric-v2", "chart", "map-summary-v2", "risk-alert-v2", // "yard-management-3d", // 데이터 탭 불필요 (레이아웃 선택만 사용) "todo", "document", "work-history", "transport-stats", "booking-alert", "maintenance", "vehicle-status", "vehicle-list", "status-summary", "delivery-status", "delivery-status-summary", "delivery-today-stats", "cargo-list", "customer-issues", "driver-management", ]; return chartTypes.includes(subtype) || dataWidgets.includes(subtype); }; const getWidgetIcon = (subtype: ElementSubtype): string => { const iconMap: Record = { "list-v2": "📋", "custom-metric-v2": "📊", chart: "📈", "map-summary-v2": "🗺️", "risk-alert-v2": "⚠️", "yard-management-3d": "🏗️", weather: "🌤️", exchange: "💱", calculator: "🧮", clock: "🕐", calendar: "📅", todo: "✅", document: "📄", }; return iconMap[subtype] || "🔧"; }; const getWidgetTitle = (subtype: ElementSubtype): string => { const titleMap: Record = { "list-v2": "리스트 위젯", "custom-metric-v2": "통계 카드", chart: "차트", "map-summary-v2": "지도", "risk-alert-v2": "리스크 알림", "yard-management-3d": "야드 관리 3D", weather: "날씨 위젯", exchange: "환율 위젯", calculator: "계산기", clock: "시계", calendar: "달력", todo: "할 일", document: "문서", }; return titleMap[subtype] || "위젯"; }; /** * 통합 위젯 설정 사이드바 * - 모든 위젯 타입에 대한 일관된 설정 UI 제공 * - 일반 탭: 제목, 헤더 표시 설정 * - 데이터 탭: 데이터 소스 및 위젯별 커스텀 설정 */ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: WidgetConfigSidebarProps) { // 일반 설정 state const [customTitle, setCustomTitle] = useState(""); const [showHeader, setShowHeader] = useState(true); // 데이터 소스 state const [dataSource, setDataSource] = useState({ type: "database", connectionType: "current", refreshInterval: 0, }); // 다중 데이터 소스 상태 추가 const [dataSources, setDataSources] = useState(element?.dataSources || []); // 쿼리 결과 const [queryResult, setQueryResult] = useState(null); // 리스트 위젯 설정 const [listConfig, setListConfig] = useState({ viewMode: "table", columns: [], pageSize: 10, enablePagination: true, showHeader: true, stripedRows: true, compactMode: false, cardColumns: 3, }); // 차트 설정 const [chartConfig, setChartConfig] = useState({}); // 커스텀 메트릭 설정 const [customMetricConfig, setCustomMetricConfig] = useState({}); // 자동 새로고침 간격 (지도 위젯용) const [refreshInterval, setRefreshInterval] = useState(5); // 사이드바 열릴 때 초기화 useEffect(() => { if (isOpen && element) { setCustomTitle(element.customTitle || ""); setShowHeader(element.showHeader !== false); setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }); // dataSources는 element.dataSources 또는 chartConfig.dataSources에서 가져옴 setDataSources(element.dataSources || element.chartConfig?.dataSources || []); setQueryResult(null); // 자동 새로고침 간격 초기화 setRefreshInterval(element.chartConfig?.refreshInterval ?? 5); // 리스트 위젯 설정 초기화 if (element.subtype === "list-v2" && element.listConfig) { setListConfig(element.listConfig); } else { setListConfig({ viewMode: "table", columns: [], pageSize: 10, enablePagination: true, showHeader: true, stripedRows: true, compactMode: false, cardColumns: 3, }); } // 차트 설정 초기화 setChartConfig(element.chartConfig || {}); // 커스텀 메트릭 설정 초기화 setCustomMetricConfig(element.customMetricConfig || {}); } else if (!isOpen) { // 사이드바 닫힐 때 초기화 setCustomTitle(""); setShowHeader(true); setDataSource({ type: "database", connectionType: "current", refreshInterval: 0 }); setDataSources([]); setQueryResult(null); setListConfig({ viewMode: "table", columns: [], pageSize: 10, enablePagination: true, showHeader: true, stripedRows: true, compactMode: false, cardColumns: 3, }); setChartConfig({}); setCustomMetricConfig({}); } }, [isOpen, element]); // Esc 키로 닫기 useEffect(() => { if (!isOpen) return; const handleEsc = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose(); } }; window.addEventListener("keydown", handleEsc); return () => window.removeEventListener("keydown", handleEsc); }, [isOpen, onClose]); // 데이터 소스 타입 변경 const handleDataSourceTypeChange = useCallback((type: "database" | "api") => { if (type === "database") { setDataSource({ type: "database", connectionType: "current", refreshInterval: 0, }); } else { setDataSource({ type: "api", method: "GET", refreshInterval: 0, }); } }, []); // 데이터 소스 업데이트 const handleDataSourceUpdate = useCallback((updates: Partial) => { setDataSource((prev) => ({ ...prev, ...updates })); }, []); // 다중 데이터 소스 변경 핸들러 const handleDataSourcesChange = useCallback((updatedSources: ChartDataSource[]) => { setDataSources(updatedSources); }, []); // 쿼리 테스트 결과 처리 const handleQueryTest = useCallback( (result: QueryResult) => { setQueryResult(result); // 리스트 위젯: 쿼리 결과로 컬럼 자동 생성 if (element?.subtype === "list-v2" && result.columns && result.columns.length > 0) { const newColumns = result.columns.map((col: string, idx: number) => ({ id: `col_${Date.now()}_${idx}`, field: col, label: col, visible: true, sortable: true, filterable: false, align: "left" as const, })); setListConfig((prev) => ({ ...prev, columns: newColumns })); } }, [element], ); // 리스트 설정 변경 const handleListConfigChange = useCallback((updates: Partial) => { setListConfig((prev) => ({ ...prev, ...updates })); }, []); // 차트 설정 변경 const handleChartConfigChange = useCallback((config: ChartConfig) => { setChartConfig(config); }, []); // 커스텀 메트릭 설정 변경 const handleCustomMetricConfigChange = useCallback((updates: Partial) => { setCustomMetricConfig((prev) => ({ ...prev, ...updates })); }, []); // 적용 const handleApply = useCallback(() => { if (!element) return; // 다중 데이터 소스를 사용하는 위젯 체크 const isMultiDataSourceWidget = element.subtype === "map-summary-v2" || element.subtype === "chart" || element.subtype === "list-v2" || element.subtype === "custom-metric-v2" || element.subtype === "risk-alert-v2"; // chartConfig 구성 (위젯 타입별로 다르게 처리) let finalChartConfig = { ...chartConfig }; if (isMultiDataSourceWidget) { finalChartConfig = { ...finalChartConfig, dataSources: dataSources, }; } // 지도 위젯인 경우 refreshInterval 추가 if (element.subtype === "map-summary-v2") { finalChartConfig = { ...finalChartConfig, refreshInterval, }; } const updatedElement: DashboardElement = { ...element, customTitle: customTitle.trim() || undefined, showHeader, // 데이터 소스 처리 ...(needsDataSource(element.subtype) ? { dataSource, // 다중 데이터 소스 위젯은 dataSources도 포함 (빈 배열도 허용 - 연결 해제) ...(isMultiDataSourceWidget ? { dataSources: dataSources, } : {}), } : {}), // 리스트 위젯 설정 ...(element.subtype === "list-v2" ? { listConfig, } : {}), // 차트 설정 (모든 위젯 공통) ...(needsDataSource(element.subtype) ? { chartConfig: finalChartConfig, } : {}), // 커스텀 메트릭 설정 ...(element.subtype === "custom-metric-v2" ? { customMetricConfig, } : {}), }; console.log("🔧 [WidgetConfigSidebar] handleApply 호출:", { subtype: element.subtype, isMultiDataSourceWidget, dataSources, listConfig, finalChartConfig, customMetricConfig, updatedElement, }); onApply(updatedElement); onClose(); }, [ element, customTitle, showHeader, dataSource, dataSources, listConfig, chartConfig, customMetricConfig, refreshInterval, onApply, onClose, ]); if (!element) return null; const hasDataTab = needsDataSource(element.subtype); const widgetIcon = getWidgetIcon(element.subtype); const widgetTitle = getWidgetTitle(element.subtype); return (
{/* 헤더 */}
{widgetIcon}
{widgetTitle} 설정
{/* 탭 영역 */} 일반 {hasDataTab && ( 데이터 )} {/* 일반 탭 */}
{/* 위젯 제목 */}
setCustomTitle(e.target.value)} placeholder={`기본 제목: ${element.title}`} className="h-9 text-sm" />

비워두면 기본 제목이 표시됩니다

{/* 헤더 표시 */}

위젯 상단 헤더를 표시합니다

{/* 레이아웃 선택 (야드 관리 3D 위젯 전용) */} {element.subtype === "yard-management-3d" && (

표시할 디지털 트윈 레이아웃을 선택하세요

위젯 내부에서 레이아웃을 선택할 수 있습니다.

편집 모드에서 레이아웃 목록을 확인하고 선택하세요.

)} {/* 자동 새로고침 설정 (지도 위젯 전용) */} {element.subtype === "map-summary-v2" && (

위젯의 모든 데이터를 자동으로 갱신하는 주기를 설정합니다

)}
{/* 데이터 탭 */} {hasDataTab && (
{/* 데이터 소스 선택 - 단일 데이터 소스 위젯에만 표시 */} {!["map-summary-v2", "chart", "risk-alert-v2"].includes(element.subtype) && (
handleDataSourceTypeChange(value as "database" | "api")} className="w-full" > 데이터베이스 REST API
)} {/* 다중 데이터 소스 설정 */} {["map-summary-v2", "chart", "risk-alert-v2"].includes(element.subtype) && ( )} {/* 위젯별 커스텀 섹션 */} {element.subtype === "list-v2" && ( )} {/* 차트 설정 */} {(element.type === "chart" || element.subtype === "chart" || ["bar", "horizontal-bar", "pie", "line", "area", "stacked-bar", "donut", "combo"].includes( element.subtype, )) && ( )} {/* 커스텀 메트릭 설정 */} {element.subtype === "custom-metric-v2" && ( )} {/* 지도 설정 */} {element.subtype === "map-summary-v2" && ( { setChartConfig((prev) => ({ ...prev, refreshInterval: interval, })); }} onMarkerTypeChange={(type) => { setChartConfig((prev) => ({ ...prev, markerType: type, })); }} /> )} {/* 리스크 알림 설정 */} {element.subtype === "risk-alert-v2" && }
)}
{/* 푸터 */}
); }