diff --git a/package-lock.json b/package-lock.json index b12247b..4c3ef0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,7 @@ "react-dnd": "^16.0.1", "react-dom": "^19.0.0", "react-draggable": "^4.4.6", - "react-grid-layout": "^1.5.0", + "react-grid-layout": "^1.5.1", "react-icons": "^5.4.0", "react-pdf": "^9.2.1", "react-plotly.js": "^2.6.0", @@ -9710,8 +9710,9 @@ } }, "node_modules/react-grid-layout": { - "version": "1.5.0", - "integrity": "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.1.tgz", + "integrity": "sha512-4Fr+kKMk0+m1HL/BWfHxi/lRuaOmDNNKQDcu7m12+NEYcen20wIuZFo789u3qWCyvUsNUxCiyf0eKq4WiJSNYw==", "dependencies": { "clsx": "^2.0.0", "fast-equals": "^4.0.3", @@ -9798,7 +9799,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==", - "license": "MIT", "dependencies": { "prop-types": "15.x", "react-draggable": "^4.0.3" diff --git a/package.json b/package.json index 2067a08..d97adb6 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "react-dnd": "^16.0.1", "react-dom": "^19.0.0", "react-draggable": "^4.4.6", - "react-grid-layout": "^1.5.0", + "react-grid-layout": "^1.5.1", "react-icons": "^5.4.0", "react-pdf": "^9.2.1", "react-plotly.js": "^2.6.0", diff --git a/src/app/(monitoring)/d/content/chartSection.tsx b/src/app/(monitoring)/d/content/chartSection.tsx index 79f50e9..4d5b38a 100644 --- a/src/app/(monitoring)/d/content/chartSection.tsx +++ b/src/app/(monitoring)/d/content/chartSection.tsx @@ -15,6 +15,7 @@ import CommonWidget from "@/app/components/dashboard/commonWidget"; import { useWidgetOptions } from "@/app/context/widgetOptionContext"; import { useWidgetStore } from "@/app/store/useWidgetStore"; import { ChartOptions, WidgetOptions } from "@/app/types/options"; +import { useDashboardStore } from "@/app/store/useDashboardStore"; Chart.register(zoomPlugin); @@ -76,6 +77,7 @@ const ChartSection = () => { const { charts, addChart, updateChart, removeChart } = useChartStore(); const { widgets, addWidget, updateWidget, removeWidget } = useWidgetStore(); + const { addPanelToDashboard } = useDashboardStore(); const existingChart = chartId ? charts[dashboardId]?.find((chart) => chart.chartId === chartId) @@ -169,50 +171,58 @@ const ChartSection = () => { }; const handleCreateClick = () => { + const newChartId = uuidv4(); + const newWidgetId = uuidv4(); + const defaultGridPos = { x: 0, y: 0, w: 4, h: 4 }; // ✅ 기본 위치값 + if (chartId) { - // 기존 차트가 있는 경우 if (existingChart) { - // 선택된 섹션이 "chartOption"이면 업데이트 if (selectedSection === "chartOption") { - updateChart(dashboardId, chartId, newChartOptions, datasets); - } - // 선택된 섹션이 "widgetOption"이면 변환 (차트 → 위젯) - else { + updateChart( + dashboardId, + chartId, + newChartOptions, + datasets, + existingChart.gridPos + ); + } else { removeChart(dashboardId, chartId); - addWidget(dashboardId, newWidgetOptions); + addWidget( + dashboardId, + newWidgetOptions, + existingChart.gridPos || defaultGridPos + ); + addPanelToDashboard(dashboardId, newWidgetId, "widget"); } - } - // 기존 위젯이 있는 경우 - else if (existingWidget) { - // 선택된 섹션이 "widgetOption"이면 업데이트 + } else if (existingWidget) { if (selectedSection === "widgetOption") { - updateWidget(dashboardId, chartId, newWidgetOptions); - } - // 선택된 섹션이 "chartOption"이면 변환 (위젯 → 차트) - else { + updateWidget( + dashboardId, + chartId, + newWidgetOptions, + existingWidget.gridPos + ); + } else { removeWidget(dashboardId, chartId); - addChart(dashboardId, newChartOptions, datasets); + addChart( + dashboardId, + newChartOptions, + datasets, + existingWidget.gridPos || defaultGridPos + ); + addPanelToDashboard(dashboardId, newChartId, "chart"); } } - // 기존 차트/위젯이 없으면 새로 추가 - else { - if (selectedSection === "chartOption") { - addChart(dashboardId, newChartOptions, datasets); - } else if (selectedSection === "widgetOption") { - addWidget(dashboardId, newWidgetOptions); - } - } - } - // 차트 ID가 없으면 새로운 차트/위젯 추가 - else { + } else { if (selectedSection === "chartOption") { - addChart(dashboardId, newChartOptions, datasets); + addChart(dashboardId, newChartOptions, datasets, defaultGridPos); + addPanelToDashboard(dashboardId, newChartId, "chart"); } else if (selectedSection === "widgetOption") { - addWidget(dashboardId, newWidgetOptions); + addWidget(dashboardId, newWidgetOptions, defaultGridPos); + addPanelToDashboard(dashboardId, newWidgetId, "widget"); } } - // 저장 후 대시보드 상세 페이지로 이동 router.push(`/detail?id=${dashboardId}`); }; diff --git a/src/app/(monitoring)/dashboard2/page.tsx b/src/app/(monitoring)/dashboard2/page.tsx index a334dd3..e894666 100644 --- a/src/app/(monitoring)/dashboard2/page.tsx +++ b/src/app/(monitoring)/dashboard2/page.tsx @@ -8,7 +8,7 @@ import SearchInput from "@/app/components/search/searchInput"; import Alert from "@/app/components/alert/alert"; import { useRouter } from "next/navigation"; import TabMenu from "@/app/components/menu/tabMenu"; -import { useDashboardStore } from "@/app/store/useDashboardStore"; +import { PanelLayout, useDashboardStore } from "@/app/store/useDashboardStore"; import { useChartStore } from "@/app/store/useChartStore"; import { useWidgetStore } from "@/app/store/useWidgetStore"; @@ -18,15 +18,17 @@ const Dashboard2Page = () => { dashboardList, addDashboard, removeDashboard, - dashboardChartMap, - addChartToDashboard, + dashboardPanels, + addPanelToDashboard, updateDashboard, } = useDashboardStore(); const { charts, addChart } = useChartStore(); - const { widgets, cloneWidget } = useWidgetStore(); + const { widgets, addWidget } = useWidgetStore(); + const { saveDashboard } = useDashboardStore(); + const [isModalOpen, setIsModalOpen] = useState(false); const [editingTabIndex, setEditingTabIndex] = useState(null); - const [menuOpenIndex, setMenuOpenIndex] = useState(null); + const [menuOpenIndex, setMenuOpenIndex] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [alertMessage, setAlertMessage] = useState(""); @@ -40,7 +42,7 @@ const Dashboard2Page = () => { tab.label.toLowerCase().includes(searchQuery.toLowerCase()) ); - // ✅ 대시보드 추가 + // 대시보드 추가 const handleTabAdd = (newTabName: string, newTabDescription: string) => { addDashboard({ id: uuidv4(), @@ -51,7 +53,7 @@ const Dashboard2Page = () => { setAlertMessage("새로운 탭이 추가되었습니다!"); }; - // ✅ 대시보드 수정 + // 대시보드 수정 const handleTabEdit = ( id: string, newName: string, @@ -62,7 +64,7 @@ const Dashboard2Page = () => { setAlertMessage("탭이 수정되었습니다!"); }; - // ✅ 대시보드 삭제 + // 대시보드 삭제 const handleTabDelete = (dashboardId: string) => { removeDashboard(dashboardId); setMenuOpenIndex(null); @@ -70,7 +72,7 @@ const Dashboard2Page = () => { setAlertMessage("대시보드가 삭제되었습니다!"); }; - // ✅ 대시보드 복제 (차트 포함) + // 대시보드 복제 (차트 & 위젯 포함) const handleTabClone = ( dashboardId: string, label: string, @@ -79,50 +81,78 @@ const Dashboard2Page = () => { const newDashboardId = uuidv4(); const newLabel = `${label}_copy`; + // 새 대시보드 추가 addDashboard({ id: newDashboardId, label: newLabel, description }); - // ✅ 기존 차트 복제 - const chartsToClone = dashboardChartMap[dashboardId] || []; - const newChartIds: string[] = []; - - chartsToClone.forEach((chartId) => { - const existingChart = Object.values(charts) - .flat() - .find((chart) => chart.chartId === chartId); - - if (existingChart) { - const newChartId = uuidv4(); - const clonedChartOptions = { ...existingChart.chartOptions }; - const clonedDatasets = existingChart.datasets.map((dataset) => ({ - ...dataset, - })); - - addChart(newDashboardId, clonedChartOptions, clonedDatasets); - addChartToDashboard(newDashboardId, newChartId); - newChartIds.push(newChartId); + // 대시보드가 추가된 후 패널을 복제 + const panelsToClone = dashboardPanels[dashboardId] || []; + const newDashboardPanels: PanelLayout[] = []; + + panelsToClone.forEach((panel) => { + const { panelId, type, gridPos } = panel; + + if (type === "chart") { + const existingChart = Object.values(charts) + .flat() + .find((chart) => chart.chartId === panelId); + + if (existingChart) { + const newChartId = uuidv4(); + const clonedChartOptions = { ...existingChart.chartOptions }; + const clonedDatasets = existingChart.datasets.map((dataset) => ({ + ...dataset, + })); + + const clonedGridPos = { ...gridPos }; + + addChart( + newDashboardId, + clonedChartOptions, + clonedDatasets, + clonedGridPos + ); + addPanelToDashboard(newDashboardId, newChartId, "chart"); + + newDashboardPanels.push({ + panelId: newChartId, + type: "chart", + gridPos: clonedGridPos, + }); + } } - }); - - // ✅ 기존 대시보드의 위젯 복제 - const widgetsToClone = widgets[dashboardId] || []; - const newWidgetIds: string[] = []; - widgetsToClone.forEach((widget) => { - const newWidgetId = uuidv4(); - const clonedWidgetOptions = { - ...widget.widgetOptions, - widgetId: newWidgetId, - }; - - useWidgetStore.getState().addWidget(newDashboardId, clonedWidgetOptions); - newWidgetIds.push(newWidgetId); + if (type === "widget") { + const existingWidget = Object.values(widgets) + .flat() + .find((widget) => widget.widgetId === panelId); + + if (existingWidget) { + const newWidgetId = uuidv4(); + const clonedWidgetOptions = { + ...existingWidget.widgetOptions, + widgetId: newWidgetId, + }; + + const clonedGridPos = { ...gridPos }; + + addWidget(newDashboardId, clonedWidgetOptions, clonedGridPos); + addPanelToDashboard(newDashboardId, newWidgetId, "widget"); + + newDashboardPanels.push({ + panelId: newWidgetId, + type: "widget", + gridPos: clonedGridPos, + }); + } + } }); - console.log("📌 새로운 대시보드 ID:", newDashboardId); - console.log("📌 복제된 차트 ID 리스트:", newChartIds); - console.log("📌 복제된 위젯 ID 리스트:", newWidgetIds); + // 복제된 패널을 저장할 때 `dashboardPanels` 업데이트 + console.log("복제된 패널 리스트:", newDashboardPanels); + saveDashboard(newDashboardId, newDashboardPanels); setAlertMessage("대시보드가 복제되었습니다!"); + router.push(`/detail?id=${newDashboardId}`); }; const handleTabClick = (tab: any) => { @@ -156,7 +186,7 @@ const Dashboard2Page = () => { onClick={() => setIsModalOpen(true)} className="flex bg-navy-btn py-1.5 px-2 rounded-lg text-white text-sm hover:bg-navy-btn_hover mb-4 justify-self-end" > - + 항목 추가 + + 대시보드 추가 {alertMessage && }
@@ -176,11 +206,11 @@ const Dashboard2Page = () => { { - e.stopPropagation(); - setMenuOpenIndex(menuOpenIndex === index ? null : index); + e.stopPropagation(); // 메뉴 클릭 유지 + setMenuOpenIndex(menuOpenIndex === tab.id ? null : tab.id); }} /> - {menuOpenIndex === index && ( + {menuOpenIndex === tab.id && ( setEditingTabIndex(tab.id)} diff --git a/src/app/(monitoring)/detail/content/detailDashboard copy.tsx b/src/app/(monitoring)/detail/content/detailDashboard copy.tsx new file mode 100644 index 0000000..807e02a --- /dev/null +++ b/src/app/(monitoring)/detail/content/detailDashboard copy.tsx @@ -0,0 +1,543 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { Chart, useChartStore } from "@/app/store/useChartStore"; +import { PanelLayout, useDashboardStore } from "@/app/store/useDashboardStore"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Layout, Responsive, WidthProvider } from "react-grid-layout"; +import "react-grid-layout/css/styles.css"; +import "react-resizable/css/styles.css"; +import AddChartBar from "@/app/components/bar/addChartBar"; +import TimeRangeBar from "@/app/components/bar/timeRangeBar"; +import ChartWidget from "@/app/components/dashboard/chartWidget"; +import CommonWidget from "@/app/components/dashboard/commonWidget"; +import CustomTable from "@/app/components/table/customTable"; +import TabMenu from "@/app/components/menu/tabMenu"; +import { MoreVertical } from "lucide-react"; +import { useWidgetStore, Widget } from "@/app/store/useWidgetStore"; +import { Dataset } from "@/app/context/chartOptionContext"; +import { v4 as uuidv4 } from "uuid"; +import Alert from "@/app/components/alert/alert"; + +const ResponsiveGridLayout = WidthProvider(Responsive); + +const DetailDashboard = () => { + const router = useRouter(); + const id = useSearchParams(); + const dashboardId = id.get("id") || "1"; + + const { charts, addChart, removeChart } = useChartStore(); + const { widgets, addWidget, removeWidget } = useWidgetStore(); + const { dashboardPanels, addPanelToDashboard, dashboardList, saveDashboard } = + useDashboardStore(); + + const [, forceUpdate] = useState(0); + + useEffect(() => { + console.log( + "🚀 Zustand 상태 확인 (차트):", + useChartStore.getState().charts + ); + console.log( + "🚀 Zustand 상태 확인 (위젯):", + useWidgetStore.getState().widgets + ); + + console.log( + "🚀 Zustand 상태 확인 (대시보드):", + useDashboardStore.getState().dashboardPanels + ); + + // 🚀 강제로 상태 업데이트 + forceUpdate((prev) => prev + 1); + }, []); + + const chartIds = + dashboardPanels[dashboardId]?.map((panel) => panel.panelId) || []; + const [from, setFrom] = useState(null); + const [to, setTo] = useState(null); + const [refreshTime, setRefreshTime] = useState(10); + const [lastUpdated, setLastUpdated] = useState(null); + const [menuOpenIndex, setMenuOpenIndex] = useState(null); + const [isCloneModalOpen, setIsCloneModalOpen] = useState(false); + const [selectedDashboard, setSelectedDashboard] = useState( + null + ); + const [selectedItem, setSelectedItem] = useState(null); + const [alertMessage, setAlertMessage] = useState(""); + const [gridLayout, setGridLayout] = useState< + { i: string; x: number; y: number; w: number; h: number }[] + >([]); + const [maxWidth, setMaxWidth] = useState(500); + const [maxHeight, setMaxHeight] = useState(500); + const [isEditing, setIsEditing] = useState(false); + const [prevLayout, setPrevLayout] = useState([]); + + const layouts = useMemo(() => ({ lg: gridLayout }), [gridLayout]); + + const handleEditClick = () => { + // if (isEditing) { + // // "Save" 버튼을 눌렀을 때 위치 및 크기 저장 + // const updatedLayouts: PanelLayout[] = gridLayout.map((layout) => ({ + // panelId: layout.i, + // type: + // dashboardPanels[dashboardId]?.find( + // (panel) => panel.panelId === layout.i + // )?.type || "chart", + // x: layout.x, + // y: layout.y, + // w: layout.w, + // h: layout.h, + // })); + + // saveDashboard(dashboardId, updatedLayouts); + // } + setIsEditing((prev) => !prev); + }; + + useEffect(() => { + if (typeof window !== "undefined") { + // 브라우저에서만 실행 + setMaxWidth(window.innerWidth); + setMaxHeight(window.innerHeight - 100); + + const handleResize = () => { + setMaxWidth(window.innerWidth); + setMaxHeight(window.innerHeight - 100); + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + } + }, []); + + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem("dashboard-layout", JSON.stringify(gridLayout)); + } + }, [gridLayout]); + + const chartDataList = (dashboardPanels[dashboardId] || []) + .filter((panel) => panel.type === "chart") + .map((panel) => + charts[dashboardId]?.find((chart) => chart?.chartId === panel.panelId) + ) + .filter((chart): chart is Chart => !!chart); + + const widgetDataList = (dashboardPanels[dashboardId] || []) + .filter((panel) => panel.type === "widget") + .map((panel) => + widgets[dashboardId]?.find((widget) => widget?.widgetId === panel.panelId) + ) + .filter((widget): widget is Widget => !!widget); + + const handleTabClone = (itemId: string) => { + setSelectedItem(itemId); + setIsCloneModalOpen(true); + }; + + const confirmClone = () => { + if (!selectedDashboard || !selectedItem) return; + + const targetDashboardId = selectedDashboard; + let newItemId: string | null = null; + + // 차트 복제 + const existingChart = Object.values(charts) + .flat() + .find((chart) => chart.chartId === selectedItem); + + if (existingChart) { + const newChartId = uuidv4(); + const clonedChartOptions = { ...existingChart.chartOptions }; + const clonedDatasets = existingChart.datasets.map((dataset) => ({ + ...dataset, + })); + + addChart(targetDashboardId, clonedChartOptions, clonedDatasets); + addPanelToDashboard(targetDashboardId, newChartId, "chart"); + + newItemId = newChartId; + } + + // 위젯 복제 + const existingWidget = Object.values(widgets) + .flat() + .find((widget) => widget.widgetId === selectedItem); + + if (existingWidget) { + const newWidgetId = uuidv4(); + const clonedWidgetOptions = { + ...existingWidget.widgetOptions, + widgetId: newWidgetId, // 새로운 ID 적용 + }; + + addWidget(targetDashboardId, clonedWidgetOptions); + addPanelToDashboard(targetDashboardId, newWidgetId, "widget"); + newItemId = newWidgetId; + } + + if (newItemId) { + setAlertMessage("선택한 차트/위젯이 복제되었습니다!"); + } else { + setAlertMessage("복제할 항목이 없습니다."); + } + + setIsCloneModalOpen(false); + }; + + const convertToTableData = (datasets: Dataset[]) => { + if (!datasets || datasets.length === 0) return { headers: [], rows: [] }; + + const headers = ["항목", ...datasets.map((dataset) => dataset.label)]; + + const rows = datasets[0].data.map((_, index) => ({ + name: `Point ${index + 1}`, + ...datasets.reduce((acc, dataset) => { + acc[dataset.label] = dataset.data[index]; + return acc; + }, {} as Record), + })); + + return { headers, rows }; + }; + + const MIN_CHART_WIDTH = 6; // 차트 최소 가로 크기 + const MIN_CHART_HEIGHT = 5; // 차트 최소 세로 크기 + const MAX_CHART_HEIGHT = 10; // 차트 최소 세로 크기 + const MIN_WIDGET_WIDTH = 3; // 위젯 최소 가로 크기 + const MIN_WIDGET_HEIGHT = 4; // 위젯 최소 세로 크기 + const MAX_WIDGET_HEIGHT = 6; // 위젯 최소 세로 크기 + + const initialLayout = useMemo(() => { + return [ + ...chartDataList.map((chart, index) => ({ + i: chart.chartId, + x: (index * 6) % 12, // 한 줄에 2개 배치 + y: Math.floor(index / 2) * 5, // 차트 배치 간격 조정 + w: Math.max(MIN_CHART_WIDTH, maxWidth / 200), // 최소 크기 보장 + h: Math.max( + MIN_CHART_HEIGHT, + chart.chartOptions.displayMode === "chart" ? 5 : 6 + ), + })), + ...widgetDataList.map((widget, index) => ({ + i: widget.widgetId, + x: (index * 3) % 12, // 한 줄에 최대 4개 배치 + y: Math.floor(index / 4) * 4 + chartDataList.length * 5, + w: Math.max(MIN_WIDGET_WIDTH, maxWidth / 250), // 최소 크기 보장 + h: MIN_WIDGET_HEIGHT, + })), + ]; + }, [chartDataList, widgetDataList, maxWidth]); + + useEffect(() => { + const savedLayout = localStorage.getItem("dashboard-layout"); + if (savedLayout) { + // ✅ 기존에 저장된 레이아웃이 있으면 그걸 적용 + console.log( + "📌 LocalStorage에서 불러온 레이아웃:", + JSON.parse(savedLayout) + ); + setGridLayout(JSON.parse(savedLayout)); + } else if (gridLayout.length === 0 && initialLayout.length > 0) { + // ✅ 기존에 저장된 값이 없고, 초기 레이아웃이 있을 때만 설정 + console.log( + "📌 저장된 레이아웃 없음, 초기 레이아웃 적용:", + initialLayout + ); + setGridLayout(initialLayout); + } + }, []); + + const closeCloneModal = () => { + setIsCloneModalOpen(false); + setSelectedDashboard(null); + }; + + useEffect(() => { + const savedLayout = localStorage.getItem("dashboard-layout"); + if (savedLayout) { + setGridLayout(JSON.parse(savedLayout)); // 저장된 위치 적용 + } + }, []); + + useEffect(() => { + localStorage.setItem("dashboard-layout", JSON.stringify(gridLayout)); + }, [gridLayout]); + const handleLayoutChange = (layout: Layout[]) => { + // 변경 사항이 없으면 상태 업데이트 하지 않음 + if (JSON.stringify(prevLayout) === JSON.stringify(layout)) { + return; + } + + console.log("📌 변경된 레이아웃:", layout); + setGridLayout(layout); + setPrevLayout(layout); // 이전 상태 업데이트 + // ✅ `i`를 `panelId`로 변환하여 저장 + // const updatedLayouts: PanelLayout[] = layout.map((l) => ({ + // panelId: l.i, // ✅ `i`를 `panelId`로 매핑 + // type: + // dashboardPanels[dashboardId]?.find((p) => p.panelId === l.i)?.type || + // "chart", + // x: l.x, + // y: l.y, + // w: l.w, + // h: l.h, + // })); + + // console.log("📌 변경된 레이아웃:", updatedLayouts); + + // if (JSON.stringify(prevLayout) !== JSON.stringify(updatedLayouts)) { + // setGridLayout(layout); // ✅ gridLayout에는 `i`를 유지 + // setPrevLayout(layout); + // saveDashboard(dashboardId, updatedLayouts); // ✅ Zustand에는 `panelId`로 저장 + // } + }; + + useEffect(() => { + if ( + dashboardPanels[dashboardId] && + dashboardPanels[dashboardId].length > 0 && + gridLayout.length === 0 + ) { + const savedLayout = dashboardPanels[dashboardId].map((panel) => ({ + i: panel.panelId, + x: panel.gridPos?.x ?? 0, + y: panel.gridPos?.y ?? 0, + w: panel.gridPos?.w ?? 4, + h: panel.gridPos?.h ?? 4, + })); + + console.log("📌 Zustand에서 불러온 gridLayout 설정: ", savedLayout); + setGridLayout(savedLayout); + setPrevLayout(savedLayout); + } + }, [dashboardPanels, dashboardId]); + + return ( +
+ router.push(`/d?id=${dashboardId}`)} + gridCols={2} + onGridChange={() => {}} + gridVisible={true} + modifiable={true} + onEditClick={handleEditClick} + /> + + + type === "from" ? setFrom(value) : setTo(value) + } + onRefreshChange={setRefreshTime} + /> +
+ + {chartDataList.map((chart) => { + const chartLayout = gridLayout.find( + (item) => item.i === chart.chartId + ) || { + i: chart.chartId, + x: 0, + y: MAX_CHART_HEIGHT, + w: Math.min(4, maxWidth / 250), + h: 4, + }; + + return ( +
+
+ {/* 메뉴 버튼 (기존 유지) */} +
+ { + e.stopPropagation(); + setMenuOpenIndex( + menuOpenIndex === chart.chartId ? null : chart.chartId + ); + }} + /> + {menuOpenIndex === chart.chartId && ( + + router.push( + `/d?id=${dashboardId}&chartId=${chart.chartId}` + ) + } + setIsModalOpen={() => {}} + setMenuOpenIndex={setMenuOpenIndex} + handleTabDelete={() => + removeChart(dashboardId, chart.chartId) + } + handleTabClone={handleTabClone} + /> + )} +
+ + {/* 제목 */} +

+ {chart.chartOptions.titleText} +

+ + {/* 차트 또는 테이블 렌더링 */} +
+ {chart.chartOptions.displayMode === "chart" ? ( + + ) : ( + ({ + key: dataset.label, + label: dataset.label, + })), + ]} + data={convertToTableData(chart.datasets).rows} + title={chart.chartOptions.titleText} + /> + )} +
+
+
+ ); + })} + + {widgetDataList.map((widget) => { + const widgetLayout = gridLayout.find( + (item) => item.i === widget.widgetId + ) || { + i: widget.widgetId, + x: 0, + y: MAX_WIDGET_HEIGHT, + w: Math.min(2, maxWidth / 250), + h: 3, + }; + + return ( +
+
+
+ { + e.stopPropagation(); + setMenuOpenIndex( + menuOpenIndex === widget.widgetId + ? null + : widget.widgetId + ); + }} + /> + {menuOpenIndex === widget.widgetId && ( + + router.push( + `/d?id=${dashboardId}&chartId=${widget.widgetId}` + ) + } + setIsModalOpen={() => {}} + setMenuOpenIndex={setMenuOpenIndex} + handleTabDelete={() => + removeWidget(dashboardId, widget.widgetId) + } + handleTabClone={handleTabClone} + /> + )} +
+ {/* 위젯 렌더링 */} + +
+
+ ); + })} +
+
+ {isCloneModalOpen && ( +
+
+

대시보드 선택

+
    + {dashboardList.map((dashboard) => ( +
  • setSelectedDashboard(dashboard.id)} + className={`cursor-pointer p-2 rounded ${ + selectedDashboard === dashboard.id + ? "bg-navy-btn text-white" + : "hover:bg-gray-100" + }`} + > + {dashboard.label} +
  • + ))} +
+
+ + +
+
+
+ )} + {alertMessage && } +
+ ); +}; + +export default DetailDashboard; diff --git a/src/app/(monitoring)/detail/content/detailDashboard.tsx b/src/app/(monitoring)/detail/content/detailDashboard.tsx index d1a467c..8fc248d 100644 --- a/src/app/(monitoring)/detail/content/detailDashboard.tsx +++ b/src/app/(monitoring)/detail/content/detailDashboard.tsx @@ -1,9 +1,10 @@ "use client"; -import React, { useState, useEffect } from "react"; -import { useChartStore } from "@/app/store/useChartStore"; -import { useDashboardStore } from "@/app/store/useDashboardStore"; +import React, { useState, useEffect, useMemo } from "react"; +import { Chart, useChartStore } from "@/app/store/useChartStore"; +import { PanelLayout, useDashboardStore } from "@/app/store/useDashboardStore"; import { useRouter, useSearchParams } from "next/navigation"; +import { Layout, Responsive, WidthProvider } from "react-grid-layout"; import AddChartBar from "@/app/components/bar/addChartBar"; import TimeRangeBar from "@/app/components/bar/timeRangeBar"; import ChartWidget from "@/app/components/dashboard/chartWidget"; @@ -11,10 +12,22 @@ import CommonWidget from "@/app/components/dashboard/commonWidget"; import CustomTable from "@/app/components/table/customTable"; import TabMenu from "@/app/components/menu/tabMenu"; import { MoreVertical } from "lucide-react"; -import { useWidgetStore } from "@/app/store/useWidgetStore"; -import { Dataset } from "@/app/context/chartOptionContext"; +import { useWidgetStore, Widget } from "@/app/store/useWidgetStore"; import { v4 as uuidv4 } from "uuid"; import Alert from "@/app/components/alert/alert"; +import { convertToTable } from "@/app/utils/convertToTable"; +import { + MIN_CHART_WIDTH, + MIN_CHART_HEIGHT, + MIN_WIDGET_WIDTH, + MIN_WIDGET_HEIGHT, + MAX_WIDGET_WIDTH, + MAX_WIDGET_HEIGHT, + MAX_CHART_HEIGHT, + MAX_CHART_WIDTH, +} from "@/app/data/chart/chartDetail"; + +const ResponsiveGridLayout = WidthProvider(Responsive); const DetailDashboard = () => { const router = useRouter(); @@ -22,42 +35,62 @@ const DetailDashboard = () => { const dashboardId = id.get("id") || "1"; const { charts, addChart, removeChart } = useChartStore(); - const { widgets, removeWidget } = useWidgetStore(); - const { dashboardChartMap, addChartToDashboard, dashboardList } = + const { widgets, addWidget, removeWidget } = useWidgetStore(); + const { dashboardPanels, addPanelToDashboard, dashboardList, saveDashboard } = useDashboardStore(); - const chartIds = dashboardChartMap[dashboardId] || []; + console.log("📌 현재 대시보드 ID:", dashboardId); + console.log("📌 해당 대시보드의 차트 목록:", charts[dashboardId]); - const chartDataList = chartIds - .map((chartId) => - charts[dashboardId]?.find((chart) => chart.chartId === chartId) - ) - .filter(Boolean); - - const widgetDataList = chartIds - .map((widgetId) => - widgets[dashboardId]?.find((widget) => widget.widgetId === widgetId) - ) - .filter(Boolean); + console.log(charts); const [from, setFrom] = useState(null); const [to, setTo] = useState(null); const [refreshTime, setRefreshTime] = useState(10); const [lastUpdated, setLastUpdated] = useState(null); const [menuOpenIndex, setMenuOpenIndex] = useState(null); - const [gridCols, setGridCols] = useState(2); - const [isCloneModalOpen, setIsCloneModalOpen] = useState(false); + const [isCloneModalOpen, setIsCloneModalOpen] = useState(false); const [selectedDashboard, setSelectedDashboard] = useState( null ); - const [alertMessage, setAlertMessage] = useState(""); const [selectedItem, setSelectedItem] = useState(null); + const [alertMessage, setAlertMessage] = useState(""); + const [gridLayout, setGridLayout] = useState< + { i: string; x: number; y: number; w: number; h: number }[] + >([]); + const [isEditing, setIsEditing] = useState(false); + const [prevLayout, setPrevLayout] = useState([]); + + const layouts = useMemo(() => ({ lg: gridLayout }), [gridLayout]); + + const handleEditClick = () => { + if (isEditing) { + const updatedLayouts: PanelLayout[] = gridLayout.map((layout) => ({ + panelId: layout.i, + type: charts[dashboardId]?.some((chart) => chart.chartId === layout.i) + ? "chart" + : "widget", + gridPos: { + x: layout.x, + y: layout.y, + w: layout.w, + h: layout.h, + }, + })); - const closeCloneModal = () => { - setIsCloneModalOpen(false); - setSelectedDashboard(null); + console.log("✅ 저장할 패널 데이터:", updatedLayouts); + saveDashboard(dashboardId, updatedLayouts); + setPrevLayout( + updatedLayouts.map((panel) => ({ ...panel.gridPos, i: panel.panelId })) + ); + } + setIsEditing((prev) => !prev); }; + const chartDataList = charts[dashboardId] || []; + + const widgetDataList = widgets[dashboardId] || []; + const handleTabClone = (itemId: string) => { setSelectedItem(itemId); setIsCloneModalOpen(true); @@ -81,9 +114,22 @@ const DetailDashboard = () => { ...dataset, })); + // ✅ gridPos 복사 (기존 위치 유지) + const clonedGridPos = { ...existingChart.gridPos }; + addChart(targetDashboardId, clonedChartOptions, clonedDatasets); - addChartToDashboard(targetDashboardId, newChartId); + addPanelToDashboard(targetDashboardId, newChartId, "chart"); + newItemId = newChartId; + + saveDashboard(targetDashboardId, [ + ...(dashboardPanels[targetDashboardId] || []), + { + panelId: newChartId, + type: "chart", + gridPos: clonedGridPos, // ✅ 기존 gridPos를 유지 + }, + ]); } // 위젯 복제 @@ -95,72 +141,101 @@ const DetailDashboard = () => { const newWidgetId = uuidv4(); const clonedWidgetOptions = { ...existingWidget.widgetOptions, - widgetId: newWidgetId, // 새로운 ID 적용 + widgetId: newWidgetId, }; - useWidgetStore - .getState() - .addWidget(targetDashboardId, clonedWidgetOptions); + // ✅ gridPos 복사 (기존 위치 유지) + const clonedGridPos = { ...existingWidget.gridPos }; + + addWidget(targetDashboardId, clonedWidgetOptions); + addPanelToDashboard(targetDashboardId, newWidgetId, "widget"); + newItemId = newWidgetId; - } - if (newItemId) { - setAlertMessage("선택한 차트/위젯이 복제되었습니다!"); - } else { - setAlertMessage("복제할 항목이 없습니다."); + saveDashboard(targetDashboardId, [ + ...(dashboardPanels[targetDashboardId] || []), + { + panelId: newWidgetId, + type: "widget", + gridPos: clonedGridPos, // ✅ 기존 gridPos를 유지 + }, + ]); } - closeCloneModal(); + setIsCloneModalOpen(false); + setAlertMessage("복제 완료!"); }; - useEffect(() => { - const now = new Date(); - setFrom(now.toISOString().slice(0, 16)); - setTo(now.toISOString().slice(0, 16)); - setLastUpdated(now.toLocaleTimeString()); - }, []); + console.log("이거 왜 안보일까 >>>", dashboardPanels[dashboardId]); useEffect(() => { - if (refreshTime !== "autoType") { - const interval = setInterval(() => { - setLastUpdated(new Date().toLocaleTimeString()); - }, refreshTime * 1000); - return () => clearInterval(interval); + if ( + dashboardPanels[dashboardId] && + dashboardPanels[dashboardId].length > 0 && + gridLayout.length === 0 + ) { + const savedLayout = dashboardPanels[dashboardId].map((panel) => ({ + i: panel.panelId, + x: panel.gridPos?.x ?? 0, + y: panel.gridPos?.y ?? 0, + w: panel.gridPos?.w ?? 4, + h: panel.gridPos?.h ?? 4, + })); + + console.log("📌 Zustand에서 불러온 gridLayout 설정: ", savedLayout); + setGridLayout(savedLayout); + setPrevLayout(savedLayout); } - }, [refreshTime]); + }, [dashboardPanels, dashboardId]); - const handleGridChange = (change: number) => { - setGridCols((prev) => Math.max(1, Math.min(4, prev + change))); + const closeCloneModal = () => { + setIsCloneModalOpen(false); + setSelectedDashboard(null); }; - const convertToTableData = (datasets: Dataset[]) => { - if (!datasets || datasets.length === 0) return { headers: [], rows: [] }; - - const headers = ["항목", ...datasets.map((dataset) => dataset.label)]; + const handleLayoutChange = (layout: Layout[]) => { + if (JSON.stringify(prevLayout) === JSON.stringify(layout)) { + return; + } - const rows = datasets[0].data.map((_, index) => ({ - name: `Point ${index + 1}`, - ...datasets.reduce((acc, dataset) => { - acc[dataset.label] = dataset.data[index]; // label을 key로 사용 - return acc; - }, {} as Record), - })); + const updatedLayouts: PanelLayout[] = layout.map((l) => { + const chartExists = charts[dashboardId]?.some( + (chart) => chart.chartId === l.i + ); + const widgetExists = widgets[dashboardId]?.some( + (widget) => widget.widgetId === l.i + ); + + return { + panelId: l.i, + type: chartExists ? "chart" : widgetExists ? "widget" : "chart", + gridPos: { + // ✅ gridPos를 올바르게 업데이트 + x: l.x, + y: l.y, + w: l.w, + h: l.h, + }, + }; + }); - return { headers, rows }; + setGridLayout(layout); + setPrevLayout(layout); + saveDashboard(dashboardId, updatedLayouts); }; return ( -
setMenuOpenIndex(null)} - > +
router.push(`/d?id=${dashboardId}`)} - gridCols={gridCols} - onGridChange={handleGridChange} + gridCols={2} + onGridChange={() => {}} gridVisible={true} + modifiable={true} + onEditClick={handleEditClick} /> + { } onRefreshChange={setRefreshTime} /> - - {widgetDataList.length > 0 && ( -
- {widgetDataList.map( - (widget) => - widget && ( -
-
e.stopPropagation()} - > -
- { - e.stopPropagation(); - setMenuOpenIndex( - menuOpenIndex === widget.widgetId - ? null - : widget.widgetId - ); - }} - /> - {menuOpenIndex === widget.widgetId && ( - - router.push( - `/d?id=${dashboardId}&chartId=${widget.widgetId}` - ) - } - setIsModalOpen={() => {}} - setMenuOpenIndex={setMenuOpenIndex} - handleTabDelete={() => - removeWidget(dashboardId, widget.widgetId) - } - handleTabClone={handleTabClone} - /> - )} -
- -
- + + {chartDataList.map((chart) => { + const chartLayout = gridLayout.find( + (item) => item.i === chart.chartId + ) || { + i: chart.chartId, + x: 0, + y: 0, + w: 4, + h: Math.max(MIN_WIDGET_HEIGHT, 4), + minW: MIN_CHART_WIDTH, + minH: MIN_CHART_HEIGHT, + }; + + return ( +
+
+ {/* 메뉴 버튼 (기존 유지) */} +
+ { + e.stopPropagation(); + setMenuOpenIndex( + menuOpenIndex === chart.chartId ? null : chart.chartId + ); + }} + /> + {menuOpenIndex === chart.chartId && ( + + router.push( + `/d?id=${dashboardId}&chartId=${chart.chartId}` + ) + } + setIsModalOpen={() => {}} + setMenuOpenIndex={setMenuOpenIndex} + handleTabDelete={() => + removeChart(dashboardId, chart.chartId) } - textColor={widget.widgetOptions.textColor} - unit={widget.widgetOptions.unit} - arrowVisible={widget.widgetOptions.arrowVisible} - className="scale-[1] max-w-[300px]" + handleTabClone={handleTabClone} /> -
+ )}
-
- ) - )} -
- )} - {/* 차트는 기존 grid 스타일 유지 */} - {chartDataList.length > 0 && ( -
- {chartDataList.map( - (chart) => - chart && ( -
-
e.stopPropagation()} - > - {/* 차트에도 TabMenu 유지 */} -
- { - e.stopPropagation(); - setMenuOpenIndex( - menuOpenIndex === chart.chartId - ? null - : chart.chartId - ); - }} - /> - {menuOpenIndex === chart.chartId && ( - - router.push( - `/d?id=${dashboardId}&chartId=${chart.chartId}` - ) - } - setIsModalOpen={() => {}} - setMenuOpenIndex={setMenuOpenIndex} - handleTabDelete={() => - removeChart(dashboardId, chart.chartId) - } - handleTabClone={handleTabClone} - /> - )} -
- - {/* 차트 또는 테이블 렌더링 */} + {/* 제목 */} +

+ {chart.chartOptions.titleText} +

+ + {/* 차트 또는 테이블 렌더링 */} +
{chart.chartOptions.displayMode === "chart" ? ( -
-

- {chart.chartOptions.titleText} -

-
- -
-
+ ) : ( { label: dataset.label, })), ]} - data={convertToTableData(chart.datasets).rows} + data={convertToTable(chart.datasets).rows} title={chart.chartOptions.titleText} /> )}
- ) - )} -
- )} +
+ ); + })} + + {widgetDataList.map((widget) => { + const widgetLayout = gridLayout.find( + (item) => item.i === widget.widgetId + ) || { + i: widget.widgetId, + x: 0, + y: 0, + w: 4, // 기본 가로 크기 + h: Math.max(MIN_WIDGET_HEIGHT, 4), // 기본 세로 크기 + minW: MIN_WIDGET_WIDTH, // 최소 가로 크기 설정 + minH: MIN_WIDGET_HEIGHT, // 최소 세로 크기 설정 + }; + + return ( +
+ {/* max-h-[230px] max-w-[530px] */} +
+
+ { + e.stopPropagation(); + setMenuOpenIndex( + menuOpenIndex === widget.widgetId + ? null + : widget.widgetId + ); + }} + /> + {menuOpenIndex === widget.widgetId && ( + + router.push( + `/d?id=${dashboardId}&chartId=${widget.widgetId}` + ) + } + setIsModalOpen={() => {}} + setMenuOpenIndex={setMenuOpenIndex} + handleTabDelete={() => + removeWidget(dashboardId, widget.widgetId) + } + handleTabClone={handleTabClone} + /> + )} +
+ {/* 위젯 렌더링 */} + +
+
+ ); + })} + +
{isCloneModalOpen && (
@@ -365,7 +451,6 @@ const DetailDashboard = () => {
)} - {/* 알림 메시지 */} {alertMessage && }
); diff --git a/src/app/(monitoring)/detail2/content/detailDashboard.tsx b/src/app/(monitoring)/detail2/content/detailDashboard.tsx new file mode 100644 index 0000000..f547f76 --- /dev/null +++ b/src/app/(monitoring)/detail2/content/detailDashboard.tsx @@ -0,0 +1,374 @@ +// "use client"; + +// import React, { useState, useEffect } from "react"; +// import { useChartStore } from "@/app/store/useChartStore"; +// import { useDashboardStore } from "@/app/store/useDashboardStore"; +// import { useRouter, useSearchParams } from "next/navigation"; +// import AddChartBar from "@/app/components/bar/addChartBar"; +// import TimeRangeBar from "@/app/components/bar/timeRangeBar"; +// import ChartWidget from "@/app/components/dashboard/chartWidget"; +// import CommonWidget from "@/app/components/dashboard/commonWidget"; +// import CustomTable from "@/app/components/table/customTable"; +// import TabMenu from "@/app/components/menu/tabMenu"; +// import { MoreVertical } from "lucide-react"; +// import { useWidgetStore } from "@/app/store/useWidgetStore"; +// import { Dataset } from "@/app/context/chartOptionContext"; +// import { v4 as uuidv4 } from "uuid"; +// import Alert from "@/app/components/alert/alert"; + +// const DetailDashboard = () => { +// const router = useRouter(); +// const id = useSearchParams(); +// const dashboardId = id.get("id") || "1"; + +// const { charts, addChart, removeChart } = useChartStore(); +// const { widgets, removeWidget } = useWidgetStore(); +// const { dashboardChartMap, addChartToDashboard, dashboardList } = +// useDashboardStore(); + +// const chartIds = dashboardChartMap[dashboardId] || []; + +// const chartDataList = chartIds +// .map((chartId) => +// charts[dashboardId]?.find((chart) => chart.chartId === chartId) +// ) +// .filter(Boolean); + +// const widgetDataList = chartIds +// .map((widgetId) => +// widgets[dashboardId]?.find((widget) => widget.widgetId === widgetId) +// ) +// .filter(Boolean); + +// const [from, setFrom] = useState(null); +// const [to, setTo] = useState(null); +// const [refreshTime, setRefreshTime] = useState(10); +// const [lastUpdated, setLastUpdated] = useState(null); +// const [menuOpenIndex, setMenuOpenIndex] = useState(null); +// const [gridCols, setGridCols] = useState(2); +// const [isCloneModalOpen, setIsCloneModalOpen] = useState(false); +// const [selectedDashboard, setSelectedDashboard] = useState( +// null +// ); +// const [alertMessage, setAlertMessage] = useState(""); +// const [selectedItem, setSelectedItem] = useState(null); + +// const closeCloneModal = () => { +// setIsCloneModalOpen(false); +// setSelectedDashboard(null); +// }; + +// const handleTabClone = (itemId: string) => { +// setSelectedItem(itemId); +// setIsCloneModalOpen(true); +// }; + +// const confirmClone = () => { +// if (!selectedDashboard || !selectedItem) return; + +// const targetDashboardId = selectedDashboard; +// let newItemId: string | null = null; + +// // 차트 복제 +// const existingChart = Object.values(charts) +// .flat() +// .find((chart) => chart.chartId === selectedItem); + +// if (existingChart) { +// const newChartId = uuidv4(); +// const clonedChartOptions = { ...existingChart.chartOptions }; +// const clonedDatasets = existingChart.datasets.map((dataset) => ({ +// ...dataset, +// })); + +// addChart(targetDashboardId, clonedChartOptions, clonedDatasets); +// addChartToDashboard(targetDashboardId, newChartId); +// newItemId = newChartId; +// } + +// // 위젯 복제 +// const existingWidget = Object.values(widgets) +// .flat() +// .find((widget) => widget.widgetId === selectedItem); + +// if (existingWidget) { +// const newWidgetId = uuidv4(); +// const clonedWidgetOptions = { +// ...existingWidget.widgetOptions, +// widgetId: newWidgetId, // 새로운 ID 적용 +// }; + +// useWidgetStore +// .getState() +// .addWidget(targetDashboardId, clonedWidgetOptions); +// newItemId = newWidgetId; +// } + +// if (newItemId) { +// setAlertMessage("선택한 차트/위젯이 복제되었습니다!"); +// } else { +// setAlertMessage("복제할 항목이 없습니다."); +// } + +// closeCloneModal(); +// }; + +// useEffect(() => { +// const now = new Date(); +// setFrom(now.toISOString().slice(0, 16)); +// setTo(now.toISOString().slice(0, 16)); +// setLastUpdated(now.toLocaleTimeString()); +// }, []); + +// useEffect(() => { +// if (refreshTime !== "autoType") { +// const interval = setInterval(() => { +// setLastUpdated(new Date().toLocaleTimeString()); +// }, refreshTime * 1000); +// return () => clearInterval(interval); +// } +// }, [refreshTime]); + +// const handleGridChange = (change: number) => { +// setGridCols((prev) => Math.max(1, Math.min(4, prev + change))); +// }; + +// const convertToTableData = (datasets: Dataset[]) => { +// if (!datasets || datasets.length === 0) return { headers: [], rows: [] }; + +// const headers = ["항목", ...datasets.map((dataset) => dataset.label)]; + +// const rows = datasets[0].data.map((_, index) => ({ +// name: `Point ${index + 1}`, +// ...datasets.reduce((acc, dataset) => { +// acc[dataset.label] = dataset.data[index]; // label을 key로 사용 +// return acc; +// }, {} as Record), +// })); + +// return { headers, rows }; +// }; + +// return ( +//
setMenuOpenIndex(null)} +// > +// router.push(`/d?id=${dashboardId}`)} +// gridCols={gridCols} +// onGridChange={handleGridChange} +// gridVisible={true} +// /> +// +// type === "from" ? setFrom(value) : setTo(value) +// } +// onRefreshChange={setRefreshTime} +// /> + +// {widgetDataList.length > 0 && ( +//
+// {widgetDataList.map( +// (widget) => +// widget && ( +//
+//
e.stopPropagation()} +// > +//
+// { +// e.stopPropagation(); +// setMenuOpenIndex( +// menuOpenIndex === widget.widgetId +// ? null +// : widget.widgetId +// ); +// }} +// /> +// {menuOpenIndex === widget.widgetId && ( +// +// router.push( +// `/d?id=${dashboardId}&chartId=${widget.widgetId}` +// ) +// } +// setIsModalOpen={() => {}} +// setMenuOpenIndex={setMenuOpenIndex} +// handleTabDelete={() => +// removeWidget(dashboardId, widget.widgetId) +// } +// handleTabClone={handleTabClone} +// /> +// )} +//
+ +//
+// +//
+//
+//
+// ) +// )} +//
+// )} + +// {/* 차트는 기존 grid 스타일 유지 */} +// {chartDataList.length > 0 && ( +//
+// {chartDataList.map( +// (chart) => +// chart && ( +//
+//
e.stopPropagation()} +// > +// {/* 차트에도 TabMenu 유지 */} +//
+// { +// e.stopPropagation(); +// setMenuOpenIndex( +// menuOpenIndex === chart.chartId +// ? null +// : chart.chartId +// ); +// }} +// /> +// {menuOpenIndex === chart.chartId && ( +// +// router.push( +// `/d?id=${dashboardId}&chartId=${chart.chartId}` +// ) +// } +// setIsModalOpen={() => {}} +// setMenuOpenIndex={setMenuOpenIndex} +// handleTabDelete={() => +// removeChart(dashboardId, chart.chartId) +// } +// handleTabClone={handleTabClone} +// /> +// )} +//
+ +// {/* 차트 또는 테이블 렌더링 */} +// {chart.chartOptions.displayMode === "chart" ? ( +//
+//

+// {chart.chartOptions.titleText} +//

+//
+// +//
+//
+// ) : ( +// ({ +// key: dataset.label, +// label: dataset.label, +// })), +// ]} +// data={convertToTableData(chart.datasets).rows} +// title={chart.chartOptions.titleText} +// /> +// )} +//
+//
+// ) +// )} +//
+// )} +// {isCloneModalOpen && ( +//
+//
+//

대시보드 선택

+//
    +// {dashboardList.map((dashboard) => ( +//
  • setSelectedDashboard(dashboard.id)} +// className={`cursor-pointer p-2 rounded ${ +// selectedDashboard === dashboard.id +// ? "bg-navy-btn text-white" +// : "hover:bg-gray-100" +// }`} +// > +// {dashboard.label} +//
  • +// ))} +//
+//
+// +// +//
+//
+//
+// )} +// {/* 알림 메시지 */} +// {alertMessage && } +//
+// ); +// }; + +// export default DetailDashboard; diff --git a/src/app/(monitoring)/detail2/page.tsx b/src/app/(monitoring)/detail2/page.tsx new file mode 100644 index 0000000..548f63a --- /dev/null +++ b/src/app/(monitoring)/detail2/page.tsx @@ -0,0 +1,14 @@ +"use client"; + +import React, { Suspense } from "react"; +import DetailDashboard from "../detail/content/detailDashboard"; + +const DashboardDetailPage = () => { + return ( + Loading...

}> + +
+ ); +}; + +export default DashboardDetailPage; diff --git a/src/app/components/bar/addChartBar.tsx b/src/app/components/bar/addChartBar.tsx index b94710a..4d06a38 100644 --- a/src/app/components/bar/addChartBar.tsx +++ b/src/app/components/bar/addChartBar.tsx @@ -5,19 +5,23 @@ import { useDashboardStore } from "@/app/store/useDashboardStore"; interface AddChartBarProps { isEdit: boolean; onCreateClick: () => void; + onEditClick?: () => void; onSaveClick?: () => void; gridCols?: number; onGridChange?: (change: number) => void; gridVisible?: boolean; + modifiable?: boolean; } const AddChartBar = ({ isEdit, onCreateClick, + onEditClick, onSaveClick, gridCols, onGridChange, gridVisible = false, + modifiable = false, }: AddChartBarProps) => { const router = useRouter(); const searchParams = useSearchParams(); @@ -70,13 +74,25 @@ const AddChartBar = ({
)} - + )} + + + onClick={onCreateClick} + > + Create + +
); diff --git a/src/app/components/dashboard/cardWidgetWithBarChart.tsx b/src/app/components/dashboard/cardWidgetWithBarChart.tsx index 002859f..6d2b60a 100644 --- a/src/app/components/dashboard/cardWidgetWithBarChart.tsx +++ b/src/app/components/dashboard/cardWidgetWithBarChart.tsx @@ -1,7 +1,7 @@ import { Chart as ChartJS, - CategoryScale, // ✅ X축용 (범주형 데이터) - LinearScale, // ✅ Y축용 (숫자형 데이터) 추가 + CategoryScale, + LinearScale, BarElement, Tooltip, Legend, @@ -39,7 +39,7 @@ const CardWidgetWithBarChart = ({ changePercent, chartData, backgroundColor, - textColor = "#000", + textColor, arrowVisible, className, }: CardWidgetProps) => { @@ -51,7 +51,9 @@ const CardWidgetWithBarChart = ({ style={{ backgroundColor }} > {/* 제목 */} -
{title}
+
+ {title} +
{/* 메인 값 */}
@@ -66,7 +68,11 @@ const CardWidgetWithBarChart = ({
{/* 부가 정보 */} - {subText &&
{subText}
} + {subText && ( +
+ {subText} +
+ )} {/* 작은 차트 (미니 바 차트) */} {chartData && ( diff --git a/src/app/components/table/customTable.tsx b/src/app/components/table/customTable.tsx index d5098e3..92e43db 100644 --- a/src/app/components/table/customTable.tsx +++ b/src/app/components/table/customTable.tsx @@ -22,8 +22,9 @@ const CustomTable: React.FC = ({ columns, data, title }) => { })); return ( -
- {title &&

{title}

} +
+ {/*
*/} + {/* {title &&

{title}

} */} {isClient ? ( void; updateChart: ( dashboardId: string, chartId: string, chartOptions: ChartOptions, - datasets: Dataset[] + datasets: Dataset[], + gridPos?: GridPosition ) => void; removeChart: (dashboardId: string, chartId: string) => void; cloneChart: (dashboardId: string, chartId: string) => void; } -export const useChartStore = create((set, get) => ({ - charts: {}, +export const useChartStore = create()( + persist( + devtools((set, get) => ({ + charts: {}, + + // 차트 추가 (gridPos 기본값 추가) + addChart: ( + dashboardId, + chartOptions, + datasets, + gridPos = { x: 0, y: 0, w: 4, h: 4 } + ) => { + const newChartId = uuidv4(); + set((state) => ({ + charts: { + ...state.charts, + [dashboardId]: [ + ...(state.charts[dashboardId] || []), + { chartId: newChartId, chartOptions, datasets, gridPos }, + ], + }, + })); - addChart: (dashboardId, chartOptions, datasets) => { - const newChartId = uuidv4(); - set((state) => ({ - charts: { - ...state.charts, - [dashboardId]: [ - ...(state.charts[dashboardId] || []), - { chartId: newChartId, chartOptions, datasets }, - ], + useDashboardStore + .getState() + .addPanelToDashboard(dashboardId, newChartId, "chart", gridPos); }, - })); - useDashboardStore - .getState() - .dashboardChartMap[dashboardId]?.push(newChartId); - }, - updateChart: (dashboardId, chartId, chartOptions, datasets) => { - set((state) => ({ - charts: { - ...state.charts, - [dashboardId]: state.charts[dashboardId]?.map((chart) => - chart.chartId === chartId - ? { ...chart, chartOptions, datasets } - : chart - ), + // 차트 수정 (gridPos 필수 적용) + updateChart: (dashboardId, chartId, chartOptions, datasets, gridPos) => { + set((state) => ({ + charts: { + ...state.charts, + [dashboardId]: state.charts[dashboardId]?.map((chart) => + chart.chartId === chartId + ? { + ...chart, + chartOptions, + datasets, + gridPos: gridPos || chart.gridPos, + } + : chart + ), + }, + })); }, - })); - }, - removeChart: (dashboardId, chartId) => { - set((state) => ({ - charts: { - ...state.charts, - [dashboardId]: state.charts[dashboardId]?.filter( - (chart) => chart.chartId !== chartId - ), + // 차트 삭제 + removeChart: (dashboardId, chartId) => { + set((state) => ({ + charts: { + ...state.charts, + [dashboardId]: state.charts[dashboardId]?.filter( + (chart) => chart.chartId !== chartId + ), + }, + })); + + useDashboardStore + .getState() + .removeChartFromDashboard(dashboardId, chartId); }, - })); - useDashboardStore.getState().dashboardChartMap[dashboardId] = - useDashboardStore - .getState() - .dashboardChartMap[dashboardId]?.filter((id) => id !== chartId); - }, - cloneChart: (dashboardId, chartId) => { - const state = get(); - const chart = state.charts[dashboardId]?.find((c) => c.chartId === chartId); - if (!chart) return; + // 차트 복제 (gridPos 포함) + cloneChart: (dashboardId, chartId) => { + const state = get(); + const chart = state.charts[dashboardId]?.find( + (c) => c.chartId === chartId + ); + if (!chart) return; + + const newChartId = uuidv4(); + set((state) => ({ + charts: { + ...state.charts, + [dashboardId]: [ + ...state.charts[dashboardId], + { ...chart, chartId: newChartId, gridPos: { ...chart.gridPos } }, + ], + }, + })); - const newChartId = uuidv4(); - set((state) => ({ - charts: { - ...state.charts, - [dashboardId]: [ - ...state.charts[dashboardId], - { ...chart, chartId: newChartId }, - ], + useDashboardStore + .getState() + .addPanelToDashboard(dashboardId, newChartId, "chart"); }, - })); - useDashboardStore - .getState() - .dashboardChartMap[dashboardId]?.push(newChartId); - }, -})); + })), + { + name: "chart-storage", + } + ) +); diff --git a/src/app/store/useDashboardStore.ts b/src/app/store/useDashboardStore.ts index 1a7309c..5161a0d 100644 --- a/src/app/store/useDashboardStore.ts +++ b/src/app/store/useDashboardStore.ts @@ -1,95 +1,236 @@ import { create } from "zustand"; +import { persist, devtools } from "zustand/middleware"; interface Dashboard { id: string; label: string; description: string; + panels?: PanelLayout[]; +} + +export interface PanelLayout { + panelId: string; + type: "chart" | "widget" | "table"; + gridPos: { + x: number; + y: number; + w: number; + h: number; + }; } interface DashboardStore { - dashboardChartMap: Record; // 대시보드 ID → 차트 ID 배열 매핑 dashboardList: Dashboard[]; // 전체 대시보드 목록 + dashboardPanels: Record; // 대시보드 ID → 패널 리스트 addDashboard: (dashboard: Dashboard) => void; updateDashboard: ( dashboardId: string, newLabel: string, newDescription: string ) => void; - addChartToDashboard: (dashboardId: string, chartId: string) => void; + addPanelToDashboard: ( + dashboardId: string, + panelId: string, + type: "chart" | "widget" | "table", + gridPos?: { + x: number; + y: number; + w: number; + h: number; + } + ) => void; + saveDashboard: (dashboardId: string, layouts: PanelLayout[]) => void; removeChartFromDashboard: (dashboardId: string, chartId: string) => void; removeDashboard: (dashboardId: string) => void; + cloneDashboard: (dashboardId: string) => void; + // 새롭게 추가된 함수들 + getDashboardById: (dashboardId: string) => Dashboard | undefined; // 대시보드 조회 + createDashboard: (label: string, description: string) => void; // 대시보드 생성 + updateDashboardDetails: ( + dashboardId: string, + label: string, + description: string + ) => void; // 대시보드 수정 + cloneDashboardWithPanels: (dashboardId: string) => void; // 대시보드 복제 (패널 포함) } -export const useDashboardStore = create((set) => ({ - dashboardChartMap: {}, - dashboardList: [], - - // 대시보드 추가 - addDashboard: (dashboard) => { - set((state) => ({ - dashboardList: [...state.dashboardList, dashboard], - dashboardChartMap: { - ...state.dashboardChartMap, - [dashboard.id]: state.dashboardChartMap[dashboard.id] || [], // 기존 차트 매핑 유지 +export const useDashboardStore = create()( + persist( + devtools((set, get) => ({ + dashboardList: [], + dashboardPanels: {}, + + // 대시보드 추가 + addDashboard: (dashboard) => { + set((state) => ({ + dashboardList: [...state.dashboardList, dashboard], + dashboardPanels: { + ...state.dashboardPanels, + [dashboard.id]: [], // 초기 패널 배열 + }, + })); + }, + + // 대시보드 이름 및 설명 수정 + updateDashboard: (dashboardId, newLabel, newDescription) => { + set((state) => ({ + dashboardList: state.dashboardList.map((dashboard) => + dashboard.id === dashboardId + ? { ...dashboard, label: newLabel, description: newDescription } + : dashboard + ), + })); + }, + + // 패널 추가 (차트, 위젯, 테이블) + addPanelToDashboard: ( + dashboardId, + panelId, + type, + gridPos = { x: 0, y: 0, w: 4, h: 4 } + ) => { + set((state) => ({ + dashboardPanels: { + ...state.dashboardPanels, + [dashboardId]: [ + ...(state.dashboardPanels[dashboardId] || []), + { panelId, type, gridPos }, + ], + }, + })); + }, + + // 대시보드 저장 (패널 위치 정보 포함) + saveDashboard: (dashboardId, layouts) => { + set((state) => ({ + dashboardPanels: { + ...state.dashboardPanels, + [dashboardId]: layouts.map((layout) => ({ + panelId: layout.panelId, + type: layout.type, + gridPos: { + x: layout.gridPos?.x ?? 0, + y: layout.gridPos?.y ?? 0, + w: layout.gridPos?.w ?? 4, + h: layout.gridPos?.h ?? 4, + }, + })), + }, + })); + }, + + // 특정 차트 제거 + removeChartFromDashboard: (dashboardId, chartId) => { + set((state) => ({ + dashboardPanels: { + ...state.dashboardPanels, + [dashboardId]: state.dashboardPanels[dashboardId]?.filter( + (panel) => panel.panelId !== chartId + ), + }, + })); }, - })); - }, - - // 기존의 대시보드를 업데이트하는 함수 추가 (설명과 이름만 변경) - updateDashboard: (dashboardId, newLabel, newDescription) => { - set((state) => { - const existingCharts = state.dashboardChartMap[dashboardId] || []; // 기존 차트 유지 - - return { - dashboardList: state.dashboardList.map((dashboard) => - dashboard.id === dashboardId - ? { ...dashboard, label: newLabel, description: newDescription } - : dashboard - ), - dashboardChartMap: { - ...state.dashboardChartMap, - [dashboardId]: existingCharts, // 기존 차트 정보 유지 - }, - }; - }); - }, - - // 대시보드에 차트 추가 - addChartToDashboard: (dashboardId, chartId) => { - set((state) => ({ - dashboardChartMap: { - ...state.dashboardChartMap, - [dashboardId]: [ - ...(state.dashboardChartMap[dashboardId] || []), - chartId, - ], + + // 대시보드 삭제 + removeDashboard: (dashboardId) => { + set((state) => ({ + dashboardList: state.dashboardList.filter( + (dashboard) => dashboard.id !== dashboardId + ), + dashboardPanels: Object.keys(state.dashboardPanels) + .filter((id) => id !== dashboardId) + .reduce((acc, id) => { + acc[id] = state.dashboardPanels[id]; + return acc; + }, {} as Record), + })); + }, + + // 대시보드 복제 + cloneDashboard: (dashboardId) => { + const newDashboardId = crypto.randomUUID(); + const state = get(); + + set((state) => ({ + dashboardList: [ + ...state.dashboardList, + { + ...state.dashboardList.find((d) => d.id === dashboardId)!, + id: newDashboardId, + }, + ], + dashboardPanels: { + ...state.dashboardPanels, + [newDashboardId]: [...(state.dashboardPanels[dashboardId] || [])], + }, + })); + }, + + // 대시보드 조회 (ID로 대시보드 정보 조회) + getDashboardById: (dashboardId) => { + const state = get(); + return state.dashboardList.find( + (dashboard) => dashboard.id === dashboardId + ); + }, + + // 대시보드 생성 + createDashboard: (label, description) => { + const newDashboardId = crypto.randomUUID(); + set((state) => ({ + dashboardList: [ + ...state.dashboardList, + { id: newDashboardId, label, description }, + ], + dashboardPanels: { + ...state.dashboardPanels, + [newDashboardId]: [], + }, + })); + }, + + // 대시보드 수정 (이름 및 설명 수정) + updateDashboardDetails: (dashboardId, label, description) => { + set((state) => ({ + dashboardList: state.dashboardList.map((dashboard) => + dashboard.id === dashboardId + ? { ...dashboard, label, description } + : dashboard + ), + })); }, - })); - }, - - // 대시보드에서 특정 차트 제거 - removeChartFromDashboard: (dashboardId, chartId) => { - set((state) => ({ - dashboardChartMap: { - ...state.dashboardChartMap, - [dashboardId]: (state.dashboardChartMap[dashboardId] || []).filter( - (id) => id !== chartId - ), + + // 대시보드 복제 (패널 포함) + cloneDashboardWithPanels: (dashboardId: string) => { + const newDashboardId = crypto.randomUUID(); + const state = get(); + const dashboardToClone = state.dashboardList.find( + (d) => d.id === dashboardId + ); + + if (dashboardToClone) { + // 패널 복제 (옵션 및 위치 정보 포함) + const newPanels = dashboardToClone.panels?.map((panel) => ({ + ...panel, + panelId: crypto.randomUUID(), // 새로운 panelId 생성 + })); + + // 새 대시보드 생성 (패널 정보 포함) + const newDashboard = { + ...dashboardToClone, + id: newDashboardId, + label: `${dashboardToClone.label}_copy`, + panels: newPanels, + }; + + set((state) => ({ + dashboardList: [...state.dashboardList, newDashboard], + })); + } }, - })); - }, - - // 대시보드 삭제 (차트 매핑도 제거) - removeDashboard: (dashboardId) => { - set((state) => { - const updatedMap = { ...state.dashboardChartMap }; - delete updatedMap[dashboardId]; - return { - dashboardChartMap: updatedMap, - dashboardList: state.dashboardList.filter( - (dashboard) => dashboard.id !== dashboardId - ), - }; - }); - }, -})); + })), + { + name: "dashboard-storage", // persist 미들웨어 적용 (새로고침해도 유지) + } + ) +); diff --git a/src/app/store/useWidgetStore.ts b/src/app/store/useWidgetStore.ts index c9f4a57..a8efd30 100644 --- a/src/app/store/useWidgetStore.ts +++ b/src/app/store/useWidgetStore.ts @@ -1,115 +1,89 @@ import { create } from "zustand"; +import { persist, devtools } from "zustand/middleware"; import { v4 as uuidv4 } from "uuid"; import { useDashboardStore } from "./useDashboardStore"; import { WidgetOptions } from "../types/options"; -interface Widget { +interface GridPosition { + x: number; + y: number; + w: number; + h: number; +} + +export interface Widget { widgetId: string; widgetOptions: WidgetOptions; + gridPos: GridPosition; // gridPos를 필수 필드로 변경 } interface WidgetStore { widgets: Record; // 대시보드 ID → 위젯 배열 addWidget: ( dashboardId: string, - widgetOptions: Omit + widgetOptions: Omit, + gridPos?: GridPosition ) => void; updateWidget: ( dashboardId: string, widgetId: string, - widgetOptions: WidgetOptions + widgetOptions: WidgetOptions, + gridPos?: GridPosition ) => void; removeWidget: (dashboardId: string, widgetId: string) => void; cloneWidget: (dashboardId: string, widgetId: string) => void; } -export const useWidgetStore = create((set, get) => ({ - widgets: {}, - - // ✅ 위젯 추가 - addWidget: (dashboardId, widgetOptions) => { - const newWidgetId = uuidv4(); - const newWidget: Widget = { - widgetId: newWidgetId, - widgetOptions: { ...widgetOptions, widgetId: newWidgetId }, // widgetId 포함 - }; +export const useWidgetStore = create()( + persist( + devtools((set, get) => ({ + widgets: {}, - set((state) => ({ - widgets: { - ...state.widgets, - [dashboardId]: [...(state.widgets[dashboardId] || []), newWidget], - }, - })); + // 위젯 추가 (gridPos 기본값 추가) + addWidget: ( + dashboardId, + widgetOptions, + gridPos = { x: 0, y: 0, w: 4, h: 4 } + ) => { + const newWidgetId = uuidv4(); + const newWidget: Widget = { + widgetId: newWidgetId, + widgetOptions: { ...widgetOptions, widgetId: newWidgetId }, + gridPos, + }; - useDashboardStore - .getState() - .dashboardChartMap[dashboardId]?.push(newWidgetId); - }, + set((state) => ({ + widgets: { + ...state.widgets, + [dashboardId]: [...(state.widgets[dashboardId] || []), newWidget], + }, + })); - // ✅ 위젯 수정 - updateWidget: (dashboardId, widgetId, widgetOptions) => { - set((state) => ({ - widgets: { - ...state.widgets, - [dashboardId]: state.widgets[dashboardId]?.map((widget) => - widget.widgetId === widgetId ? { ...widget, widgetOptions } : widget - ), + useDashboardStore + .getState() + .addPanelToDashboard(dashboardId, newWidgetId, "widget", gridPos); }, - })); - }, - // ✅ 위젯 삭제 - removeWidget: (dashboardId, widgetId) => { - set((state) => ({ - widgets: { - ...state.widgets, - [dashboardId]: state.widgets[dashboardId]?.filter( - (widget) => widget.widgetId !== widgetId - ), + // 위젯 수정 (gridPos 필수 적용) + updateWidget: (dashboardId, widgetId, widgetOptions, gridPos) => { + set((state) => ({ + widgets: { + ...state.widgets, + [dashboardId]: state.widgets[dashboardId]?.map((widget) => + widget.widgetId === widgetId + ? { + ...widget, + widgetOptions, + gridPos: gridPos || widget.gridPos, + } + : widget + ), + }, + })); }, - })); - - useDashboardStore.getState().dashboardChartMap[dashboardId] = - useDashboardStore - .getState() - .dashboardChartMap[dashboardId]?.filter((id) => id !== widgetId); - }, - - // ✅ 위젯 복제 - cloneWidget: (newDashboardId, widgetId) => { - const newWidgetId = uuidv4(); // ✅ set() 밖에서 선언하여 사용 가능하도록 변경 - - set((state) => { - // 기존 위젯 찾기 - const originalWidget = Object.values(state.widgets) - .flat() - .find((w) => w.widgetId === widgetId); - - if (!originalWidget) return state; - - // 새로운 위젯 데이터 복제 - const clonedWidget: Widget = { - widgetId: newWidgetId, // ✅ 새로 생성한 ID 사용 - widgetOptions: { - ...originalWidget.widgetOptions, - widgetId: newWidgetId, // ✅ 새로운 ID 적용 - }, - }; - - return { - widgets: { - ...state.widgets, - [newDashboardId]: [ - ...(state.widgets[newDashboardId] || []), - clonedWidget, - ], // ✅ 새로운 대시보드 ID로 복제 - }, - }; - }); - - // ✅ 복제된 대시보드의 dashboardChartMap 업데이트 - useDashboardStore - .getState() - .dashboardChartMap[newDashboardId]?.push(newWidgetId); - }, -})); + })), + { + name: "widget-storage", + } + ) +); diff --git a/src/app/utils/convertToTable.ts b/src/app/utils/convertToTable.ts new file mode 100644 index 0000000..d41b4cf --- /dev/null +++ b/src/app/utils/convertToTable.ts @@ -0,0 +1,17 @@ +import { Dataset } from "../context/chartOptionContext"; + +export const convertToTable = (datasets: Dataset[]) => { + if (!datasets || datasets.length === 0) return { headers: [], rows: [] }; + + const headers = ["항목", ...datasets.map((dataset) => dataset.label)]; + + const rows = datasets[0].data.map((_, index) => ({ + name: `Point ${index + 1}`, + ...datasets.reduce((acc, dataset) => { + acc[dataset.label] = dataset.data[index]; + return acc; + }, {} as Record), + })); + + return { headers, rows }; +};