diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 35c3bb02..f646061c 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -39,6 +39,10 @@ const RiskAlertWidget = dynamic(() => import("@/components/dashboard/widgets/Ris // 시계 위젯 임포트 import { ClockWidget } from "./widgets/ClockWidget"; +// 달력 위젯 임포트 +import { CalendarWidget } from "./widgets/CalendarWidget"; +// 기사 관리 위젯 임포트 +import { DriverManagementWidget } from "./widgets/DriverManagementWidget"; interface CanvasElementProps { element: DashboardElement; @@ -137,9 +141,13 @@ export function CanvasElement({ const deltaY = e.clientY - dragStart.y; // 임시 위치 계산 (스냅 안 됨) - const rawX = Math.max(0, dragStart.elementX + deltaX); + let rawX = Math.max(0, dragStart.elementX + deltaX); const rawY = Math.max(0, dragStart.elementY + deltaY); + // X 좌표가 캔버스 너비를 벗어나지 않도록 제한 + const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width; + rawX = Math.min(rawX, maxX); + setTempPosition({ x: rawX, y: rawY }); } else if (isResizing) { const deltaX = e.clientX - resizeStart.x; @@ -150,46 +158,58 @@ export function CanvasElement({ let newX = resizeStart.elementX; let newY = resizeStart.elementY; - const minSize = GRID_CONFIG.CELL_SIZE * 2; // 최소 2셀 + // 최소 크기 설정: 달력은 2x3, 나머지는 2x2 + const minWidthCells = 2; + const minHeightCells = element.type === "widget" && element.subtype === "calendar" ? 3 : 2; + const minWidth = GRID_CONFIG.CELL_SIZE * minWidthCells; + const minHeight = GRID_CONFIG.CELL_SIZE * minHeightCells; switch (resizeStart.handle) { case "se": // 오른쪽 아래 - newWidth = Math.max(minSize, resizeStart.width + deltaX); - newHeight = Math.max(minSize, resizeStart.height + deltaY); + newWidth = Math.max(minWidth, resizeStart.width + deltaX); + newHeight = Math.max(minHeight, resizeStart.height + deltaY); break; case "sw": // 왼쪽 아래 - newWidth = Math.max(minSize, resizeStart.width - deltaX); - newHeight = Math.max(minSize, resizeStart.height + deltaY); + newWidth = Math.max(minWidth, resizeStart.width - deltaX); + newHeight = Math.max(minHeight, resizeStart.height + deltaY); newX = resizeStart.elementX + deltaX; break; case "ne": // 오른쪽 위 - newWidth = Math.max(minSize, resizeStart.width + deltaX); - newHeight = Math.max(minSize, resizeStart.height - deltaY); + newWidth = Math.max(minWidth, resizeStart.width + deltaX); + newHeight = Math.max(minHeight, resizeStart.height - deltaY); newY = resizeStart.elementY + deltaY; break; case "nw": // 왼쪽 위 - newWidth = Math.max(minSize, resizeStart.width - deltaX); - newHeight = Math.max(minSize, resizeStart.height - deltaY); + 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 = GRID_CONFIG.CANVAS_WIDTH - newX; + newWidth = Math.min(newWidth, maxWidth); + // 임시 크기/위치 저장 (스냅 안 됨) setTempPosition({ x: Math.max(0, newX), y: Math.max(0, newY) }); setTempSize({ width: newWidth, height: newHeight }); } }, - [isDragging, isResizing, dragStart, resizeStart], + [isDragging, isResizing, dragStart, resizeStart, element.size.width, element.type, element.subtype], ); // 마우스 업 처리 (그리드 스냅 적용) const handleMouseUp = useCallback(() => { if (isDragging && tempPosition) { // 드래그 종료 시 그리드에 스냅 (동적 셀 크기 사용) - const snappedX = snapToGrid(tempPosition.x, cellSize); + let snappedX = snapToGrid(tempPosition.x, cellSize); const snappedY = snapToGrid(tempPosition.y, cellSize); + // X 좌표가 캔버스 너비를 벗어나지 않도록 최종 제한 + const maxX = GRID_CONFIG.CANVAS_WIDTH - element.size.width; + snappedX = Math.min(snappedX, maxX); + onUpdate(element.id, { position: { x: snappedX, y: snappedY }, }); @@ -201,9 +221,13 @@ export function CanvasElement({ // 리사이즈 종료 시 그리드에 스냅 (동적 셀 크기 사용) const snappedX = snapToGrid(tempPosition.x, cellSize); const snappedY = snapToGrid(tempPosition.y, cellSize); - const snappedWidth = snapSizeToGrid(tempSize.width, 2, cellSize); + let snappedWidth = snapSizeToGrid(tempSize.width, 2, cellSize); const snappedHeight = snapSizeToGrid(tempSize.height, 2, cellSize); + // 가로 너비가 캔버스를 벗어나지 않도록 최종 제한 + const maxWidth = GRID_CONFIG.CANVAS_WIDTH - snappedX; + snappedWidth = Math.min(snappedWidth, maxWidth); + onUpdate(element.id, { position: { x: snappedX, y: snappedY }, size: { width: snappedWidth, height: snappedHeight }, @@ -215,7 +239,7 @@ export function CanvasElement({ setIsDragging(false); setIsResizing(false); - }, [isDragging, isResizing, tempPosition, tempSize, element.id, onUpdate, cellSize]); + }, [isDragging, isResizing, tempPosition, tempSize, element.id, element.size.width, onUpdate, cellSize]); // 전역 마우스 이벤트 등록 React.useEffect(() => { @@ -253,12 +277,11 @@ export function CanvasElement({ executionTime: 0, }); } catch (error) { - // console.error('❌ 데이터 로딩 오류:', error); setChartData(null); } finally { setIsLoadingData(false); } - }, [element.dataSource?.query, element.type, element.subtype]); + }, [element.dataSource?.query, element.type]); // 컴포넌트 마운트 시 및 쿼리 변경 시 데이터 로딩 useEffect(() => { @@ -301,6 +324,10 @@ export function CanvasElement({ return "bg-gradient-to-br from-cyan-400 to-indigo-800"; case "clock": return "bg-gradient-to-br from-teal-400 to-cyan-600"; + case "calendar": + return "bg-gradient-to-br from-indigo-400 to-purple-600"; + case "driver-management": + return "bg-gradient-to-br from-blue-400 to-indigo-600"; default: return "bg-gray-200"; } @@ -330,16 +357,20 @@ export function CanvasElement({
{element.title}
- {/* 설정 버튼 (시계 위젯은 자체 설정 UI 사용) */} - {onConfigure && !(element.type === "widget" && element.subtype === "clock") && ( - - )} + {/* 설정 버튼 (시계, 달력, 기사관리 위젯은 자체 설정 UI 사용) */} + {onConfigure && + !( + element.type === "widget" && + (element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "driver-management") + ) && ( + + )} {/* 삭제 버튼 */}
diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 50909504..eb936652 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -15,10 +15,12 @@ export type ElementSubtype = | "exchange" | "weather" | "clock" + | "calendar" | "calculator" | "vehicle-map" | "delivery-status" - | "risk-alert"; // 위젯 타입 + | "risk-alert" + | "driver-management"; // 위젯 타입 export interface Position { x: number; @@ -41,6 +43,8 @@ export interface DashboardElement { dataSource?: ChartDataSource; // 데이터 소스 설정 chartConfig?: ChartConfig; // 차트 설정 clockConfig?: ClockConfig; // 시계 설정 + calendarConfig?: CalendarConfig; // 달력 설정 + driverManagementConfig?: DriverManagementConfig; // 기사 관리 설정 } export interface DragData { @@ -90,3 +94,42 @@ export interface ClockConfig { theme: "light" | "dark" | "custom"; // 테마 customColor?: string; // 사용자 지정 색상 (custom 테마일 때) } + +// 달력 위젯 설정 +export interface CalendarConfig { + view: "month" | "week" | "day"; // 뷰 타입 + startWeekOn: "monday" | "sunday"; // 주 시작 요일 + highlightWeekends: boolean; // 주말 강조 + highlightToday: boolean; // 오늘 강조 + showHolidays: boolean; // 공휴일 표시 + theme: "light" | "dark" | "custom"; // 테마 + customColor?: string; // 사용자 지정 색상 + showWeekNumbers?: boolean; // 주차 표시 (선택) +} + +// 기사 관리 위젯 설정 +export interface DriverManagementConfig { + viewType: "list"; // 뷰 타입 (현재는 리스트만) + autoRefreshInterval: number; // 자동 새로고침 간격 (초) + visibleColumns: string[]; // 표시할 컬럼 목록 + theme: "light" | "dark" | "custom"; // 테마 + customColor?: string; // 사용자 지정 색상 + statusFilter: "all" | "driving" | "standby" | "resting" | "maintenance"; // 상태 필터 + sortBy: "name" | "vehicleNumber" | "status" | "departureTime"; // 정렬 기준 + sortOrder: "asc" | "desc"; // 정렬 순서 +} + +// 기사 정보 +export interface DriverInfo { + id: string; // 기사 고유 ID + name: string; // 기사 이름 + vehicleNumber: string; // 차량 번호 + vehicleType: string; // 차량 유형 + phone: string; // 연락처 + status: "standby" | "driving" | "resting" | "maintenance"; // 운행 상태 + departure?: string; // 출발지 + destination?: string; // 목적지 + departureTime?: string; // 출발 시간 + estimatedArrival?: string; // 예상 도착 시간 + progress?: number; // 운행 진행률 (0-100) +}