"use client"; import React, { useState, useCallback, useEffect } from "react"; import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from "./types"; import { QueryEditor } from "./QueryEditor"; import { ChartConfigPanel } from "./ChartConfigPanel"; import { VehicleMapConfigPanel } from "./VehicleMapConfigPanel"; import { MapTestConfigPanel } from "./MapTestConfigPanel"; import { DatabaseConfig } from "./data-sources/DatabaseConfig"; import { ApiConfig } from "./data-sources/ApiConfig"; import MultiDataSourceConfig from "./data-sources/MultiDataSourceConfig"; import { ListWidgetConfigSidebar } from "./widgets/ListWidgetConfigSidebar"; import { YardWidgetConfigSidebar } from "./widgets/YardWidgetConfigSidebar"; import { X } from "lucide-react"; import { cn } from "@/lib/utils"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import CustomMetricConfigSidebar from "./widgets/custom-metric/CustomMetricConfigSidebar"; interface ElementConfigSidebarProps { element: DashboardElement | null; isOpen: boolean; onClose: () => void; onApply: (element: DashboardElement) => void; } /** * 요소 설정 사이드바 컴포넌트 * - 왼쪽에서 슬라이드 인/아웃 * - 캔버스 위에 오버레이 * - "적용" 버튼으로 명시적 저장 */ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: ElementConfigSidebarProps) { const [dataSource, setDataSource] = useState({ type: "database", connectionType: "current", refreshInterval: 0, }); const [dataSources, setDataSources] = useState([]); const [chartConfig, setChartConfig] = useState({}); const [queryResult, setQueryResult] = useState(null); const [customTitle, setCustomTitle] = useState(""); const [showHeader, setShowHeader] = useState(true); // 사이드바가 열릴 때 초기화 useEffect(() => { if (isOpen && element) { setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }); // dataSources는 element.dataSources 또는 chartConfig.dataSources에서 로드 setDataSources(element.dataSources || element.chartConfig?.dataSources || []); setChartConfig(element.chartConfig || {}); setQueryResult(null); setCustomTitle(element.customTitle || ""); setShowHeader(element.showHeader !== false); } }, [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, }); } setQueryResult(null); setChartConfig({}); }, []); // 데이터 소스 업데이트 const handleDataSourceUpdate = useCallback((updates: Partial) => { setDataSource((prev) => ({ ...prev, ...updates })); }, []); // 차트 설정 변경 처리 const handleChartConfigChange = useCallback( (newConfig: ChartConfig) => { setChartConfig(newConfig); // 🎯 실시간 미리보기: 즉시 부모에게 전달 (map-test 위젯용) if (element && element.subtype === "map-test" && newConfig.tileMapUrl) { onApply({ ...element, chartConfig: newConfig, dataSource: dataSource, customTitle: customTitle, showHeader: showHeader, }); } }, [element, dataSource, customTitle, showHeader, onApply], ); // 쿼리 테스트 결과 처리 const handleQueryTest = useCallback((result: QueryResult) => { setQueryResult(result); setChartConfig({}); }, []); // 적용 처리 const handleApply = useCallback(() => { if (!element) return; console.log("🔧 적용 버튼 클릭 - dataSource:", dataSource); console.log("🔧 적용 버튼 클릭 - dataSources:", dataSources); console.log("🔧 적용 버튼 클릭 - chartConfig:", chartConfig); // 다중 데이터 소스 위젯 체크 const isMultiDS = element.subtype === "map-test-v2" || element.subtype === "chart-test" || element.subtype === "list-test" || element.subtype === "custom-metric-test" || element.subtype === "risk-alert-test"; const updatedElement: DashboardElement = { ...element, // 다중 데이터 소스 위젯은 dataSources를 chartConfig에 저장 chartConfig: isMultiDS ? { ...chartConfig, dataSources } : chartConfig, dataSources: isMultiDS ? dataSources : undefined, // 프론트엔드 호환성 dataSource: isMultiDS ? undefined : dataSource, customTitle: customTitle.trim() || undefined, showHeader, }; console.log("🔧 적용할 요소:", updatedElement); onApply(updatedElement); // 사이드바는 열린 채로 유지 (연속 수정 가능) }, [element, dataSource, dataSources, chartConfig, customTitle, showHeader, onApply]); // 요소가 없으면 렌더링하지 않음 if (!element) return null; // 리스트 위젯은 별도 사이드바로 처리 if (element.subtype === "list") { return ( { onApply(updatedElement); }} /> ); } // 야드 위젯은 사이드바로 처리 if (element.subtype === "yard-management-3d") { return ( { onApply({ ...element, ...updates }); }} onClose={onClose} /> ); } // 사용자 커스텀 카드 위젯은 사이드바로 처리 if (element.subtype === "custom-metric") { return ( { onApply({ ...element, ...updates }); }} /> ); } // 차트 설정이 필요 없는 위젯 (쿼리/API만 필요) const isSimpleWidget = element.subtype === "todo" || element.subtype === "booking-alert" || element.subtype === "maintenance" || element.subtype === "document" || element.subtype === "risk-alert" || element.subtype === "vehicle-status" || element.subtype === "vehicle-list" || element.subtype === "status-summary" || element.subtype === "delivery-status" || element.subtype === "delivery-status-summary" || element.subtype === "delivery-today-stats" || element.subtype === "cargo-list" || element.subtype === "customer-issues" || element.subtype === "driver-management" || element.subtype === "work-history" || element.subtype === "transport-stats"; // 자체 기능 위젯 (DB 연결 불필요, 헤더 설정만 가능) const isSelfContainedWidget = element.subtype === "weather" || element.subtype === "exchange" || element.subtype === "calculator"; // 지도 위젯 (위도/경도 매핑 필요) const isMapWidget = element.subtype === "vehicle-map" || element.subtype === "map-summary" || element.subtype === "map-test"; // 헤더 전용 위젯 const isHeaderOnlyWidget = element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar" || isSelfContainedWidget); // 다중 데이터 소스 테스트 위젯 const isMultiDataSourceWidget = element.subtype === "map-test-v2" || element.subtype === "chart-test" || element.subtype === "list-test" || element.subtype === "custom-metric-test" || element.subtype === "status-summary-test" || element.subtype === "risk-alert-test"; // 저장 가능 여부 확인 const isPieChart = element.subtype === "pie" || element.subtype === "donut"; const isApiSource = dataSource.type === "api"; const hasYAxis = chartConfig.yAxis && (typeof chartConfig.yAxis === "string" || (Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0)); const isTitleChanged = customTitle.trim() !== (element.customTitle || ""); const isHeaderChanged = showHeader !== (element.showHeader !== false); const canApply = isTitleChanged || isHeaderChanged || (isMultiDataSourceWidget ? true // 다중 데이터 소스 위젯은 항상 적용 가능 : isSimpleWidget ? queryResult && queryResult.rows.length > 0 : isMapWidget ? element.subtype === "map-test" ? chartConfig.tileMapUrl || (queryResult && queryResult.rows.length > 0) // 🧪 지도 테스트 위젯: 타일맵 URL 또는 API 데이터 : queryResult && queryResult.rows.length > 0 && chartConfig.latitudeColumn && chartConfig.longitudeColumn : queryResult && queryResult.rows.length > 0 && chartConfig.xAxis && (isPieChart || isApiSource ? (chartConfig.aggregation === "count" ? true : hasYAxis) : hasYAxis)); return (
{/* 헤더 */}
{element.title}
{/* 본문: 스크롤 가능 영역 */}
{/* 기본 설정 카드 */}
기본 설정
{/* 커스텀 제목 입력 */}
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" />
{/* 헤더 표시 옵션 */}
{/* 다중 데이터 소스 위젯 */} {isMultiDataSourceWidget && ( <>
{/* 지도 테스트 V2: 타일맵 URL 설정 */} {element.subtype === "map-test-v2" && (
타일맵 설정 (선택사항)
기본 VWorld 타일맵 사용 중
)} )} {/* 헤더 전용 위젯이 아닐 때만 데이터 소스 표시 */} {!isHeaderOnlyWidget && !isMultiDataSourceWidget && (
데이터 소스
handleDataSourceTypeChange(value as "database" | "api")} className="w-full" > 데이터베이스 REST API {/* 차트/지도 설정 */} {!isSimpleWidget && (element.subtype === "map-test" || (queryResult && queryResult.rows.length > 0)) && (
{isMapWidget ? ( element.subtype === "map-test" ? ( ) : ( queryResult && queryResult.rows.length > 0 && ( ) ) ) : ( queryResult && queryResult.rows.length > 0 && ( ) )}
)}
{/* 차트/지도 설정 */} {!isSimpleWidget && (element.subtype === "map-test" || (queryResult && queryResult.rows.length > 0)) && (
{isMapWidget ? ( element.subtype === "map-test" ? ( ) : ( queryResult && queryResult.rows.length > 0 && ( ) ) ) : ( queryResult && queryResult.rows.length > 0 && ( ) )}
)}
{/* 데이터 로드 상태 */} {queryResult && (
{queryResult.rows.length}개 데이터 로드됨
)}
)}
{/* 푸터: 적용 버튼 */}
); }