모든 위젯 복구 (캘린더, 운전자 관리 포함) - main과 동일하게 유지

This commit is contained in:
leeheejin 2025-10-14 16:45:16 +09:00
parent c6930a4e66
commit 6a3ee2f132
3 changed files with 144 additions and 103 deletions

View File

@ -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({
<div className="flex cursor-move items-center justify-between border-b border-gray-200 bg-gray-50 p-3">
<span className="text-sm font-bold text-gray-800">{element.title}</span>
<div className="flex gap-1">
{/* 설정 버튼 (시계 위젯은 자체 설정 UI 사용) */}
{onConfigure && !(element.type === "widget" && element.subtype === "clock") && (
<button
className="hover:bg-accent0 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"
onClick={() => onConfigure(element)}
title="설정"
>
</button>
)}
{/* 설정 버튼 (시계, 달력, 기사관리 위젯은 자체 설정 UI 사용) */}
{onConfigure &&
!(
element.type === "widget" &&
(element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "driver-management")
) && (
<button
className="hover:bg-accent0 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"
onClick={() => onConfigure(element)}
title="설정"
>
</button>
)}
{/* 삭제 버튼 */}
<button
className="element-close hover:bg-destructive/100 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"
@ -366,7 +397,7 @@ export function CanvasElement({
) : (
<ChartRenderer
element={element}
data={chartData}
data={chartData || undefined}
width={element.size.width}
height={element.size.height - 45}
/>
@ -375,16 +406,12 @@ export function CanvasElement({
) : element.type === "widget" && element.subtype === "weather" ? (
// 날씨 위젯 렌더링
<div className="widget-interactive-area h-full w-full">
<WeatherWidget city={element.config?.city || "서울"} refreshInterval={600000} />
<WeatherWidget city="서울" refreshInterval={600000} />
</div>
) : element.type === "widget" && element.subtype === "exchange" ? (
// 환율 위젯 렌더링
<div className="widget-interactive-area h-full w-full">
<ExchangeWidget
baseCurrency={element.config?.baseCurrency || "KRW"}
targetCurrency={element.config?.targetCurrency || "USD"}
refreshInterval={600000}
/>
<ExchangeWidget baseCurrency="KRW" targetCurrency="USD" refreshInterval={600000} />
</div>
) : element.type === "widget" && element.subtype === "clock" ? (
// 시계 위젯 렌더링
@ -416,6 +443,26 @@ export function CanvasElement({
<div className="widget-interactive-area h-full w-full">
<RiskAlertWidget />
</div>
) : element.type === "widget" && element.subtype === "calendar" ? (
// 달력 위젯 렌더링
<div className="h-full w-full">
<CalendarWidget
element={element}
onConfigUpdate={(newConfig) => {
onUpdate(element.id, { calendarConfig: newConfig });
}}
/>
</div>
) : element.type === "widget" && element.subtype === "driver-management" ? (
// 기사 관리 위젯 렌더링
<div className="h-full w-full">
<DriverManagementWidget
element={element}
onConfigUpdate={(newConfig) => {
onUpdate(element.id, { driverManagementConfig: newConfig });
}}
/>
</div>
) : (
// 기타 위젯 렌더링
<div
@ -471,68 +518,3 @@ function ResizeHandle({ position, onMouseDown }: ResizeHandleProps) {
/>
);
}
/**
* ( API )
*/
function generateSampleData(query: string, chartType: string): QueryResult {
// 쿼리에서 키워드 추출하여 적절한 샘플 데이터 생성
const isMonthly = query.toLowerCase().includes("month");
const isSales = query.toLowerCase().includes("sales") || query.toLowerCase().includes("매출");
const isUsers = query.toLowerCase().includes("users") || query.toLowerCase().includes("사용자");
const isProducts = query.toLowerCase().includes("product") || query.toLowerCase().includes("상품");
let columns: string[];
let rows: Record<string, any>[];
if (isMonthly && isSales) {
// 월별 매출 데이터
columns = ["month", "sales", "order_count"];
rows = [
{ month: "2024-01", sales: 1200000, order_count: 45 },
{ month: "2024-02", sales: 1350000, order_count: 52 },
{ month: "2024-03", sales: 1180000, order_count: 41 },
{ month: "2024-04", sales: 1420000, order_count: 58 },
{ month: "2024-05", sales: 1680000, order_count: 67 },
{ month: "2024-06", sales: 1540000, order_count: 61 },
];
} else if (isUsers) {
// 사용자 가입 추이
columns = ["week", "new_users"];
rows = [
{ week: "2024-W10", new_users: 23 },
{ week: "2024-W11", new_users: 31 },
{ week: "2024-W12", new_users: 28 },
{ week: "2024-W13", new_users: 35 },
{ week: "2024-W14", new_users: 42 },
{ week: "2024-W15", new_users: 38 },
];
} else if (isProducts) {
// 상품별 판매량
columns = ["product_name", "total_sold", "revenue"];
rows = [
{ product_name: "스마트폰", total_sold: 156, revenue: 234000000 },
{ product_name: "노트북", total_sold: 89, revenue: 178000000 },
{ product_name: "태블릿", total_sold: 134, revenue: 67000000 },
{ product_name: "이어폰", total_sold: 267, revenue: 26700000 },
{ product_name: "스마트워치", total_sold: 98, revenue: 49000000 },
];
} else {
// 기본 샘플 데이터
columns = ["category", "value", "count"];
rows = [
{ category: "A", value: 100, count: 10 },
{ category: "B", value: 150, count: 15 },
{ category: "C", value: 120, count: 12 },
{ category: "D", value: 180, count: 18 },
{ category: "E", value: 90, count: 9 },
];
}
return {
columns,
rows,
totalRows: rows.length,
executionTime: Math.floor(Math.random() * 100) + 50, // 50-150ms
};
}

View File

@ -133,15 +133,31 @@ export function DashboardSidebar() {
type="widget"
subtype="delivery-status"
onDragStart={handleDragStart}
className="border-l-4 border-blue-600"
className="border-l-4 border-amber-500"
/>
<DraggableItem
icon="🚨"
title="리스크 / 알림"
icon="⚠️"
title="리스크/알림 위젯"
type="widget"
subtype="risk-alert"
onDragStart={handleDragStart}
className="border-l-4 border-red-600"
className="border-l-4 border-rose-500"
/>
<DraggableItem
icon="📅"
title="달력 위젯"
type="widget"
subtype="calendar"
onDragStart={handleDragStart}
className="border-l-4 border-indigo-500"
/>
<DraggableItem
icon="🚗"
title="기사 관리 위젯"
type="widget"
subtype="driver-management"
onDragStart={handleDragStart}
className="border-l-4 border-blue-500"
/>
</div>
</div>

View File

@ -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)
}