diff --git a/apps/cm/src/pages/dashboard/lib/hooks/use-panel-order.ts b/apps/cm/src/pages/dashboard/lib/hooks/use-panel-order.ts index 8842e43db..412774210 100644 --- a/apps/cm/src/pages/dashboard/lib/hooks/use-panel-order.ts +++ b/apps/cm/src/pages/dashboard/lib/hooks/use-panel-order.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { arrayMove } from "@dnd-kit/sortable"; import type { DragEndEvent } from "@dnd-kit/core"; import type { PanelItem } from "../../model/types"; @@ -12,25 +12,28 @@ export function usePanelOrder({ initialPanels, storageKey, }: UsePanelOrderProps) { - const [panels, setPanels] = useState(() => { - if (typeof window !== "undefined") { - const saved = localStorage.getItem(storageKey); - if (saved) { - try { - const savedIds = JSON.parse(saved); - const restoredPanels = savedIds - .map((id: string) => initialPanels.find((p) => p.id === id)) - .filter(Boolean); - if (restoredPanels.length === initialPanels.length) { - return restoredPanels; - } - } catch { - return initialPanels; + const [panels, setPanels] = useState(initialPanels); + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + if (isInitialized) return; + + const saved = localStorage.getItem(storageKey); + if (saved) { + try { + const savedIds = JSON.parse(saved); + const restoredPanels = savedIds + .map((id: string) => initialPanels.find((p) => p.id === id)) + .filter(Boolean); + if (restoredPanels.length === initialPanels.length) { + setPanels(restoredPanels); } + } catch { + setPanels(initialPanels); } } - return initialPanels; - }); + setIsInitialized(true); + }, [storageKey]); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; diff --git a/apps/cm/src/pages/dashboard/ui/banner-list.tsx b/apps/cm/src/pages/dashboard/ui/banner-list.tsx index d3ae54610..6404faa85 100644 --- a/apps/cm/src/pages/dashboard/ui/banner-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/banner-list.tsx @@ -9,6 +9,7 @@ import { type Banner, } from "../api/banner-service"; import { getCookie } from "@/shared/lib"; +import { useDragHandle } from "./sortable-panel"; import { Carousel, @@ -25,6 +26,7 @@ type BannerImageProps = { }; const BannerList: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const [siteId, setSiteId] = useState(""); useEffect(() => { @@ -157,6 +159,12 @@ const BannerList: React.FC = () => { return (
+ {/* 드래그 핸들 - 배너 위에 오버레이 */} +
(null); const [error, setError] = useState(""); + const [isMounted, setIsMounted] = useState(false); const router = useRouter(); // dnd-kit 센서 설정 @@ -70,6 +71,11 @@ function DashboardPage() { storageKey: "dashboard-bottom-panels-order", }); + // 클라이언트에서만 드래그 기능 활성화 (hydration 문제 방지) + useEffect(() => { + setIsMounted(true); + }, []); + useEffect(() => { const fetchUserInfo = async () => { console.log("fetchUserInfo called"); @@ -134,28 +140,38 @@ function DashboardPage() {
{/* 좌측: 드래그 가능한 6개 패널 (2x3 그리드, 화면의 50%) */}
- - p.id)} - strategy={rectSortingStrategy} + {isMounted ? ( + -
- {topPanels.map((panel) => ( - - {panel.component} - - ))} -
-
-
+ p.id)} + strategy={rectSortingStrategy} + > +
+ {topPanels.map((panel) => ( + + {panel.component} + + ))} +
+
+ + ) : ( +
+ {topPanels.map((panel) => ( +
+ {panel.component} +
+ ))} +
+ )}
{/* 우측: 고정된 지도와 사용자 정보 (화면의 50%) */} @@ -231,28 +247,38 @@ function DashboardPage() {
{/* 두번째 라인 - 드래그 가능한 4개 패널 */} - - p.id)} - strategy={rectSortingStrategy} + {isMounted ? ( + -
- {bottomPanels.map((panel) => ( - -
{panel.component}
-
- ))} -
-
-
+ p.id)} + strategy={rectSortingStrategy} + > +
+ {bottomPanels.map((panel) => ( + +
{panel.component}
+
+ ))} +
+
+ + ) : ( +
+ {bottomPanels.map((panel) => ( +
+
{panel.component}
+
+ ))} +
+ )}
); diff --git a/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx b/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx index bae89e338..74e10afea 100644 --- a/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/datastatus-list.tsx @@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; import { getDataStatusList, type DataStatus } from "../api/datastatus-service"; import type { PageableList } from "@/shared/model"; +import { useDragHandle } from "./sortable-panel"; import { Carousel, @@ -35,6 +36,7 @@ const getRegionInfo = (regionName: string) => { }; const DataStatusList: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const plugin = useRef( Autoplay({ delay: 3000, stopOnInteraction: true }) ); @@ -106,7 +108,11 @@ const DataStatusList: React.FC = () => { return ( <> -
+

자료현황

diff --git a/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx b/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx index 2a0264e43..5db22db6e 100644 --- a/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx +++ b/apps/cm/src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx @@ -9,8 +9,10 @@ import { Doughnut } from "react-chartjs-2"; import type { Chart as ChartJS } from "chart.js"; // import type { mapOperFcltItem, mapBasinData } from "../model/fo-interface"; import { OperationApi, operFcltStore, basinStore } from "../api/fo-service"; +import { useDragHandle } from "./sortable-panel"; const FoFcltCntRt: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const [year, setYear] = useState(null); const { selectedBasin, setSelectedBasin } = useStore(basinStore); const operFcltStatus = useStore(operFcltStore, (state) => state.operFcltStatus); @@ -57,9 +59,18 @@ const FoFcltCntRt: React.FC = () => { return (
-
+

수계별 시설운영 개소수 및 가동률

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx b/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx index 4c01fbdd0..6b416c5ed 100644 --- a/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx +++ b/apps/cm/src/pages/dashboard/ui/fo-wqlt-Infl-effl.tsx @@ -14,6 +14,7 @@ import { import type { OperWqltContent } from "../model/fo-interface"; import { OperationApi, basinStore } from "../api/fo-service"; import { defaultInitOperWqltState } from "../api/fo-service"; +import { useDragHandle } from "./sortable-panel"; // Chart.js 등록 ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); @@ -21,6 +22,7 @@ ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, T type WaterQualityType = 'BOD' | 'TOC' | 'SS' | 'TP' | 'TN'; const FoWqltInflEffl: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const { selectedBasin, setSelectedBasin } = useStore(basinStore); const [selectedTab, setSelectedTab] = useState('BOD'); const [wqltData, setWqltData] = useState([]); @@ -195,9 +197,18 @@ const FoWqltInflEffl: React.FC = () => { return (
-
+

수계별 유입ㆍ방류수질 분석

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/notice-list.tsx b/apps/cm/src/pages/dashboard/ui/notice-list.tsx index 86e21a4a3..b69e5ae0d 100644 --- a/apps/cm/src/pages/dashboard/ui/notice-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/notice-list.tsx @@ -10,9 +10,11 @@ import { noticePostTableColumns, } from "../model/table-columns"; import { Button } from "@common/components"; +import { useDragHandle } from "./sortable-panel"; const NoticeList: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const router = useRouter(); const noticePostQuery = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], @@ -51,9 +53,18 @@ const NoticeList: React.FC = () => { return (
-
+

공지사항

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
{noticePostTableComponent}
diff --git a/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx b/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx index edcc19b97..55d9e6abb 100644 --- a/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx +++ b/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx @@ -8,6 +8,7 @@ import { Line } from 'react-chartjs-2'; import { Button } from "@common/components"; import type { ReuseStatusContent } from "../model/fo-interface"; import { OperationApi, reuseStore } from "../api/fo-service"; +import { useDragHandle } from "./sortable-panel"; import { Chart as ChartJS, CategoryScale, @@ -36,6 +37,7 @@ ChartJS.register( const ReuseChart: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const [activeIndex, setActiveIndex] = useState(0); const intervalRef = useRef(null); const reuseStatusData = useStore( @@ -183,9 +185,18 @@ const ReuseChart: React.FC = () => { return (
-
+

{chartItems[activeIndex]?.title}

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/reuse-status.tsx b/apps/cm/src/pages/dashboard/ui/reuse-status.tsx index d1e7c4c15..6c596eba9 100644 --- a/apps/cm/src/pages/dashboard/ui/reuse-status.tsx +++ b/apps/cm/src/pages/dashboard/ui/reuse-status.tsx @@ -7,8 +7,10 @@ import { useStore } from "zustand"; import { Button } from "@common/components"; import type { ReuseStatusContent } from "../model/fo-interface"; import { OperationApi, reuseStore} from "../api/fo-service"; +import { useDragHandle } from "./sortable-panel"; const ReuseStatus: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); // const [reuseStatusData, setReuseStatusData] = useState([]); const reuseStatusData = useStore( reuseStore, @@ -32,9 +34,18 @@ const ReuseStatus: React.FC = () => { return (
-
+

물재이용 현황 (재이용량, m³)

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx b/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx index b1a34139c..fa502e7c5 100644 --- a/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx +++ b/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx @@ -17,12 +17,14 @@ import { Bar } from "react-chartjs-2"; import { Button } from "@common/components"; import type { PplLengthContent, ChartDataSet, OldPplChartData } from "../model/sn-interface"; import { SewerageApi, useOldPplStore,useMapOldPplStore, transformChartDataToMapData } from "../api/sn-service"; +import { useDragHandle } from "./sortable-panel"; // ChartJS 컴포넌트 등록 ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); export default function SnOldPpl() { + const { dragHandleProps, isDragging } = useDragHandle(); const [oldPplLenData, setOldPplLenData] = useState([]); const {chartData, setChartData} = useOldPplStore(); const {setMapData} = useMapOldPplStore(); @@ -170,9 +172,18 @@ export default function SnOldPpl() { return (
-
+

노후 하수관로 현황

- +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx b/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx index 014f592c4..022430da0 100644 --- a/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx +++ b/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx @@ -17,6 +17,7 @@ import { Bar } from "react-chartjs-2"; import { Button } from "@common/components"; import type { PplLengthContent } from "../model/sn-interface"; import { SewerageApi,useUsePplStore } from "../api/sn-service"; +import { useDragHandle } from "./sortable-panel"; interface ChartDataSet { label: string; @@ -29,6 +30,7 @@ interface ChartDataSet { ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); export default function SnPplStatus() { + const { dragHandleProps, isDragging } = useDragHandle(); const {chartData, setChartData} = useUsePplStore(); const [usgPplLenData, setUsgPplLenData] = useState([]); @@ -157,10 +159,18 @@ export default function SnPplStatus() { }; return (
-
+

오수관로 및 우수관로 현황

- - +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
diff --git a/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx b/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx index ce9d2c949..1fb891980 100644 --- a/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx +++ b/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx @@ -1,5 +1,6 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; +import { createContext, useContext } from "react"; interface SortablePanelProps { id: string; @@ -7,6 +8,13 @@ interface SortablePanelProps { className?: string; } +interface DragHandleContextValue { + dragHandleProps: React.HTMLAttributes; + isDragging: boolean; +} + +const DragHandleContext = createContext(null); + export function SortablePanel({ id, children, className }: SortablePanelProps) { const { attributes, @@ -21,18 +29,36 @@ export function SortablePanel({ id, children, className }: SortablePanelProps) { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, - cursor: isDragging ? "grabbing" : "grab", + }; + + const contextValue: DragHandleContextValue = { + dragHandleProps: { ...attributes, ...listeners }, + isDragging, }; return ( -
- {children} -
+ +
+ {children} +
+
); } + +export function useDragHandle() { + const context = useContext(DragHandleContext); + + // Context가 없을 때 (서버 렌더링 또는 isMounted = false) 기본값 반환 + if (!context) { + return { + dragHandleProps: {}, + isDragging: false, + }; + } + + return context; +} diff --git a/apps/cm/src/pages/dashboard/ui/support-list.tsx b/apps/cm/src/pages/dashboard/ui/support-list.tsx index f62cce9b1..f37dad7eb 100644 --- a/apps/cm/src/pages/dashboard/ui/support-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/support-list.tsx @@ -7,8 +7,10 @@ import { useDataTable } from "@/shared/lib"; import type { PageableList } from "@/shared/model"; import { supportTableColumns } from "../model/support-columns"; import { getSupportList, type Support } from "../api/support-service"; +import { useDragHandle } from "./sortable-panel"; const SupportList: React.FC = () => { + const { dragHandleProps, isDragging } = useDragHandle(); const supportQuery = useQuery>({ queryKey: [ENDPOINT.IC_SERVICE.GROUPING_STATISTICS], queryFn: async () => { @@ -29,7 +31,11 @@ const SupportList: React.FC = () => { return (
-
+

지원현황

{supportTableComponent}