ERP-node/frontend/components/admin/dashboard/DashboardDesigner.tsx

809 lines
27 KiB
TypeScript
Raw Normal View History

"use client";
import React, { useState, useRef, useCallback } from "react";
2025-10-14 17:25:07 +09:00
import { useRouter } from "next/navigation";
import { DashboardCanvas } from "./DashboardCanvas";
2025-10-16 09:55:14 +09:00
import { DashboardTopMenu } from "./DashboardTopMenu";
2025-10-31 11:02:06 +09:00
import { WidgetConfigSidebar } from "./WidgetConfigSidebar";
2025-10-16 16:43:04 +09:00
import { DashboardSaveModal } from "./DashboardSaveModal";
import { DashboardElement, ElementType, ElementSubtype } from "./types";
2025-10-30 18:05:45 +09:00
import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize, calculateBoxSize } from "./gridUtils";
2025-10-16 09:55:14 +09:00
import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector";
import { DashboardProvider } from "@/contexts/DashboardContext";
2025-10-16 16:43:04 +09:00
import { useMenu } from "@/contexts/MenuContext";
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
2025-11-11 17:35:24 +09:00
import {
ResizableDialog,
ResizableDialogContent,
ResizableDialogDescription,
ResizableDialogHeader,
ResizableDialogTitle,
} from "@/components/ui/resizable-dialog";
2025-10-16 16:51:24 +09:00
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
2025-11-11 17:35:24 +09:00
AlertDialogContent,
AlertDialogDescription,
2025-10-16 16:51:24 +09:00
AlertDialogFooter,
2025-11-11 17:35:24 +09:00
AlertDialogHeader,
2025-10-16 16:51:24 +09:00
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
2025-10-16 16:43:04 +09:00
import { Button } from "@/components/ui/button";
import { CheckCircle2 } from "lucide-react";
interface DashboardDesignerProps {
dashboardId?: string;
}
/**
*
* - /
* - (12 )
* - , ,
* - /
*/
export default function DashboardDesigner({ dashboardId: initialDashboardId }: DashboardDesignerProps = {}) {
2025-10-14 17:25:07 +09:00
const router = useRouter();
2025-10-16 16:43:04 +09:00
const { refreshMenus } = useMenu();
const [elements, setElements] = useState<DashboardElement[]>([]);
const [selectedElement, setSelectedElement] = useState<string | null>(null);
2025-10-22 10:45:10 +09:00
const [selectedElements, setSelectedElements] = useState<string[]>([]); // 다중 선택
const [elementCounter, setElementCounter] = useState(0);
const [dashboardId, setDashboardId] = useState<string | null>(initialDashboardId || null);
const [dashboardTitle, setDashboardTitle] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [canvasBackgroundColor, setCanvasBackgroundColor] = useState<string>("transparent");
const canvasRef = useRef<HTMLDivElement>(null);
2025-10-16 16:43:04 +09:00
// 저장 모달 상태
const [saveModalOpen, setSaveModalOpen] = useState(false);
const [dashboardDescription, setDashboardDescription] = useState<string>("");
const [successModalOpen, setSuccessModalOpen] = useState(false);
2025-10-16 16:51:24 +09:00
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
2025-10-16 14:53:06 +09:00
2025-10-22 10:45:10 +09:00
// 사이드바 상태
const [sidebarOpen, setSidebarOpen] = useState(false);
const [sidebarElement, setSidebarElement] = useState<DashboardElement | null>(null);
// 클립보드 (복사/붙여넣기용)
const [clipboard, setClipboard] = useState<DashboardElement | null>(null);
2025-10-16 14:53:06 +09:00
2025-10-16 10:27:43 +09:00
// 화면 해상도 자동 감지
2025-10-16 09:55:14 +09:00
const [screenResolution] = useState<Resolution>(() => detectScreenResolution());
const [resolution, setResolution] = useState<Resolution>(() => {
// 새 대시보드인 경우 (dashboardId 없음) 화면 해상도 감지값 사용
// 기존 대시보드 편집인 경우 FHD로 시작 (로드 시 덮어씀)
return initialDashboardId ? "fhd" : detectScreenResolution();
});
2025-10-16 09:55:14 +09:00
// resolution 변경 감지 및 요소 자동 조정
const handleResolutionChange = useCallback(
(newResolution: Resolution) => {
setResolution((prev) => {
// 이전 해상도와 새 해상도의 캔버스 너비 비율 계산
const oldConfig = RESOLUTIONS[prev];
const newConfig = RESOLUTIONS[newResolution];
const widthRatio = newConfig.width / oldConfig.width;
// 요소들의 위치와 크기를 비율에 맞춰 조정
if (widthRatio !== 1 && elements.length > 0) {
// 새 해상도의 셀 크기 계산
const newCellSize = calculateCellSize(newConfig.width);
const adjustedElements = elements.map((el) => {
// 비율에 맞춰 조정 (X와 너비만)
const scaledX = el.position.x * widthRatio;
const scaledWidth = el.size.width * widthRatio;
// 그리드에 스냅 (X, Y, 너비, 높이 모두)
const snappedX = snapToGrid(scaledX, newCellSize);
const snappedY = snapToGrid(el.position.y, newCellSize);
const snappedWidth = snapSizeToGrid(scaledWidth, newConfig.width);
const snappedHeight = snapSizeToGrid(el.size.height, newConfig.width);
return {
...el,
position: {
x: snappedX,
y: snappedY,
},
size: {
width: snappedWidth,
height: snappedHeight,
},
};
});
setElements(adjustedElements);
}
return newResolution;
});
},
[elements],
);
2025-10-16 11:29:45 +09:00
2025-10-16 10:05:43 +09:00
// 현재 해상도 설정 (안전하게 기본값 제공)
const canvasConfig = RESOLUTIONS[resolution] || RESOLUTIONS.fhd;
2025-10-16 09:55:14 +09:00
2025-10-16 11:09:11 +09:00
// 캔버스 높이 동적 계산 (요소들의 최하단 위치 기준)
const calculateCanvasHeight = useCallback(() => {
if (elements.length === 0) {
return canvasConfig.height; // 기본 높이
}
// 모든 요소의 최하단 y 좌표 계산
const maxBottomY = Math.max(...elements.map((el) => el.position.y + el.size.height));
// 최소 높이는 기본 높이, 요소가 아래로 내려가면 자동으로 늘어남
// 패딩 추가 (100px 여유)
return Math.max(canvasConfig.height, maxBottomY + 100);
}, [elements, canvasConfig.height]);
const dynamicCanvasHeight = calculateCanvasHeight();
// 대시보드 ID가 props로 전달되면 로드
React.useEffect(() => {
if (initialDashboardId) {
loadDashboard(initialDashboardId);
}
}, [initialDashboardId]);
// 대시보드 데이터 로드
const loadDashboard = async (id: string) => {
setIsLoading(true);
try {
const { dashboardApi } = await import("@/lib/api/dashboard");
const dashboard = await dashboardApi.getDashboard(id);
2025-11-21 10:29:47 +09:00
console.log("🔍 [loadDashboard] 대시보드 응답:", {
id: dashboard.id,
title: dashboard.title,
settingsType: typeof (dashboard as any).settings,
settingsValue: (dashboard as any).settings,
});
// 대시보드 정보 설정
setDashboardId(dashboard.id);
setDashboardTitle(dashboard.title);
2025-10-16 10:27:43 +09:00
// 저장된 설정 복원
2025-10-16 16:51:24 +09:00
const settings = (dashboard as { settings?: { resolution?: Resolution; backgroundColor?: string } }).settings;
2025-11-21 10:29:47 +09:00
console.log("🔍 [loadDashboard] 파싱된 settings:", {
settings,
resolution: settings?.resolution,
backgroundColor: settings?.backgroundColor,
});
2025-10-23 09:52:14 +09:00
// 배경색 설정
2025-10-16 16:51:24 +09:00
if (settings?.backgroundColor) {
setCanvasBackgroundColor(settings.backgroundColor);
2025-10-16 10:27:43 +09:00
}
2025-10-23 09:52:14 +09:00
// 해상도와 요소를 함께 설정 (해상도가 먼저 반영되어야 함)
const loadedResolution = settings?.resolution || "fhd";
2025-11-21 10:29:47 +09:00
console.log("🔍 [loadDashboard] 로드할 resolution:", loadedResolution);
2025-10-23 09:52:14 +09:00
setResolution(loadedResolution);
// 요소들 설정
if (dashboard.elements && dashboard.elements.length > 0) {
// chartConfig.dataSources를 element.dataSources로 복사 (프론트엔드 호환성)
const elementsWithDataSources = dashboard.elements.map((el) => ({
...el,
dataSources: el.chartConfig?.dataSources || el.dataSources,
}));
2025-10-30 18:05:45 +09:00
setElements(elementsWithDataSources);
// elementCounter를 가장 큰 ID 번호로 설정
const maxId = dashboard.elements.reduce((max, el) => {
const match = el.id.match(/element-(\d+)/);
if (match) {
const num = parseInt(match[1]);
return num > max ? num : max;
}
return max;
}, 0);
setElementCounter(maxId);
}
} catch (error) {
alert(
"대시보드를 불러오는 중 오류가 발생했습니다.\n\n" +
(error instanceof Error ? error.message : "알 수 없는 오류"),
);
} finally {
setIsLoading(false);
}
};
2025-10-16 09:55:14 +09:00
// 새로운 요소 생성 (동적 그리드 기반 기본 크기)
const createElement = useCallback(
(type: ElementType, subtype: ElementSubtype, x: number, y: number) => {
2025-10-16 09:55:14 +09:00
// 좌표 유효성 검사
if (isNaN(x) || isNaN(y)) {
return;
}
// 기본 크기 설정 (그리드 박스 단위)
const boxSize = calculateBoxSize(canvasConfig.width);
// 그리드 박스 단위 기본 크기
let boxesWidth = 3; // 기본 위젯: 박스 3개
let boxesHeight = 3; // 기본 위젯: 박스 3개
2025-10-14 10:48:17 +09:00
if (type === "chart") {
boxesWidth = 4; // 차트: 박스 4개
boxesHeight = 3; // 차트: 박스 3개
2025-10-14 10:48:17 +09:00
} else if (type === "widget" && subtype === "calendar") {
boxesWidth = 3; // 달력: 박스 3개
boxesHeight = 4; // 달력: 박스 4개
2025-10-14 10:48:17 +09:00
}
// 박스 개수를 픽셀로 변환 (마지막 간격 제거)
const defaultWidth = boxesWidth * boxSize + (boxesWidth - 1) * GRID_CONFIG.GRID_BOX_GAP;
const defaultHeight = boxesHeight * boxSize + (boxesHeight - 1) * GRID_CONFIG.GRID_BOX_GAP;
2025-10-16 10:05:43 +09:00
// 크기 유효성 검사
if (isNaN(defaultWidth) || isNaN(defaultHeight) || defaultWidth <= 0 || defaultHeight <= 0) {
return;
}
const newElement: DashboardElement = {
id: `element-${elementCounter + 1}`,
type,
subtype,
position: { x, y },
size: { width: defaultWidth, height: defaultHeight },
title: getElementTitle(type, subtype),
content: getElementContent(type, subtype),
};
setElements((prev) => [...prev, newElement]);
setElementCounter((prev) => prev + 1);
setSelectedElement(newElement.id);
// 새 요소 생성 시 자동으로 설정 사이드바 열기
setSidebarElement(newElement);
setSidebarOpen(true);
},
2025-10-16 16:51:24 +09:00
[elementCounter, canvasConfig],
2025-10-16 09:55:14 +09:00
);
// 메뉴에서 요소 추가 시 (캔버스 중앙에 배치)
const addElementFromMenu = useCallback(
(type: ElementType, subtype: ElementSubtype) => {
// 캔버스 중앙 좌표 계산
const centerX = Math.floor(canvasConfig.width / 2);
const centerY = Math.floor(canvasConfig.height / 2);
// 좌표 유효성 확인
if (isNaN(centerX) || isNaN(centerY)) {
return;
}
createElement(type, subtype, centerX, centerY);
},
2025-10-16 16:51:24 +09:00
[canvasConfig, createElement],
);
// 요소 업데이트
const updateElement = useCallback((id: string, updates: Partial<DashboardElement>) => {
setElements((prev) =>
prev.map((el) => {
if (el.id === id) {
return { ...el, ...updates };
}
return el;
}),
);
}, []);
// 요소 삭제
const removeElement = useCallback(
(id: string) => {
setElements((prev) => prev.filter((el) => el.id !== id));
if (selectedElement === id) {
setSelectedElement(null);
}
2025-10-22 13:40:15 +09:00
// 삭제된 요소의 사이드바가 열려있으면 닫기
if (sidebarElement?.id === id) {
setSidebarOpen(false);
setSidebarElement(null);
}
},
2025-10-22 13:40:15 +09:00
[selectedElement, sidebarElement],
);
// 키보드 단축키 핸들러들
const handleCopyElement = useCallback(() => {
if (!selectedElement) return;
const element = elements.find((el) => el.id === selectedElement);
if (element) {
setClipboard(element);
}
}, [selectedElement, elements]);
const handlePasteElement = useCallback(() => {
if (!clipboard) return;
// 새 ID 생성
const newId = `element-${elementCounter + 1}`;
setElementCounter((prev) => prev + 1);
// 위치를 약간 오프셋 (오른쪽 아래로 20px씩)
const newElement: DashboardElement = {
...clipboard,
id: newId,
position: {
x: clipboard.position.x + 20,
y: clipboard.position.y + 20,
},
};
setElements((prev) => [...prev, newElement]);
setSelectedElement(newId);
}, [clipboard, elementCounter]);
const handleDeleteSelected = useCallback(() => {
if (selectedElement) {
removeElement(selectedElement);
}
}, [selectedElement, removeElement]);
// 키보드 단축키 활성화
useKeyboardShortcuts({
selectedElementId: selectedElement,
onDelete: handleDeleteSelected,
onCopy: handleCopyElement,
onPaste: handlePasteElement,
2025-10-22 10:45:10 +09:00
enabled: !saveModalOpen && !successModalOpen && !clearConfirmOpen && !sidebarOpen,
});
2025-10-16 16:51:24 +09:00
// 전체 삭제 확인 모달 열기
const clearCanvas = useCallback(() => {
2025-10-16 16:51:24 +09:00
setClearConfirmOpen(true);
}, []);
// 실제 초기화 실행
const handleClearConfirm = useCallback(() => {
setElements([]);
setSelectedElement(null);
setElementCounter(0);
setClearConfirmOpen(false);
}, []);
2025-10-22 10:45:10 +09:00
// 사이드바 닫기
const handleCloseSidebar = useCallback(() => {
setSidebarOpen(false);
setSidebarElement(null);
setSelectedElement(null);
}, []);
2025-10-22 10:45:10 +09:00
// 사이드바 적용
const handleApplySidebar = useCallback(
(updatedElement: DashboardElement) => {
// 현재 요소의 최신 상태를 가져와서 position과 size는 유지
const currentElement = elements.find((el) => el.id === updatedElement.id);
if (currentElement) {
// id, position, size 제거 후 나머지만 업데이트
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, position, size, ...updates } = updatedElement;
const finalElement = {
...currentElement,
...updates,
};
updateElement(id, updates);
// 사이드바도 최신 상태로 업데이트
setSidebarElement(finalElement);
}
},
[elements, updateElement],
);
// 레이아웃 저장
2025-10-16 16:43:04 +09:00
const saveLayout = useCallback(() => {
if (elements.length === 0) {
alert("저장할 요소가 없습니다. 차트나 위젯을 추가해주세요.");
return;
}
2025-10-16 16:43:04 +09:00
// 저장 모달 열기
setSaveModalOpen(true);
}, [elements]);
// 저장 처리
const handleSave = useCallback(
async (data: {
title: string;
description: string;
assignToMenu: boolean;
menuType?: "admin" | "user";
menuId?: string;
}) => {
try {
const { dashboardApi } = await import("@/lib/api/dashboard");
2025-10-20 11:52:23 +09:00
const elementsData = elements.map((el) => {
return {
id: el.id,
type: el.type,
subtype: el.subtype,
2025-10-23 09:52:14 +09:00
// 위치와 크기는 정수로 반올림 (DB integer 타입)
position: {
x: Math.round(el.position.x),
y: Math.round(el.position.y),
},
size: {
width: Math.round(el.size.width),
height: Math.round(el.size.height),
},
2025-10-20 11:52:23 +09:00
title: el.title,
customTitle: el.customTitle,
showHeader: el.showHeader,
content: el.content,
dataSource: el.dataSource,
// dataSources는 chartConfig에 포함시켜서 저장 (백엔드 스키마 수정 불필요)
chartConfig:
el.dataSources && el.dataSources.length > 0
? { ...el.chartConfig, dataSources: el.dataSources }
: el.chartConfig,
2025-10-20 11:52:23 +09:00
listConfig: el.listConfig,
yardConfig: el.yardConfig,
customMetricConfig: el.customMetricConfig,
2025-10-20 11:52:23 +09:00
};
});
2025-10-16 16:43:04 +09:00
let savedDashboard;
if (dashboardId) {
// 기존 대시보드 업데이트
const updateData = {
title: data.title,
description: data.description || undefined,
elements: elementsData,
settings: {
resolution,
backgroundColor: canvasBackgroundColor,
},
};
2025-11-21 10:29:47 +09:00
console.log("🔍 [handleSave] 업데이트 데이터:", {
dashboardId,
settings: updateData.settings,
});
2025-10-16 16:43:04 +09:00
savedDashboard = await dashboardApi.updateDashboard(dashboardId, updateData);
} else {
// 새 대시보드 생성
const dashboardData = {
title: data.title,
description: data.description || undefined,
isPublic: false,
elements: elementsData,
settings: {
resolution,
backgroundColor: canvasBackgroundColor,
},
};
2025-11-21 10:29:47 +09:00
console.log("🔍 [handleSave] 생성 데이터:", {
settings: dashboardData.settings,
});
2025-10-16 16:43:04 +09:00
savedDashboard = await dashboardApi.createDashboard(dashboardData);
setDashboardId(savedDashboard.id);
}
2025-10-16 14:53:06 +09:00
setDashboardTitle(savedDashboard.title);
2025-10-16 16:43:04 +09:00
setDashboardDescription(data.description);
2025-10-16 14:53:06 +09:00
2025-10-16 16:43:04 +09:00
// 메뉴 할당 처리
if (data.assignToMenu && data.menuId) {
2025-10-16 14:53:06 +09:00
const { menuApi } = await import("@/lib/api/menu");
2025-10-16 14:53:06 +09:00
// 대시보드 URL 생성 (관리자 메뉴면 mode=admin 추가)
2025-10-16 16:43:04 +09:00
let dashboardUrl = `/dashboard/${savedDashboard.id}`;
if (data.menuType === "admin") {
2025-10-16 14:53:06 +09:00
dashboardUrl += "?mode=admin";
}
// 메뉴 정보 가져오기
2025-10-16 16:43:04 +09:00
const menuResponse = await menuApi.getMenuInfo(data.menuId);
2025-10-16 14:53:06 +09:00
if (menuResponse.success && menuResponse.data) {
const menu = menuResponse.data;
2025-10-16 16:43:04 +09:00
const updateData = {
2025-10-16 14:53:06 +09:00
menuUrl: dashboardUrl,
parentObjId: menu.parent_obj_id || menu.PARENT_OBJ_ID || "0",
menuNameKor: menu.menu_name_kor || menu.MENU_NAME_KOR || "",
menuDesc: menu.menu_desc || menu.MENU_DESC || "",
seq: menu.seq || menu.SEQ || 1,
menuType: menu.menu_type || menu.MENU_TYPE || "1",
status: menu.status || menu.STATUS || "active",
companyCode: menu.company_code || menu.COMPANY_CODE || "",
langKey: menu.lang_key || menu.LANG_KEY || "",
2025-10-16 16:43:04 +09:00
};
// 메뉴 URL 업데이트
await menuApi.updateMenu(data.menuId, updateData);
2025-10-16 14:53:06 +09:00
2025-10-16 16:43:04 +09:00
// 메뉴 목록 새로고침
await refreshMenus();
2025-10-16 14:53:06 +09:00
}
}
2025-10-16 14:53:06 +09:00
2025-10-16 16:43:04 +09:00
// 성공 모달 표시
setSuccessModalOpen(true);
2025-10-16 14:53:06 +09:00
} catch (error) {
2025-10-16 16:43:04 +09:00
const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류";
alert(`대시보드 저장 중 오류가 발생했습니다.\n\n오류: ${errorMessage}`);
throw error;
}
2025-10-16 14:53:06 +09:00
},
2025-10-16 16:51:24 +09:00
[elements, dashboardId, resolution, canvasBackgroundColor, refreshMenus],
2025-10-16 14:53:06 +09:00
);
// 로딩 중이면 로딩 화면 표시
if (isLoading) {
return (
2025-10-30 18:05:45 +09:00
<div className="bg-muted flex h-full items-center justify-center">
<div className="text-center">
<div className="border-primary mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-4 border-t-transparent" />
2025-10-30 18:05:45 +09:00
<div className="text-foreground text-lg font-medium"> ...</div>
<div className="text-muted-foreground mt-1 text-sm"> </div>
</div>
</div>
);
}
return (
<DashboardProvider>
2025-10-30 18:05:45 +09:00
<div className="bg-muted flex h-full flex-col">
{/* 상단 메뉴바 */}
<DashboardTopMenu
onSaveLayout={saveLayout}
onClearCanvas={clearCanvas}
dashboardTitle={dashboardTitle}
onAddElement={addElementFromMenu}
resolution={resolution}
onResolutionChange={handleResolutionChange}
currentScreenResolution={screenResolution}
backgroundColor={canvasBackgroundColor}
onBackgroundColorChange={setCanvasBackgroundColor}
/>
2025-10-16 09:55:14 +09:00
2025-10-17 13:44:51 +09:00
{/* 캔버스 영역 - 해상도에 따른 크기, 중앙 정렬 */}
{/* overflow-auto 제거 - 외부 페이지 스크롤 사용 */}
2025-10-30 18:05:45 +09:00
<div className="dashboard-canvas-container bg-muted flex flex-1 items-start justify-center p-8">
2025-10-17 13:44:51 +09:00
<div
className="relative"
2025-10-17 13:44:51 +09:00
style={{
width: `${canvasConfig.width}px`,
minHeight: `${canvasConfig.height}px`,
}}
>
<DashboardCanvas
ref={canvasRef}
elements={elements}
selectedElement={selectedElement}
2025-10-22 10:10:21 +09:00
selectedElements={selectedElements}
2025-10-17 13:44:51 +09:00
onCreateElement={createElement}
onUpdateElement={updateElement}
onRemoveElement={removeElement}
2025-10-22 10:10:21 +09:00
onSelectElement={(id) => {
setSelectedElement(id);
2025-10-22 10:45:10 +09:00
setSelectedElements([]);
// 선택된 요소 찾아서 사이드바 열기
const element = elements.find((el) => el.id === id);
if (element) {
2025-10-22 13:40:15 +09:00
setSidebarElement(element);
setSidebarOpen(true);
2025-10-22 10:45:10 +09:00
}
2025-10-22 10:10:21 +09:00
}}
onSelectMultiple={(ids) => {
setSelectedElements(ids);
2025-10-22 10:45:10 +09:00
setSelectedElement(null);
setSidebarOpen(false);
setSidebarElement(null);
2025-10-22 10:10:21 +09:00
}}
2025-10-22 10:45:10 +09:00
onConfigureElement={() => {}}
2025-10-17 13:44:51 +09:00
backgroundColor={canvasBackgroundColor}
canvasWidth={canvasConfig.width}
canvasHeight={dynamicCanvasHeight}
/>
</div>
2025-10-13 18:09:20 +09:00
</div>
2025-10-31 11:02:06 +09:00
{/* 요소 설정 사이드바 (통합) */}
<WidgetConfigSidebar
2025-10-22 10:45:10 +09:00
element={sidebarElement}
isOpen={sidebarOpen}
onClose={handleCloseSidebar}
onApply={handleApplySidebar}
/>
2025-10-17 13:44:51 +09:00
{/* 저장 모달 */}
<DashboardSaveModal
isOpen={saveModalOpen}
onClose={() => setSaveModalOpen(false)}
onSave={handleSave}
initialTitle={dashboardTitle}
initialDescription={dashboardDescription}
isEditing={!!dashboardId}
/>
{/* 저장 성공 모달 */}
2025-11-11 17:35:24 +09:00
<ResizableDialog
2025-10-17 13:44:51 +09:00
open={successModalOpen}
onOpenChange={() => {
setSuccessModalOpen(false);
router.push("/admin/dashboard");
}}
>
2025-11-11 17:35:24 +09:00
<ResizableDialogContent className="sm:max-w-md">
<ResizableDialogHeader>
2025-10-30 18:05:45 +09:00
<div className="bg-success/10 mx-auto flex h-12 w-12 items-center justify-center rounded-full">
<CheckCircle2 className="text-success h-6 w-6" />
2025-10-17 13:44:51 +09:00
</div>
2025-11-11 17:35:24 +09:00
<ResizableDialogTitle className="text-center"> </ResizableDialogTitle>
<ResizableDialogDescription className="text-center">
.
</ResizableDialogDescription>
</ResizableDialogHeader>
2025-10-17 13:44:51 +09:00
<div className="flex justify-center pt-4">
<Button
onClick={() => {
setSuccessModalOpen(false);
router.push("/admin/dashboard");
}}
>
</Button>
2025-10-16 16:43:04 +09:00
</div>
2025-11-11 17:35:24 +09:00
</ResizableDialogContent>
</ResizableDialog>
2025-10-17 13:44:51 +09:00
{/* 초기화 확인 모달 */}
<AlertDialog open={clearConfirmOpen} onOpenChange={setClearConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
. .
<br />
?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
2025-10-29 17:53:03 +09:00
<AlertDialogAction onClick={handleClearConfirm} className="bg-destructive hover:bg-destructive/90">
2025-10-17 13:44:51 +09:00
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</DashboardProvider>
);
}
// 요소 제목 생성 헬퍼 함수
function getElementTitle(type: ElementType, subtype: ElementSubtype): string {
if (type === "chart") {
switch (subtype) {
case "bar":
return "바 차트";
2025-10-14 16:49:57 +09:00
case "horizontal-bar":
return "수평 바 차트";
case "stacked-bar":
return "누적 바 차트";
case "pie":
return "원형 차트";
case "donut":
return "도넛 차트";
case "line":
return "꺾은선 차트";
case "area":
return "영역 차트";
case "combo":
return "콤보 차트";
default:
return "차트";
}
} else if (type === "widget") {
switch (subtype) {
case "exchange":
return "환율 위젯";
case "weather":
return "날씨 위젯";
case "clock":
return "시계 위젯";
case "calculator":
return "계산기 위젯";
case "vehicle-map":
return "차량 위치 지도";
2025-10-14 10:48:17 +09:00
case "calendar":
return "달력 위젯";
2025-10-14 11:26:53 +09:00
case "driver-management":
return "기사 관리 위젯";
2025-10-30 18:10:52 +09:00
case "list-v2":
return "리스트 위젯";
2025-10-30 18:10:52 +09:00
case "map-summary-v2":
return "커스텀 지도 카드";
case "status-summary":
return "커스텀 상태 카드";
2025-10-30 18:10:52 +09:00
case "risk-alert-v2":
return "리스크 알림 위젯";
case "todo":
return "할 일 위젯";
case "booking-alert":
return "예약 알림 위젯";
case "maintenance":
return "정비 일정 위젯";
case "document":
return "문서 위젯";
case "yard-management-3d":
return "야드 관리 3D";
case "work-history":
return "작업 이력";
case "transport-stats":
return "커스텀 통계 카드";
default:
return "위젯";
}
}
return "요소";
}
// 요소 내용 생성 헬퍼 함수
function getElementContent(type: ElementType, subtype: ElementSubtype): string {
if (type === "chart") {
switch (subtype) {
case "bar":
return "바 차트가 여기에 표시됩니다";
2025-10-14 16:49:57 +09:00
case "horizontal-bar":
return "수평 바 차트가 여기에 표시됩니다";
case "pie":
return "원형 차트가 여기에 표시됩니다";
case "line":
return "꺾은선 차트가 여기에 표시됩니다";
default:
return "차트가 여기에 표시됩니다";
}
} else if (type === "widget") {
switch (subtype) {
case "exchange":
return "USD: ₩1,320\nJPY: ₩900\nEUR: ₩1,450";
case "weather":
return "서울\n23°C\n구름 많음";
case "clock":
return "clock";
case "calculator":
return "calculator";
case "vehicle-map":
return "vehicle-map";
2025-10-14 10:48:17 +09:00
case "calendar":
return "calendar";
2025-10-14 11:26:53 +09:00
case "driver-management":
return "driver-management";
2025-10-30 18:10:52 +09:00
case "list-v2":
2025-10-15 11:17:09 +09:00
return "list-widget";
case "yard-management-3d":
return "yard-3d";
case "work-history":
return "work-history";
case "transport-stats":
return "커스텀 통계 카드";
default:
return "위젯 내용이 여기에 표시됩니다";
}
}
return "내용이 여기에 표시됩니다";
}