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

424 lines
14 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";
import { ElementConfigModal } from "./ElementConfigModal";
2025-10-15 11:17:09 +09:00
import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal";
import { DashboardElement, ElementType, ElementSubtype } from "./types";
import { GRID_CONFIG } from "./gridUtils";
2025-10-16 09:55:14 +09:00
import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector";
interface DashboardDesignerProps {
dashboardId?: string;
}
/**
*
* - /
* - (12 )
* - , ,
* - /
*/
export default function DashboardDesigner({ dashboardId: initialDashboardId }: DashboardDesignerProps = {}) {
2025-10-14 17:25:07 +09:00
const router = useRouter();
const [elements, setElements] = useState<DashboardElement[]>([]);
const [selectedElement, setSelectedElement] = useState<string | null>(null);
const [elementCounter, setElementCounter] = useState(0);
const [configModalElement, setConfigModalElement] = useState<DashboardElement | null>(null);
const [dashboardId, setDashboardId] = useState<string | null>(initialDashboardId || null);
const [dashboardTitle, setDashboardTitle] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [canvasBackgroundColor, setCanvasBackgroundColor] = useState<string>("#f9fafb");
const canvasRef = useRef<HTMLDivElement>(null);
2025-10-16 09:55:14 +09:00
// 화면 해상도 자동 감지 및 기본 해상도 설정
const [screenResolution] = useState<Resolution>(() => detectScreenResolution());
const [resolution, setResolution] = useState<Resolution>(screenResolution);
// 현재 해상도 설정
const canvasConfig = RESOLUTIONS[resolution];
// 대시보드 ID가 props로 전달되면 로드
React.useEffect(() => {
if (initialDashboardId) {
loadDashboard(initialDashboardId);
}
}, [initialDashboardId]);
// 대시보드 데이터 로드
const loadDashboard = async (id: string) => {
setIsLoading(true);
try {
// console.log('🔄 대시보드 로딩:', id);
const { dashboardApi } = await import("@/lib/api/dashboard");
const dashboard = await dashboardApi.getDashboard(id);
// console.log('✅ 대시보드 로딩 완료:', dashboard);
// 대시보드 정보 설정
setDashboardId(dashboard.id);
setDashboardTitle(dashboard.title);
// 요소들 설정
if (dashboard.elements && dashboard.elements.length > 0) {
setElements(dashboard.elements);
// 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) {
// console.error('❌ 대시보드 로딩 오류:', 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)) {
console.error("Invalid coordinates:", { x, y });
return;
}
2025-10-14 10:48:17 +09:00
// 기본 크기 설정
let defaultCells = { width: 2, height: 2 }; // 기본 위젯 크기
if (type === "chart") {
defaultCells = { width: 4, height: 3 }; // 차트
} else if (type === "widget" && subtype === "calendar") {
defaultCells = { width: 2, height: 3 }; // 달력 최소 크기
}
2025-10-16 09:55:14 +09:00
// 현재 해상도에 맞는 셀 크기 계산
const cellSize = Math.floor((canvasConfig.width + GRID_CONFIG.GAP) / GRID_CONFIG.COLUMNS) - GRID_CONFIG.GAP;
const cellWithGap = cellSize + GRID_CONFIG.GAP;
const defaultWidth = defaultCells.width * cellWithGap - GRID_CONFIG.GAP;
const defaultHeight = defaultCells.height * cellWithGap - GRID_CONFIG.GAP;
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);
},
2025-10-16 09:55:14 +09:00
[elementCounter, canvasConfig.width],
);
// 메뉴에서 요소 추가 시 (캔버스 중앙에 배치)
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)) {
console.error("Invalid canvas config:", canvasConfig);
return;
}
createElement(type, subtype, centerX, centerY);
},
[canvasConfig.width, canvasConfig.height, createElement],
);
// 요소 업데이트
const updateElement = useCallback((id: string, updates: Partial<DashboardElement>) => {
setElements((prev) => prev.map((el) => (el.id === id ? { ...el, ...updates } : el)));
}, []);
// 요소 삭제
const removeElement = useCallback(
(id: string) => {
setElements((prev) => prev.filter((el) => el.id !== id));
if (selectedElement === id) {
setSelectedElement(null);
}
},
[selectedElement],
);
// 전체 삭제
const clearCanvas = useCallback(() => {
if (window.confirm("모든 요소를 삭제하시겠습니까?")) {
setElements([]);
setSelectedElement(null);
setElementCounter(0);
}
}, []);
// 요소 설정 모달 열기
const openConfigModal = useCallback((element: DashboardElement) => {
setConfigModalElement(element);
}, []);
// 요소 설정 모달 닫기
const closeConfigModal = useCallback(() => {
setConfigModalElement(null);
}, []);
// 요소 설정 저장
const saveElementConfig = useCallback(
(updatedElement: DashboardElement) => {
updateElement(updatedElement.id, updatedElement);
},
[updateElement],
);
2025-10-15 11:17:09 +09:00
// 리스트 위젯 설정 저장 (Partial 업데이트)
const saveListWidgetConfig = useCallback(
(updates: Partial<DashboardElement>) => {
if (configModalElement) {
updateElement(configModalElement.id, updates);
}
},
[configModalElement, updateElement],
);
// 레이아웃 저장
const saveLayout = useCallback(async () => {
if (elements.length === 0) {
alert("저장할 요소가 없습니다. 차트나 위젯을 추가해주세요.");
return;
}
try {
// 실제 API 호출
const { dashboardApi } = await import("@/lib/api/dashboard");
const elementsData = elements.map((el) => ({
id: el.id,
type: el.type,
subtype: el.subtype,
position: el.position,
size: el.size,
title: el.title,
content: el.content,
dataSource: el.dataSource,
chartConfig: el.chartConfig,
}));
let savedDashboard;
if (dashboardId) {
// 기존 대시보드 업데이트
savedDashboard = await dashboardApi.updateDashboard(dashboardId, {
elements: elementsData,
});
alert(`대시보드 "${savedDashboard.title}"이 업데이트되었습니다!`);
2025-10-14 17:25:07 +09:00
// Next.js 라우터로 뷰어 페이지 이동
router.push(`/dashboard/${savedDashboard.id}`);
} else {
// 새 대시보드 생성
const title = prompt("대시보드 제목을 입력하세요:", "새 대시보드");
if (!title) return;
const description = prompt("대시보드 설명을 입력하세요 (선택사항):", "");
const dashboardData = {
title,
description: description || undefined,
isPublic: false,
elements: elementsData,
};
savedDashboard = await dashboardApi.createDashboard(dashboardData);
const viewDashboard = confirm(`대시보드 "${title}"이 저장되었습니다!\n\n지금 확인해보시겠습니까?`);
if (viewDashboard) {
2025-10-14 17:25:07 +09:00
// Next.js 라우터로 뷰어 페이지 이동
router.push(`/dashboard/${savedDashboard.id}`);
}
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류";
alert(`대시보드 저장 중 오류가 발생했습니다.\n\n오류: ${errorMessage}\n\n관리자에게 문의하세요.`);
}
2025-10-14 17:25:07 +09:00
}, [elements, dashboardId, router]);
// 로딩 중이면 로딩 화면 표시
if (isLoading) {
return (
<div className="flex h-full items-center justify-center bg-gray-50">
<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" />
<div className="text-lg font-medium text-gray-700"> ...</div>
<div className="mt-1 text-sm text-gray-500"> </div>
</div>
</div>
);
}
return (
2025-10-16 09:55:14 +09:00
<div className="flex h-full flex-col bg-gray-50">
{/* 상단 메뉴바 */}
<DashboardTopMenu
onSaveLayout={saveLayout}
onClearCanvas={clearCanvas}
onViewDashboard={dashboardId ? () => router.push(`/dashboard/${dashboardId}`) : undefined}
dashboardTitle={dashboardTitle}
onAddElement={addElementFromMenu}
resolution={resolution}
onResolutionChange={setResolution}
currentScreenResolution={screenResolution}
backgroundColor={canvasBackgroundColor}
onBackgroundColorChange={setCanvasBackgroundColor}
/>
{/* 캔버스 영역 - 해상도에 따른 크기, 중앙 정렬 */}
<div className="flex flex-1 items-center justify-center overflow-hidden bg-gray-100">
<div
className="relative shadow-2xl"
style={{
width: `${canvasConfig.width}px`,
height: `${canvasConfig.height}px`,
}}
>
2025-10-13 18:09:20 +09:00
<DashboardCanvas
ref={canvasRef}
elements={elements}
selectedElement={selectedElement}
onCreateElement={createElement}
onUpdateElement={updateElement}
onRemoveElement={removeElement}
onSelectElement={setSelectedElement}
onConfigureElement={openConfigModal}
backgroundColor={canvasBackgroundColor}
2025-10-16 09:55:14 +09:00
canvasWidth={canvasConfig.width}
canvasHeight={canvasConfig.height}
2025-10-13 18:09:20 +09:00
/>
</div>
</div>
{/* 요소 설정 모달 */}
{configModalElement && (
2025-10-15 11:17:09 +09:00
<>
{configModalElement.type === "widget" && configModalElement.subtype === "list" ? (
<ListWidgetConfigModal
element={configModalElement}
isOpen={true}
onClose={closeConfigModal}
onSave={saveListWidgetConfig}
/>
) : (
<ElementConfigModal
element={configModalElement}
isOpen={true}
onClose={closeConfigModal}
onSave={saveElementConfig}
/>
)}
</>
)}
</div>
);
}
// 요소 제목 생성 헬퍼 함수
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 "pie":
return "🥧 원형 차트";
case "line":
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-15 11:17:09 +09:00
case "list":
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-15 11:17:09 +09:00
case "list":
return "list-widget";
default:
return "위젯 내용이 여기에 표시됩니다";
}
}
return "내용이 여기에 표시됩니다";
}