"use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { DashboardElement, QueryResult } from "@/components/admin/dashboard/types"; import { ChartRenderer } from "@/components/admin/dashboard/charts/ChartRenderer"; import { DashboardProvider } from "@/contexts/DashboardContext"; import { RESOLUTIONS, Resolution } from "@/components/admin/dashboard/ResolutionSelector"; import dynamic from "next/dynamic"; // 위젯 동적 import - 모든 위젯 const MapSummaryWidget = dynamic(() => import("./widgets/MapSummaryWidget"), { ssr: false }); const MapTestWidget = dynamic(() => import("./widgets/MapTestWidget"), { ssr: false }); const MapTestWidgetV2 = dynamic(() => import("./widgets/MapTestWidgetV2"), { ssr: false }); const ChartTestWidget = dynamic(() => import("./widgets/ChartTestWidget"), { ssr: false }); const ListTestWidget = dynamic( () => import("./widgets/ListTestWidget").then((mod) => ({ default: mod.ListTestWidget })), { ssr: false }, ); const CustomMetricTestWidget = dynamic(() => import("./widgets/CustomMetricTestWidget"), { ssr: false }); const RiskAlertTestWidget = dynamic(() => import("./widgets/RiskAlertTestWidget"), { ssr: false }); const StatusSummaryWidget = dynamic(() => import("./widgets/StatusSummaryWidget"), { ssr: false }); const RiskAlertWidget = dynamic(() => import("./widgets/RiskAlertWidget"), { ssr: false }); const WeatherWidget = dynamic(() => import("./widgets/WeatherWidget"), { ssr: false }); const WeatherMapWidget = dynamic(() => import("./widgets/WeatherMapWidget"), { ssr: false }); const ExchangeWidget = dynamic(() => import("./widgets/ExchangeWidget"), { ssr: false }); const VehicleStatusWidget = dynamic(() => import("./widgets/VehicleStatusWidget"), { ssr: false }); const VehicleListWidget = dynamic(() => import("./widgets/VehicleListWidget"), { ssr: false }); const VehicleMapOnlyWidget = dynamic(() => import("./widgets/VehicleMapOnlyWidget"), { ssr: false }); const CargoListWidget = dynamic(() => import("./widgets/CargoListWidget"), { ssr: false }); const CustomerIssuesWidget = dynamic(() => import("./widgets/CustomerIssuesWidget"), { ssr: false }); const DeliveryStatusWidget = dynamic(() => import("./widgets/DeliveryStatusWidget"), { ssr: false }); const DeliveryStatusSummaryWidget = dynamic(() => import("./widgets/DeliveryStatusSummaryWidget"), { ssr: false }); const DeliveryTodayStatsWidget = dynamic(() => import("./widgets/DeliveryTodayStatsWidget"), { ssr: false }); const TaskWidget = dynamic(() => import("./widgets/TaskWidget"), { ssr: false }); const DocumentWidget = dynamic(() => import("./widgets/DocumentWidget"), { ssr: false }); const BookingAlertWidget = dynamic(() => import("./widgets/BookingAlertWidget"), { ssr: false }); const CalculatorWidget = dynamic(() => import("./widgets/CalculatorWidget"), { ssr: false }); const CalendarWidget = dynamic( () => import("@/components/admin/dashboard/widgets/CalendarWidget").then((mod) => ({ default: mod.CalendarWidget })), { ssr: false }, ); const ClockWidget = dynamic( () => import("@/components/admin/dashboard/widgets/ClockWidget").then((mod) => ({ default: mod.ClockWidget })), { ssr: false }, ); const ListWidget = dynamic( () => import("@/components/admin/dashboard/widgets/ListWidget").then((mod) => ({ default: mod.ListWidget })), { ssr: false }, ); const YardManagement3DWidget = dynamic(() => import("@/components/admin/dashboard/widgets/YardManagement3DWidget"), { ssr: false, }); const WorkHistoryWidget = dynamic(() => import("./widgets/WorkHistoryWidget"), { ssr: false, }); const CustomStatsWidget = dynamic(() => import("./widgets/CustomStatsWidget"), { ssr: false, }); const CustomMetricWidget = dynamic(() => import("./widgets/CustomMetricWidget"), { ssr: false, }); /** * 위젯 렌더링 함수 - DashboardSidebar의 모든 subtype 처리 * ViewerElement에서 사용하기 위해 컴포넌트 외부에 정의 */ function renderWidget(element: DashboardElement) { switch (element.subtype) { // 차트는 ChartRenderer에서 처리됨 (이 함수 호출 안됨) // === 위젯 종류 === case "exchange": return ; case "weather": return ; case "weather-map": return ; case "calculator": return ; case "clock": return ; case "map-summary": return ; case "map-test": return ; case "map-test-v2": return ; case "chart-test": return ; case "list-test": return ; case "custom-metric-test": return ; case "risk-alert-test": return ; case "risk-alert": return ; case "calendar": return ; case "status-summary": return ; case "custom-metric": return ; // === 운영/작업 지원 === case "todo": case "maintenance": return ; case "booking-alert": return ; case "document": return ; case "list": return ; case "yard-management-3d": // console.log("🏗️ 야드관리 위젯 렌더링:", { // elementId: element.id, // yardConfig: element.yardConfig, // yardConfigType: typeof element.yardConfig, // hasLayoutId: !!element.yardConfig?.layoutId, // layoutId: element.yardConfig?.layoutId, // layoutName: element.yardConfig?.layoutName, // }); return ; case "work-history": return ; case "transport-stats": // console.log("📊 [DashboardViewer] CustomStatsWidget 렌더링:", { // elementId: element.id, // hasDataSource: !!element.dataSource, // query: element.dataSource?.query?.substring(0, 50) + "...", // dataSourceType: element.dataSource?.type, // }); return ; // === 차량 관련 (추가 위젯) === case "vehicle-status": return ; case "vehicle-list": return ; case "vehicle-map": return ; // === 배송 관련 (추가 위젯) === case "delivery-status": return ; case "delivery-status-summary": return ; case "delivery-today-stats": return ; case "cargo-list": return ; case "customer-issues": return ; // === 기본 fallback === default: return (
알 수 없는 위젯 타입: {element.subtype}
); } } interface DashboardViewerProps { elements: DashboardElement[]; dashboardId?: string; refreshInterval?: number; // 전체 대시보드 새로고침 간격 (ms) backgroundColor?: string; // 배경색 resolution?: string; // 대시보드 해상도 } /** * 대시보드 뷰어 컴포넌트 * - 저장된 대시보드를 읽기 전용으로 표시 * - 실시간 데이터 업데이트 * - 편집 화면과 동일한 레이아웃 (중앙 정렬, 고정 크기) */ export function DashboardViewer({ elements, refreshInterval, backgroundColor = "#f9fafb", resolution = "fhd", }: DashboardViewerProps) { const [elementData, setElementData] = useState>({}); const [loadingElements, setLoadingElements] = useState>(new Set()); // 캔버스 설정 계산 const canvasConfig = useMemo(() => { return RESOLUTIONS[resolution as Resolution] || RESOLUTIONS.fhd; }, [resolution]); // 캔버스 높이 동적 계산 const canvasHeight = useMemo(() => { if (elements.length === 0) { return canvasConfig.height; } const maxBottomY = Math.max(...elements.map((el) => el.position.y + el.size.height)); return Math.max(canvasConfig.height, maxBottomY + 100); }, [elements, canvasConfig.height]); // 개별 요소 데이터 로딩 const loadElementData = useCallback(async (element: DashboardElement) => { if (!element.dataSource?.query || element.type !== "chart") { return; } setLoadingElements((prev) => new Set([...prev, element.id])); try { let result; // 외부 DB vs 현재 DB 분기 if (element.dataSource.connectionType === "external" && element.dataSource.externalConnectionId) { // 외부 DB const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection"); const externalResult = await ExternalDbConnectionAPI.executeQuery( parseInt(element.dataSource.externalConnectionId), element.dataSource.query, ); if (!externalResult.success) { throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패"); } const data: QueryResult = { columns: externalResult.data?.[0] ? Object.keys(externalResult.data[0]) : [], rows: externalResult.data || [], totalRows: externalResult.data?.length || 0, executionTime: 0, }; setElementData((prev) => ({ ...prev, [element.id]: data, })); } else { // 현재 DB const { dashboardApi } = await import("@/lib/api/dashboard"); result = await dashboardApi.executeQuery(element.dataSource.query); const data: QueryResult = { columns: result.columns || [], rows: result.rows || [], totalRows: result.rowCount || 0, executionTime: 0, }; setElementData((prev) => ({ ...prev, [element.id]: data, })); } } catch { // 에러 발생 시 무시 (차트는 빈 상태로 표시됨) } finally { setLoadingElements((prev) => { const newSet = new Set(prev); newSet.delete(element.id); return newSet; }); } }, []); // 모든 요소 데이터 로딩 const loadAllData = useCallback(async () => { const chartElements = elements.filter((el) => el.type === "chart" && el.dataSource?.query); // 병렬로 모든 차트 데이터 로딩 await Promise.all(chartElements.map((element) => loadElementData(element))); }, [elements, loadElementData]); // 초기 데이터 로딩 useEffect(() => { loadAllData(); }, [loadAllData]); // 전체 새로고침 간격 설정 useEffect(() => { if (!refreshInterval || refreshInterval === 0) { return; } const interval = setInterval(loadAllData, refreshInterval); return () => clearInterval(interval); }, [refreshInterval, loadAllData]); // 요소를 자연스러운 읽기 순서로 정렬 (왼쪽→오른쪽, 위→아래) - 태블릿 이하에서 세로 정렬 시 사용 const sortedElements = useMemo(() => { return [...elements].sort((a, b) => { // Y 좌표 차이가 50px 이상이면 Y 우선 (같은 행으로 간주 안함) const yDiff = a.position.y - b.position.y; if (Math.abs(yDiff) > 50) { return yDiff; } // 같은 행이면 X 좌표로 정렬 return a.position.x - b.position.x; }); }, [elements]); // 요소가 없는 경우 if (elements.length === 0) { return (
📊
표시할 요소가 없습니다
대시보드 편집기에서 차트나 위젯을 추가해보세요
); } return ( {/* 데스크톱: 디자이너에서 설정한 위치 그대로 렌더링 (화면에 맞춰 비율 유지) */}
{sortedElements.map((element) => ( loadElementData(element)} isMobile={false} canvasWidth={canvasConfig.width} /> ))}
{/* 태블릿 이하: 반응형 세로 정렬 */}
{sortedElements.map((element) => ( loadElementData(element)} isMobile={true} /> ))}
); } interface ViewerElementProps { element: DashboardElement; data?: QueryResult; isLoading: boolean; onRefresh: () => void; isMobile: boolean; canvasWidth?: number; } /** * 개별 뷰어 요소 컴포넌트 * - 데스크톱(lg 이상): absolute positioning으로 디자이너에서 설정한 위치 그대로 렌더링 (너비는 화면 비율에 따라 조정) * - 태블릿 이하: 세로 스택 카드 레이아웃 */ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWidth = 1920 }: ViewerElementProps) { const [isMounted, setIsMounted] = useState(false); // 마운트 확인 (Leaflet 지도 초기화 문제 해결) useEffect(() => { setIsMounted(true); }, []); if (isMobile) { // 태블릿 이하: 세로 스택 카드 스타일 return (
{element.showHeader !== false && (

{element.customTitle || element.title}

)}
{!isMounted ? (
) : element.type === "chart" ? ( ) : ( renderWidget(element) )}
{isLoading && (
업데이트 중...
)}
); } // 데스크톱: 디자이너에서 설정한 위치 그대로 absolute positioning // 단, 너비는 화면 크기에 따라 비율로 조정 const widthPercentage = (element.size.width / canvasWidth) * 100; return (
{element.showHeader !== false && (

{element.customTitle || element.title}

)}
{!isMounted ? (
) : element.type === "chart" ? ( ) : ( renderWidget(element) )}
{isLoading && (
업데이트 중...
)}
); }