모든 위젯 복구 (캘린더, 운전자 관리 포함) - main과 동일하게 유지
This commit is contained in:
parent
c6930a4e66
commit
6a3ee2f132
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue