"use client"; import React, { useState, useCallback, useRef, useEffect } from "react"; import dynamic from "next/dynamic"; import { DashboardElement, QueryResult, Position, ElementSubtype, AXIS_BASED_CHARTS, CIRCULAR_CHARTS, getChartCategory, } from "./types"; import { ChartRenderer } from "./charts/ChartRenderer"; import { GRID_CONFIG, magneticSnap, snapSizeToGrid } from "./gridUtils"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select"; // 위젯 동적 임포트 const WeatherWidget = dynamic(() => import("@/components/dashboard/widgets/WeatherWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const ExchangeWidget = dynamic(() => import("@/components/dashboard/widgets/ExchangeWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const CalculatorWidget = dynamic(() => import("@/components/dashboard/widgets/CalculatorWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const VehicleStatusWidget = dynamic(() => import("@/components/dashboard/widgets/VehicleStatusWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const VehicleListWidget = dynamic(() => import("@/components/dashboard/widgets/VehicleListWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const VehicleMapOnlyWidget = dynamic(() => import("@/components/dashboard/widgets/VehicleMapOnlyWidget"), { ssr: false, loading: () => (
로딩 중...
), }); // 범용 지도 위젯 (차량, 창고, 고객 등 모든 위치 위젯 통합) const MapSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/MapSummaryWidget"), { ssr: false, loading: () => (
로딩 중...
), }); // 🧪 테스트용 지도 위젯 (REST API 지원) const MapTestWidget = dynamic(() => import("@/components/dashboard/widgets/MapTestWidget"), { ssr: false, loading: () => (
로딩 중...
), }); // 🧪 테스트용 지도 위젯 V2 (다중 데이터 소스) const MapTestWidgetV2 = dynamic(() => import("@/components/dashboard/widgets/MapTestWidgetV2"), { ssr: false, loading: () => (
로딩 중...
), }); // 🧪 테스트용 차트 위젯 (다중 데이터 소스) const ChartTestWidget = dynamic(() => import("@/components/dashboard/widgets/ChartTestWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const ListTestWidget = dynamic( () => import("@/components/dashboard/widgets/ListTestWidget").then((mod) => ({ default: mod.ListTestWidget })), { ssr: false, loading: () => (
로딩 중...
), }, ); const CustomMetricTestWidget = dynamic(() => import("@/components/dashboard/widgets/CustomMetricTestWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const RiskAlertTestWidget = dynamic(() => import("@/components/dashboard/widgets/RiskAlertTestWidget"), { ssr: false, loading: () => (
로딩 중...
), }); // 범용 상태 요약 위젯 (차량, 배송 등 모든 상태 위젯 통합) const StatusSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/StatusSummaryWidget"), { ssr: false, loading: () => (
로딩 중...
), }); // 범용 목록 위젯 (차량, 기사, 제품 등 모든 목록 위젯 통합) - 다른 분 작업 중, 임시 주석 /* const ListSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/ListSummaryWidget"), { ssr: false, loading: () =>
로딩 중...
, }); */ // 개별 위젯들 (주석 처리 - StatusSummaryWidget으로 통합됨) // const DeliveryStatusSummaryWidget = dynamic(() => import("@/components/dashboard/widgets/DeliveryStatusSummaryWidget"), { // ssr: false, // loading: () =>
로딩 중...
, // }); // const DeliveryTodayStatsWidget = dynamic(() => import("@/components/dashboard/widgets/DeliveryTodayStatsWidget"), { // ssr: false, // loading: () =>
로딩 중...
, // }); // const CargoListWidget = dynamic(() => import("@/components/dashboard/widgets/CargoListWidget"), { // ssr: false, // loading: () =>
로딩 중...
, // }); // const CustomerIssuesWidget = dynamic(() => import("@/components/dashboard/widgets/CustomerIssuesWidget"), { // ssr: false, // loading: () =>
로딩 중...
, // }); const RiskAlertWidget = dynamic(() => import("@/components/dashboard/widgets/RiskAlertWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const TaskWidget = dynamic(() => import("@/components/dashboard/widgets/TaskWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const BookingAlertWidget = dynamic(() => import("@/components/dashboard/widgets/BookingAlertWidget"), { ssr: false, loading: () => (
로딩 중...
), }); const DocumentWidget = dynamic(() => import("@/components/dashboard/widgets/DocumentWidget"), { ssr: false, loading: () => (
로딩 중...
), }); // 시계 위젯 임포트 import { ClockWidget } from "./widgets/ClockWidget"; // 달력 위젯 임포트 import { CalendarWidget } from "./widgets/CalendarWidget"; // 기사 관리 위젯 임포트 import { DriverManagementWidget } from "./widgets/DriverManagementWidget"; // 리스트 위젯 임포트 (구버전) import { ListWidget } from "./widgets/ListWidget"; import { X } from "lucide-react"; import { Button } from "@/components/ui/button"; // 야드 관리 3D 위젯 const YardManagement3DWidget = dynamic(() => import("./widgets/YardManagement3DWidget"), { ssr: false, loading: () => (
로딩 중...
), }); // 작업 이력 위젯 const WorkHistoryWidget = dynamic(() => import("@/components/dashboard/widgets/WorkHistoryWidget"), { ssr: false, loading: () => (
로딩 중...
), }); // 커스텀 통계 카드 위젯 const CustomStatsWidget = dynamic(() => import("@/components/dashboard/widgets/CustomStatsWidget"), { ssr: false, loading: () => (
로딩 중...
), }); // 사용자 커스텀 카드 위젯 const CustomMetricWidget = dynamic(() => import("@/components/dashboard/widgets/CustomMetricWidget"), { ssr: false, loading: () => (
로딩 중...
), }); interface CanvasElementProps { element: DashboardElement; isSelected: boolean; selectedElements?: string[]; // 🔥 다중 선택된 요소 ID 배열 allElements?: DashboardElement[]; // 🔥 모든 요소 배열 multiDragOffset?: { x: number; y: number }; // 🔥 다중 드래그 시 이 요소의 오프셋 cellSize: number; subGridSize: number; canvasWidth?: number; verticalGuidelines: number[]; horizontalGuidelines: number[]; onUpdate: (id: string, updates: Partial) => void; onUpdateMultiple?: (updates: { id: string; updates: Partial }[]) => void; // 🔥 다중 업데이트 onMultiDragStart?: (draggedId: string, otherOffsets: Record) => void; onMultiDragMove?: (draggedElement: DashboardElement, tempPosition: { x: number; y: number }) => void; onMultiDragEnd?: () => void; onRemove: (id: string) => void; onSelect: (id: string | null) => void; } /** * 캔버스에 배치된 개별 요소 컴포넌트 * - 드래그로 이동 가능 (그리드 스냅) * - 크기 조절 핸들 (그리드 스냅) * - 삭제 버튼 */ export function CanvasElement({ element, isSelected, selectedElements = [], allElements = [], multiDragOffset, cellSize, subGridSize, canvasWidth = 1560, verticalGuidelines, horizontalGuidelines, onUpdate, onUpdateMultiple, onMultiDragStart, onMultiDragMove, onMultiDragEnd, onRemove, onSelect, }: CanvasElementProps) { const [isDragging, setIsDragging] = useState(false); const [isResizing, setIsResizing] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0, elementX: 0, elementY: 0 }); const dragStartRef = useRef({ x: 0, y: 0, elementX: 0, elementY: 0, initialScrollY: 0 }); // 🔥 스크롤 조정용 ref const autoScrollDirectionRef = useRef<"up" | "down" | null>(null); // 🔥 자동 스크롤 방향 const autoScrollFrameRef = useRef(null); // 🔥 requestAnimationFrame ID const lastMouseYRef = useRef(window.innerHeight / 2); // 🔥 마지막 마우스 Y 위치 (초기값: 화면 중간) const [resizeStart, setResizeStart] = useState({ x: 0, y: 0, width: 0, height: 0, elementX: 0, elementY: 0, handle: "", }); const [chartData, setChartData] = useState(null); const [isLoadingData, setIsLoadingData] = useState(false); const elementRef = useRef(null); // 드래그/리사이즈 중 임시 위치/크기 (스냅 전) const [tempPosition, setTempPosition] = useState<{ x: number; y: number } | null>(null); const [tempSize, setTempSize] = useState<{ width: number; height: number } | null>(null); // 요소 선택 처리 const handleMouseDown = useCallback( (e: React.MouseEvent) => { // 모달이나 다이얼로그가 열려있으면 드래그 무시 if (document.querySelector('[role="dialog"]') || document.querySelector('[role="alertdialog"]')) { return; } // 닫기 버튼이나 리사이즈 핸들 클릭 시 무시 if ((e.target as HTMLElement).closest(".element-close, .resize-handle")) { return; } // 위젯 내부 (헤더 제외) 클릭 시 드래그 무시 - 인터랙티브 사용 가능 if ((e.target as HTMLElement).closest(".widget-interactive-area")) { return; } // 선택되지 않은 경우에만 선택 처리 if (!isSelected) { onSelect(element.id); } setIsDragging(true); const startPos = { x: e.clientX, y: e.clientY, elementX: element.position.x, elementY: element.position.y, initialScrollY: window.pageYOffset, // 🔥 드래그 시작 시점의 스크롤 위치 }; setDragStart(startPos); dragStartRef.current = startPos; // 🔥 ref에도 저장 // 🔥 드래그 시작 시 마우스 위치 초기화 (화면 중간) lastMouseYRef.current = window.innerHeight / 2; // 🔥 다중 선택된 경우, 다른 위젯들의 오프셋 계산 if (selectedElements.length > 1 && selectedElements.includes(element.id) && onMultiDragStart) { const offsets: Record = {}; selectedElements.forEach((id) => { if (id !== element.id) { const targetElement = allElements.find((el) => el.id === id); if (targetElement) { offsets[id] = { x: targetElement.position.x - element.position.x, y: targetElement.position.y - element.position.y, }; } } }); onMultiDragStart(element.id, offsets); } e.preventDefault(); }, [ element.id, element.position.x, element.position.y, onSelect, isSelected, selectedElements, allElements, onMultiDragStart, ], ); // 리사이즈 핸들 마우스다운 const handleResizeMouseDown = useCallback( (e: React.MouseEvent, handle: string) => { // 모달이나 다이얼로그가 열려있으면 리사이즈 무시 if (document.querySelector('[role="dialog"]') || document.querySelector('[role="alertdialog"]')) { return; } e.stopPropagation(); setIsResizing(true); setResizeStart({ x: e.clientX, y: e.clientY, width: element.size.width, height: element.size.height, elementX: element.position.x, elementY: element.position.y, handle, }); }, [element.size.width, element.size.height, element.position.x, element.position.y], ); // 마우스 이동 처리 (그리드 스냅 적용) const handleMouseMove = useCallback( (e: MouseEvent) => { if (isDragging) { // 🔥 자동 스크롤: 다중 선택 시 첫 번째 위젯에서만 처리 const isFirstSelectedElement = !selectedElements || selectedElements.length === 0 || selectedElements[0] === element.id; if (isFirstSelectedElement) { const scrollThreshold = 100; const viewportHeight = window.innerHeight; const mouseY = e.clientY; // 🔥 항상 마우스 위치 업데이트 lastMouseYRef.current = mouseY; // console.log("🖱️ 마우스 위치 업데이트:", { mouseY, viewportHeight, top: scrollThreshold, bottom: viewportHeight - scrollThreshold }); } // 🔥 현재 스크롤 위치를 고려한 deltaY 계산 const currentScrollY = window.pageYOffset; const scrollDelta = currentScrollY - dragStartRef.current.initialScrollY; const deltaX = e.clientX - dragStartRef.current.x; const deltaY = e.clientY - dragStartRef.current.y + scrollDelta; // 🔥 스크롤 변화량 반영 // 임시 위치 계산 (드래그 중에는 부드럽게 이동) let rawX = Math.max(0, dragStart.elementX + deltaX); const rawY = Math.max(0, dragStart.elementY + deltaY); // X 좌표가 캔버스 너비를 벗어나지 않도록 제한 const maxX = canvasWidth - element.size.width; rawX = Math.min(rawX, maxX); // 드래그 중에는 스냅 없이 부드럽게 이동 setTempPosition({ x: rawX, y: rawY }); // 🔥 다중 드래그 중 - 다른 위젯들의 위치 업데이트 if (selectedElements.length > 1 && selectedElements.includes(element.id) && onMultiDragMove) { onMultiDragMove(element, { x: rawX, y: rawY }); } } else if (isResizing) { const deltaX = e.clientX - resizeStart.x; const deltaY = e.clientY - resizeStart.y; let newWidth = resizeStart.width; let newHeight = resizeStart.height; let newX = resizeStart.elementX; let newY = resizeStart.elementY; // 최소 크기 설정: 모든 위젯 1x1 const minWidthCells = 1; const minHeightCells = 1; const minWidth = cellSize * minWidthCells; const minHeight = cellSize * minHeightCells; switch (resizeStart.handle) { case "se": // 오른쪽 아래 newWidth = Math.max(minWidth, resizeStart.width + deltaX); newHeight = Math.max(minHeight, resizeStart.height + deltaY); break; case "sw": // 왼쪽 아래 newWidth = Math.max(minWidth, resizeStart.width - deltaX); newHeight = Math.max(minHeight, resizeStart.height + deltaY); newX = resizeStart.elementX + deltaX; break; case "ne": // 오른쪽 위 newWidth = Math.max(minWidth, resizeStart.width + deltaX); newHeight = Math.max(minHeight, resizeStart.height - deltaY); newY = resizeStart.elementY + deltaY; break; case "nw": // 왼쪽 위 newWidth = Math.max(minWidth, resizeStart.width - deltaX); newHeight = Math.max(minHeight, resizeStart.height - deltaY); newX = resizeStart.elementX + deltaX; newY = resizeStart.elementY + deltaY; break; } // 가로 너비가 캔버스를 벗어나지 않도록 제한 const maxWidth = canvasWidth - newX; newWidth = Math.min(newWidth, maxWidth); // 리사이즈 중에는 스냅 없이 부드럽게 조절 const boundedX = Math.max(0, Math.min(newX, canvasWidth - newWidth)); const boundedY = Math.max(0, newY); // 임시 크기/위치 저장 (부드러운 이동) setTempPosition({ x: boundedX, y: boundedY }); setTempSize({ width: newWidth, height: newHeight }); } }, [ isDragging, isResizing, dragStart, resizeStart, element, canvasWidth, cellSize, verticalGuidelines, horizontalGuidelines, selectedElements, allElements, onUpdateMultiple, onMultiDragMove, // dragStartRef, autoScrollDirectionRef, autoScrollFrameRef는 ref라서 dependency 불필요 ], ); // 마우스 업 처리 (이미 스냅된 위치 사용) const handleMouseUp = useCallback(() => { if (isDragging && tempPosition) { // 마우스를 놓을 때 그리드에 스냅 let finalX = magneticSnap(tempPosition.x, verticalGuidelines); const finalY = magneticSnap(tempPosition.y, horizontalGuidelines); // X 좌표가 캔버스 너비를 벗어나지 않도록 최종 제한 const maxX = canvasWidth - element.size.width; finalX = Math.min(finalX, maxX); onUpdate(element.id, { position: { x: finalX, y: finalY }, }); // 🔥 다중 선택된 요소들도 함께 업데이트 if (selectedElements.length > 1 && selectedElements.includes(element.id) && onUpdateMultiple) { const updates = selectedElements .filter((id) => id !== element.id) // 현재 요소 제외 .map((id) => { const targetElement = allElements.find((el) => el.id === id); if (!targetElement) return null; // 현재 요소와의 상대적 위치 유지 const relativeX = targetElement.position.x - dragStart.elementX; const relativeY = targetElement.position.y - dragStart.elementY; const newPosition: Position = { x: Math.max(0, Math.min(canvasWidth - targetElement.size.width, finalX + relativeX)), y: Math.max(0, finalY + relativeY), }; if (targetElement.position.z !== undefined) { newPosition.z = targetElement.position.z; } return { id, updates: { position: { x: Math.max(0, Math.min(canvasWidth - targetElement.size.width, finalX + relativeX)), y: Math.max(0, finalY + relativeY), }, }, }; }) .filter((update): update is { id: string; updates: { position: Position } } => update !== null); if (updates.length > 0) { // console.log("🔥 다중 선택 요소 함께 이동:", updates); onUpdateMultiple(updates); } } setTempPosition(null); // 🔥 다중 드래그 종료 if (onMultiDragEnd) { onMultiDragEnd(); } } if (isResizing && tempPosition && tempSize) { // 마우스를 놓을 때 그리드에 스냅 const finalX = magneticSnap(tempPosition.x, verticalGuidelines); const finalY = magneticSnap(tempPosition.y, horizontalGuidelines); const finalWidth = snapSizeToGrid(tempSize.width, canvasWidth || 1560); const finalHeight = snapSizeToGrid(tempSize.height, canvasWidth || 1560); // 가로 너비가 캔버스를 벗어나지 않도록 최종 제한 const maxWidth = canvasWidth - finalX; const boundedWidth = Math.min(finalWidth, maxWidth); onUpdate(element.id, { position: { x: finalX, y: finalY }, size: { width: boundedWidth, height: finalHeight }, }); setTempPosition(null); setTempSize(null); } setIsDragging(false); setIsResizing(false); // 🔥 자동 스크롤 정리 autoScrollDirectionRef.current = null; if (autoScrollFrameRef.current) { cancelAnimationFrame(autoScrollFrameRef.current); autoScrollFrameRef.current = null; } }, [ isDragging, isResizing, tempPosition, tempSize, element.id, element.size.width, onUpdate, onUpdateMultiple, onMultiDragEnd, cellSize, canvasWidth, selectedElements, allElements, dragStart.elementX, dragStart.elementY, verticalGuidelines, horizontalGuidelines, ]); // 🔥 자동 스크롤 루프 (requestAnimationFrame 사용) useEffect(() => { if (!isDragging) return; const scrollSpeed = 3; // 🔥 속도를 좀 더 부드럽게 (5 → 3) const scrollThreshold = 100; let animationFrameId: number; let lastTime = performance.now(); const autoScrollLoop = (currentTime: number) => { const viewportHeight = window.innerHeight; const lastMouseY = lastMouseYRef.current; // 🔥 스크롤 방향 결정 let shouldScroll = false; let scrollDirection = 0; if (lastMouseY < scrollThreshold) { // 위쪽 영역 shouldScroll = true; scrollDirection = -scrollSpeed; // console.log("⬆️ 위로 스크롤 조건 만족:", { lastMouseY, scrollThreshold }); } else if (lastMouseY > viewportHeight - scrollThreshold) { // 아래쪽 영역 shouldScroll = true; scrollDirection = scrollSpeed; // console.log("⬇️ 아래로 스크롤 조건 만족:", { lastMouseY, boundary: viewportHeight - scrollThreshold }); } // 🔥 프레임 간격 계산 const deltaTime = currentTime - lastTime; // 🔥 10ms 간격으로 스크롤 if (shouldScroll && deltaTime >= 10) { window.scrollBy(0, scrollDirection); // console.log("✅ 스크롤 실행:", { scrollDirection, deltaTime }); lastTime = currentTime; } // 계속 반복 animationFrameId = requestAnimationFrame(autoScrollLoop); }; // 루프 시작 animationFrameId = requestAnimationFrame(autoScrollLoop); autoScrollFrameRef.current = animationFrameId; return () => { if (animationFrameId) { cancelAnimationFrame(animationFrameId); } }; }, [isDragging]); // 전역 마우스 이벤트 등록 React.useEffect(() => { if (isDragging || isResizing) { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; } }, [isDragging, isResizing, handleMouseMove, handleMouseUp]); // 데이터 로딩 const loadChartData = useCallback(async () => { if (!element.dataSource?.query || element.type !== "chart") { return; } setIsLoadingData(true); try { let result; // 필터 적용 (날짜 필터 등) const { applyQueryFilters } = await import("./utils/queryHelpers"); const filteredQuery = applyQueryFilters(element.dataSource.query, element.chartConfig); // 외부 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), filteredQuery, ); if (!externalResult.success) { throw new Error(externalResult.message || "외부 DB 쿼리 실행 실패"); } setChartData({ columns: externalResult.data?.[0] ? Object.keys(externalResult.data[0]) : [], rows: externalResult.data || [], totalRows: externalResult.data?.length || 0, executionTime: 0, }); } else { // 현재 DB const { dashboardApi } = await import("@/lib/api/dashboard"); result = await dashboardApi.executeQuery(filteredQuery); setChartData({ columns: result.columns || [], rows: result.rows || [], totalRows: result.rowCount || 0, executionTime: 0, }); } } catch (error) { // console.error("Chart data loading error:", error); setChartData(null); } finally { setIsLoadingData(false); } }, [ element.dataSource?.query, element.dataSource?.connectionType, element.dataSource?.externalConnectionId, element.chartConfig, element.type, ]); // 컴포넌트 마운트 시 및 쿼리 변경 시 데이터 로딩 useEffect(() => { loadChartData(); }, [loadChartData]); // 자동 새로고침 설정 useEffect(() => { if (!element.dataSource?.refreshInterval || element.dataSource.refreshInterval === 0) { return; } const interval = setInterval(loadChartData, element.dataSource.refreshInterval); return () => clearInterval(interval); }, [element.dataSource?.refreshInterval, loadChartData]); // 요소 삭제 const handleRemove = useCallback(() => { onRemove(element.id); }, [element.id, onRemove]); // 스타일 클래스 생성 const getContentClass = () => { if (element.type === "chart") { switch (element.subtype) { case "bar": return "bg-gradient-to-br from-primary to-purple-500"; case "pie": return "bg-gradient-to-br from-destructive to-destructive/80"; case "line": return "bg-gradient-to-br from-primary to-primary/80"; default: return "bg-muted"; } } else if (element.type === "widget") { switch (element.subtype) { case "exchange": return "bg-gradient-to-br from-warning to-warning/80"; case "weather": return "bg-gradient-to-br from-primary to-primary/80"; case "clock": return "bg-gradient-to-br from-primary to-primary/80"; case "calendar": return "bg-gradient-to-br from-primary to-purple-500"; case "driver-management": return "bg-gradient-to-br from-primary to-primary"; case "list": return "bg-gradient-to-br from-primary to-primary/80"; default: return "bg-muted"; } } return "bg-muted"; }; // 드래그/리사이즈 중일 때는 임시 위치/크기 사용, 아니면 실제 값 사용 // 🔥 다중 드래그 중이면 multiDragOffset 적용 (단, 드래그 중인 위젯은 tempPosition 우선) const displayPosition: Position = tempPosition || (multiDragOffset && !isDragging ? { x: element.position.x + multiDragOffset.x, y: element.position.y + multiDragOffset.y, ...(element.position.z !== undefined && { z: element.position.z }), } : element.position); const displaySize = tempSize || element.size; return (
{/* 헤더 - showHeader가 false이면 숨김 */} {element.showHeader !== false && (
{/* 차트 타입 전환 드롭다운 (차트일 경우만) */} {element.type === "chart" && ( )} {/* 제목 */} {!element.type || element.type !== "chart" ? ( element.subtype === "map-summary-v2" && !element.customTitle ? null : ( {element.customTitle || element.title} ) ) : null}
)} {/* 삭제 버튼 - 항상 표시 (우측 상단 절대 위치) */} {/* 내용 */}
{element.type === "chart" ? ( // 차트 렌더링
{isLoadingData ? (
데이터 로딩 중...
) : ( )}
) : element.type === "widget" && element.subtype === "weather" ? ( // 날씨 위젯 렌더링
) : element.type === "widget" && element.subtype === "exchange" ? ( // 환율 위젯 렌더링
) : element.type === "widget" && element.subtype === "clock" ? ( // 시계 위젯 렌더링
{ onUpdate(element.id, { clockConfig: newConfig }); }} />
) : element.type === "widget" && element.subtype === "calculator" ? ( // 계산기 위젯 렌더링
) : element.type === "widget" && element.subtype === "vehicle-status" ? ( // 차량 상태 현황 위젯 렌더링
) : element.type === "widget" && element.subtype === "vehicle-list" ? ( // 차량 목록 위젯 렌더링
) : element.type === "widget" && element.subtype === "map-summary" ? ( // 커스텀 지도 카드 - 범용 위젯
) : element.type === "widget" && element.subtype === "map-test" ? ( // 🧪 테스트용 지도 위젯 (REST API 지원)
) : element.type === "widget" && element.subtype === "map-summary-v2" ? ( // 지도 위젯 (다중 데이터 소스) - 승격 완료
) : element.type === "widget" && element.subtype === "chart" ? ( // 차트 위젯 (다중 데이터 소스) - 승격 완료
) : element.type === "widget" && element.subtype === "custom-metric-v2" ? ( // 통계 카드 위젯 (다중 데이터 소스) - 승격 완료
) : element.type === "widget" && element.subtype === "risk-alert-v2" ? ( // 리스크/알림 위젯 (다중 데이터 소스) - 승격 완료
) : element.type === "widget" && element.subtype === "vehicle-map" ? ( // 차량 위치 지도 위젯 렌더링 (구버전 - 호환용)
) : element.type === "widget" && element.subtype === "status-summary" ? ( // 커스텀 상태 카드 - 범용 위젯
) : /* element.type === "widget" && element.subtype === "list-summary" ? ( // 커스텀 목록 카드 - 범용 위젯 (다른 분 작업 중 - 임시 주석)
) : */ element.type === "widget" && element.subtype === "delivery-status" ? ( // 배송/화물 현황 위젯 - 범용 위젯 사용 (구버전 호환)
) : element.type === "widget" && element.subtype === "delivery-status-summary" ? ( // 배송 상태 요약 - 범용 위젯 사용
) : element.type === "widget" && element.subtype === "delivery-today-stats" ? ( // 오늘 처리 현황 - 범용 위젯 사용
) : element.type === "widget" && element.subtype === "cargo-list" ? ( // 화물 목록 - 범용 위젯 사용
) : element.type === "widget" && element.subtype === "customer-issues" ? ( // 고객 클레임/이슈 - 범용 위젯 사용
) : element.type === "widget" && element.subtype === "risk-alert" ? ( // 리스크/알림 위젯 렌더링
) : element.type === "widget" && element.subtype === "calendar" ? ( // 달력 위젯 렌더링
{ onUpdate(element.id, { calendarConfig: newConfig }); }} />
) : element.type === "widget" && element.subtype === "driver-management" ? ( // 기사 관리 위젯 렌더링
{ onUpdate(element.id, { driverManagementConfig: newConfig }); }} />
) : element.type === "widget" && (element.subtype === "list" || element.subtype === "list-v2") ? ( // 리스트 위젯 렌더링 (v1 & v2)
) : element.type === "widget" && element.subtype === "yard-management-3d" ? ( // 야드 관리 3D 위젯 렌더링
{ // console.log("🏗️ 야드 설정 업데이트:", { elementId: element.id, newConfig }); onUpdate(element.id, { yardConfig: newConfig }); }} />
) : element.type === "widget" && element.subtype === "work-history" ? ( // 작업 이력 위젯 렌더링
) : element.type === "widget" && element.subtype === "transport-stats" ? ( // 커스텀 통계 카드 위젯 렌더링
) : element.type === "widget" && element.subtype === "custom-metric" ? ( // 사용자 커스텀 카드 위젯 렌더링 (main에서 추가)
) : element.type === "widget" && (element.subtype === "todo" || element.subtype === "maintenance") ? ( // Task 위젯 렌더링 (To-Do + 정비 일정 통합, lhj)
) : element.type === "widget" && element.subtype === "booking-alert" ? ( // 예약 요청 알림 위젯 렌더링
) : element.type === "widget" && element.subtype === "document" ? ( // 문서 다운로드 위젯 렌더링
) : ( // 기타 위젯 렌더링
🔧
{element.content}
)}
{/* 리사이즈 핸들 (선택된 요소에만 표시) */} {isSelected && ( <> )}
); } interface ResizeHandleProps { position: "nw" | "ne" | "sw" | "se"; onMouseDown: (e: React.MouseEvent, handle: string) => void; } /** * 크기 조절 핸들 컴포넌트 */ function ResizeHandle({ position, onMouseDown }: ResizeHandleProps) { const getPositionClass = () => { switch (position) { case "nw": return "top-[-5px] left-[-5px] cursor-nw-resize"; case "ne": return "top-[-5px] right-[-5px] cursor-ne-resize"; case "sw": return "bottom-[-5px] left-[-5px] cursor-sw-resize"; case "se": return "bottom-[-5px] right-[-5px] cursor-se-resize"; } }; return (
onMouseDown(e, position)} /> ); }