"use client"; import React, { useState, useRef, useCallback } from "react"; import { useRouter } from "next/navigation"; import { DashboardCanvas } from "./DashboardCanvas"; import { DashboardTopMenu } from "./DashboardTopMenu"; import { ElementConfigModal } from "./ElementConfigModal"; import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal"; import { DashboardElement, ElementType, ElementSubtype } from "./types"; import { GRID_CONFIG, snapToGrid, snapSizeToGrid, calculateCellSize } from "./gridUtils"; import { Resolution, RESOLUTIONS, detectScreenResolution } from "./ResolutionSelector"; interface DashboardDesignerProps { dashboardId?: string; } /** * 대시보드 설계 도구 메인 컴포넌트 * - 드래그 앤 드롭으로 차트/위젯 배치 * - 그리드 기반 레이아웃 (12 컬럼) * - 요소 이동, 크기 조절, 삭제 기능 * - 레이아웃 저장/불러오기 기능 */ export default function DashboardDesigner({ dashboardId: initialDashboardId }: DashboardDesignerProps = {}) { const router = useRouter(); const [elements, setElements] = useState([]); const [selectedElement, setSelectedElement] = useState(null); const [elementCounter, setElementCounter] = useState(0); const [configModalElement, setConfigModalElement] = useState(null); const [dashboardId, setDashboardId] = useState(initialDashboardId || null); const [dashboardTitle, setDashboardTitle] = useState(""); const [isLoading, setIsLoading] = useState(false); const [canvasBackgroundColor, setCanvasBackgroundColor] = useState("#f9fafb"); const canvasRef = useRef(null); // 화면 해상도 자동 감지 const [screenResolution] = useState(() => detectScreenResolution()); const [resolution, setResolution] = useState(screenResolution); // resolution 변경 감지 및 요소 자동 조정 const handleResolutionChange = useCallback( (newResolution: Resolution) => { console.log("🎯 해상도 변경 요청:", newResolution); setResolution((prev) => { console.log("🎯 이전 해상도:", prev); // 이전 해상도와 새 해상도의 캔버스 너비 비율 계산 const oldConfig = RESOLUTIONS[prev]; const newConfig = RESOLUTIONS[newResolution]; const widthRatio = newConfig.width / oldConfig.width; console.log("📐 너비 비율:", widthRatio, `(${oldConfig.width}px → ${newConfig.width}px)`); // 요소들의 위치와 크기를 비율에 맞춰 조정 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, 2, newCellSize); const snappedHeight = snapSizeToGrid(el.size.height, 2, newCellSize); return { ...el, position: { x: snappedX, y: snappedY, }, size: { width: snappedWidth, height: snappedHeight, }, }; }); console.log("✨ 요소 위치/크기 자동 조정 (그리드 스냅 적용):", adjustedElements.length, "개"); setElements(adjustedElements); } return newResolution; }); }, [elements], ); // 현재 해상도 설정 (안전하게 기본값 제공) const canvasConfig = RESOLUTIONS[resolution] || RESOLUTIONS.fhd; // 캔버스 높이 동적 계산 (요소들의 최하단 위치 기준) 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 { // 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); // 저장된 설정 복원 console.log("🔍 로드된 대시보드:", dashboard); console.log("📦 저장된 settings:", (dashboard as any).settings); console.log("🎯 settings 타입:", typeof (dashboard as any).settings); console.log("🔍 resolution 값:", (dashboard as any).settings?.resolution); if ((dashboard as any).settings?.resolution) { const savedResolution = (dashboard as any).settings.resolution as Resolution; console.log("✅ 저장된 해상도 복원:", savedResolution); setResolution(savedResolution); } else { console.log("⚠️ 저장된 해상도 없음"); } if ((dashboard as any).settings?.backgroundColor) { console.log("✅ 저장된 배경색 복원:", (dashboard as any).settings.backgroundColor); setCanvasBackgroundColor((dashboard as any).settings.backgroundColor); } // 요소들 설정 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); } }; // 새로운 요소 생성 (동적 그리드 기반 기본 크기) const createElement = useCallback( (type: ElementType, subtype: ElementSubtype, x: number, y: number) => { // 좌표 유효성 검사 if (isNaN(x) || isNaN(y)) { console.error("Invalid coordinates:", { x, y }); return; } // 기본 크기 설정 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 }; // 달력 최소 크기 } // 현재 해상도에 맞는 셀 크기 계산 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; // 크기 유효성 검사 if (isNaN(defaultWidth) || isNaN(defaultHeight) || defaultWidth <= 0 || defaultHeight <= 0) { console.error("Invalid size calculated:", { canvasConfig, cellSize, cellWithGap, defaultCells, defaultWidth, defaultHeight, }); 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); }, [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) => { console.log("🔧 updateElement 호출:", id, updates); 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) => { console.log("💾 saveElementConfig 호출:", updatedElement.id, updatedElement.customTitle); updateElement(updatedElement.id, updatedElement); closeConfigModal(); }, [updateElement, closeConfigModal], ); // 리스트 위젯 설정 저장 (Partial 업데이트) const saveListWidgetConfig = useCallback( (updates: Partial) => { 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, customTitle: el.customTitle, // customTitle을 별도로 저장 showHeader: el.showHeader, // 헤더 표시 여부도 저장 content: el.content, dataSource: el.dataSource, chartConfig: el.chartConfig, })); let savedDashboard; if (dashboardId) { // 기존 대시보드 업데이트 console.log("💾 저장 시작 - 현재 resolution 상태:", resolution); console.log("💾 저장 시작 - 현재 배경색 상태:", canvasBackgroundColor); const updateData = { elements: elementsData, settings: { resolution, backgroundColor: canvasBackgroundColor, }, }; console.log("💾 저장할 데이터:", updateData); console.log("💾 저장할 settings:", updateData.settings); savedDashboard = await dashboardApi.updateDashboard(dashboardId, updateData); console.log("✅ 저장된 대시보드:", savedDashboard); console.log("✅ 저장된 settings:", (savedDashboard as any).settings); alert(`대시보드 "${savedDashboard.title}"이 업데이트되었습니다!`); // 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, settings: { resolution, backgroundColor: canvasBackgroundColor, }, }; savedDashboard = await dashboardApi.createDashboard(dashboardData); const viewDashboard = confirm(`대시보드 "${title}"이 저장되었습니다!\n\n지금 확인해보시겠습니까?`); if (viewDashboard) { // Next.js 라우터로 뷰어 페이지 이동 router.push(`/dashboard/${savedDashboard.id}`); } } } catch (error) { const errorMessage = error instanceof Error ? error.message : "알 수 없는 오류"; alert(`대시보드 저장 중 오류가 발생했습니다.\n\n오류: ${errorMessage}\n\n관리자에게 문의하세요.`); } }, [elements, dashboardId, router, resolution, canvasBackgroundColor]); // 로딩 중이면 로딩 화면 표시 if (isLoading) { return (
대시보드 로딩 중...
잠시만 기다려주세요
); } return (
{/* 상단 메뉴바 */} router.push(`/dashboard/${dashboardId}`) : undefined} dashboardTitle={dashboardTitle} onAddElement={addElementFromMenu} resolution={resolution} onResolutionChange={handleResolutionChange} currentScreenResolution={screenResolution} backgroundColor={canvasBackgroundColor} onBackgroundColorChange={setCanvasBackgroundColor} /> {/* 캔버스 영역 - 해상도에 따른 크기, 중앙 정렬 */}
{/* 요소 설정 모달 */} {configModalElement && ( <> {configModalElement.type === "widget" && configModalElement.subtype === "list" ? ( ) : ( )} )}
); } // 요소 제목 생성 헬퍼 함수 function getElementTitle(type: ElementType, subtype: ElementSubtype): string { if (type === "chart") { switch (subtype) { case "bar": return "바 차트"; 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 "차량 위치 지도"; case "calendar": return "달력 위젯"; case "driver-management": return "기사 관리 위젯"; case "list": return "리스트 위젯"; default: return "위젯"; } } return "요소"; } // 요소 내용 생성 헬퍼 함수 function getElementContent(type: ElementType, subtype: ElementSubtype): string { if (type === "chart") { switch (subtype) { case "bar": return "바 차트가 여기에 표시됩니다"; 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"; case "calendar": return "calendar"; case "driver-management": return "driver-management"; case "list": return "list-widget"; default: return "위젯 내용이 여기에 표시됩니다"; } } return "내용이 여기에 표시됩니다"; }