From 8491d0243a9d19ffe0e203ffc183fdead0105ec6 Mon Sep 17 00:00:00 2001 From: bicochan Date: Wed, 4 Feb 2026 15:22:17 +0900 Subject: [PATCH 001/126] =?UTF-8?q?[cm]=20feat:=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=20dnd=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/package.json | 3 + .../dashboard/lib/hooks/use-dnd-sensors.ts | 16 ++ .../dashboard/lib/hooks/use-panel-order.ts | 56 ++++ .../pages/dashboard/model/panel-config.tsx | 71 ++++++ apps/cm/src/pages/dashboard/model/types.ts | 7 + .../src/pages/dashboard/ui/dashboard-page.tsx | 240 +++++++++++------- .../src/pages/dashboard/ui/sortable-panel.tsx | 38 +++ pnpm-lock.yaml | 61 ++++- 8 files changed, 394 insertions(+), 98 deletions(-) create mode 100644 apps/cm/src/pages/dashboard/lib/hooks/use-dnd-sensors.ts create mode 100644 apps/cm/src/pages/dashboard/lib/hooks/use-panel-order.ts create mode 100644 apps/cm/src/pages/dashboard/model/panel-config.tsx create mode 100644 apps/cm/src/pages/dashboard/model/types.ts create mode 100644 apps/cm/src/pages/dashboard/ui/sortable-panel.tsx diff --git a/apps/cm/package.json b/apps/cm/package.json index 9d4e1da8f..1b40efc69 100644 --- a/apps/cm/package.json +++ b/apps/cm/package.json @@ -12,6 +12,9 @@ "@common/assets": "workspace:*", "@common/components": "workspace:*", "@common/config": "workspace:*", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/noto-sans-kr": "^5.2.5", "@frontend-opensource/use-react-hooks": "^1.1.4", "@fullcalendar/core": "^6.1.17", diff --git a/apps/cm/src/pages/dashboard/lib/hooks/use-dnd-sensors.ts b/apps/cm/src/pages/dashboard/lib/hooks/use-dnd-sensors.ts new file mode 100644 index 000000000..953bc874f --- /dev/null +++ b/apps/cm/src/pages/dashboard/lib/hooks/use-dnd-sensors.ts @@ -0,0 +1,16 @@ +import { + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; + +export function useDndSensors() { + return useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); +} 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 new file mode 100644 index 000000000..8842e43db --- /dev/null +++ b/apps/cm/src/pages/dashboard/lib/hooks/use-panel-order.ts @@ -0,0 +1,56 @@ +import { useState } from "react"; +import { arrayMove } from "@dnd-kit/sortable"; +import type { DragEndEvent } from "@dnd-kit/core"; +import type { PanelItem } from "../../model/types"; + +interface UsePanelOrderProps { + initialPanels: PanelItem[]; + storageKey: string; +} + +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; + } + } + } + return initialPanels; + }); + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (over && active.id !== over.id) { + setPanels((items) => { + const oldIndex = items.findIndex((item) => item.id === active.id); + const newIndex = items.findIndex((item) => item.id === over.id); + const newItems = arrayMove(items, oldIndex, newIndex); + + // localStorage에 저장 + localStorage.setItem( + storageKey, + JSON.stringify(newItems.map((item) => item.id)), + ); + + return newItems; + }); + } + }; + + return { panels, handleDragEnd }; +} diff --git a/apps/cm/src/pages/dashboard/model/panel-config.tsx b/apps/cm/src/pages/dashboard/model/panel-config.tsx new file mode 100644 index 000000000..e995e41c6 --- /dev/null +++ b/apps/cm/src/pages/dashboard/model/panel-config.tsx @@ -0,0 +1,71 @@ +import FoFcltCntRt from "@/pages/dashboard/ui/fo-fclt-cnt-rt"; +import SnPplStatus from "@/pages/dashboard/ui/sn-ppl-status"; +import ReuseChart from "@/pages/dashboard/ui/reuse-chart"; +import FoWqltInflEffl from "@/pages/dashboard/ui/fo-wqlt-Infl-effl"; +import SnOldPpl from "@/pages/dashboard/ui/sn-old-ppl"; +import ReuseStatus from "@/pages/dashboard/ui/reuse-status"; +import SupportList from "@/pages/dashboard/ui/support-list"; +import DataStatusList from "@/pages/dashboard/ui/datastatus-list"; +import NoticeList from "@/pages/dashboard/ui/notice-list"; +import BannerList from "@/pages/dashboard/ui/banner-list"; +import type { PanelItem } from "./types"; + +export function getInitialTopPanels(): PanelItem[] { + return [ + { + id: "panel-1", + component: , + className: "", + }, + { + id: "panel-2", + component: , + className: "", + }, + { + id: "panel-3", + component: , + className: "", + }, + { + id: "panel-4", + component: , + className: "", + }, + { + id: "panel-5", + component: , + className: "", + }, + { + id: "panel-6", + component: , + className: "", + }, + ]; +} + +export function getInitialBottomPanels(): PanelItem[] { + return [ + { + id: "panel-7", + component: , + className: "", + }, + { + id: "panel-8", + component: , + className: "", + }, + { + id: "panel-9", + component: , + className: "", + }, + { + id: "panel-10", + component: , + className: "", + }, + ]; +} diff --git a/apps/cm/src/pages/dashboard/model/types.ts b/apps/cm/src/pages/dashboard/model/types.ts new file mode 100644 index 000000000..8cd41a37e --- /dev/null +++ b/apps/cm/src/pages/dashboard/model/types.ts @@ -0,0 +1,7 @@ +export interface PanelItem { + id: string; + component: React.ReactNode; + className?: string; +} + +export type PanelType = "top" | "bottom"; diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index 5f9ce3440..41e855c01 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -6,6 +6,8 @@ import { tokenManager } from "@/entities/auth/lib/token-manager"; import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import { KoreaBasinMap } from "../lib/korea-basin-map"; +import { useDndSensors } from "../lib/hooks/use-dnd-sensors"; +import { usePanelOrder } from "../lib/hooks/use-panel-order"; import { Chart as ChartJS, CategoryScale, @@ -18,18 +20,15 @@ import { Tooltip, ArcElement, } from "chart.js"; -import SnPplStatus from "./sn-ppl-status"; -import SnOldPpl from "./sn-old-ppl"; -import FoFcltCntRt from "./fo-fclt-cnt-rt"; -import FoWqltInflEffl from "./fo-wqlt-Infl-effl"; -import ReuseChart from "./reuse-chart"; -import ReuseStatus from "./reuse-status"; -import NoticeList from "./notice-list"; -import BannerList from "./banner-list"; -import SupportList from "./support-list"; -import DataStatusList from "./datastatus-list"; +import { DndContext, closestCenter } from "@dnd-kit/core"; +import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable"; +import { + getInitialTopPanels, + getInitialBottomPanels, +} from "../model/panel-config"; import MyPostList from "./mypost-list"; import ScheduleList from "./schedule-list"; +import { SortablePanel } from "./sortable-panel"; // Chart.js 등록 ChartJS.register( @@ -41,7 +40,7 @@ ChartJS.register( Filler, Legend, Tooltip, - ArcElement + ArcElement, ); interface UserInfo { @@ -54,7 +53,23 @@ interface UserInfo { function DashboardPage() { const [userInfo, setUserInfo] = useState(null); const [error, setError] = useState(""); - const router = useRouter(); + const router = useRouter(); + + // dnd-kit 센서 설정 + const sensors = useDndSensors(); + + // 패널 순서 관리 + const { panels: topPanels, handleDragEnd: handleTopDragEnd } = usePanelOrder({ + initialPanels: getInitialTopPanels(), + storageKey: "dashboard-top-panels-order", + }); + + const { panels: bottomPanels, handleDragEnd: handleBottomDragEnd } = + usePanelOrder({ + initialPanels: getInitialBottomPanels(), + storageKey: "dashboard-bottom-panels-order", + }); + useEffect(() => { const fetchUserInfo = async () => { console.log("fetchUserInfo called"); @@ -115,96 +130,129 @@ function DashboardPage() { return (
- {/* 첫번째 라인 */} -
- {/* 첫번쨰 열 */} -
- {/* 전국 하수처리시설 개소수 및 가동률 */} - - - {/* 오수관로 및 우수관로 현황 */} - - - {/* 하수처리수 재이용 (m³) */} - -
- - {/* 두번쨰 열 */} -
- {/* 수계별 유입ㆍ방류수질 분석 */} - - - {/* 노후하수관로현황 */} - - - {/* 물재이용 현황 (재이용량, m³) */} - -
- - {/* 지도영역 */} -
- -
- - {/* 사용자 정보 카드 */} -
- {error &&
{error}
} - {userInfo ? ( -
-
-
- 프로필 { - const target = e.target as HTMLImageElement; - target.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='18' fill='%23e1e5e9'/%3E%3Ctext x='20' y='26' font-size='20' text-anchor='middle' fill='%23999'%3E%3F%3C/text%3E%3C/svg%3E"; - }} - /> + {/* 첫번째 라인 - 좌측 6개 패널(2x3) + 우측 지도/사용자 정보 */} +
+ {/* 좌측: 드래그 가능한 6개 패널 (2x3 그리드, 화면의 50%) */} +
+ + p.id)} + strategy={rectSortingStrategy} + > +
+ {topPanels.map((panel) => ( + + {panel.component} + + ))}
-
-
{userInfo.userName || "-"}
-
온라인
-
- 2FA 활성화:{" "} - {userInfo.twoFactorEnabled ? "예" : "아니오"} + + +
+ + {/* 우측: 고정된 지도와 사용자 정보 (화면의 50%) */} +
+ {/* 지도영역 - 고정 (드래그 불가) */} +
+ +
+ + {/* 사용자 정보 카드 - 고정 (드래그 불가) */} +
+ {error &&
{error}
} + {userInfo ? ( +
+
+
+ 프로필 { + const target = e.target as HTMLImageElement; + target.src = + "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Ccircle cx='20' cy='20' r='18' fill='%23e1e5e9'/%3E%3Ctext x='20' y='26' font-size='20' text-anchor='middle' fill='%23999'%3E%3F%3C/text%3E%3C/svg%3E"; + }} + /> +
+
+
+ {userInfo.userName || "-"} +
+
온라인
+
+ 2FA 활성화:{" "} + {userInfo.twoFactorEnabled ? "예" : "아니오"} +
+
+
+
+ +
-
-
- - -
+ ) : ( + !error &&
사용자 정보를 불러오는 중...
+ )} + + + +
- ) : ( - !error &&
사용자 정보를 불러오는 중...
- )} - - - - -
-
- - {/* 두번째 라인 */} -
-
- -
-
- -
-
- -
- - {/* 배너 영역 */} -
-
+ + {/* 두번째 라인 - 드래그 가능한 4개 패널 */} + + p.id)} + strategy={rectSortingStrategy} + > +
+ {bottomPanels.map((panel) => ( + +
{panel.component}
+
+ ))} +
+
+
); diff --git a/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx b/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx new file mode 100644 index 000000000..ce9d2c949 --- /dev/null +++ b/apps/cm/src/pages/dashboard/ui/sortable-panel.tsx @@ -0,0 +1,38 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +interface SortablePanelProps { + id: string; + children: React.ReactNode; + className?: string; +} + +export function SortablePanel({ id, children, className }: SortablePanelProps) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + cursor: isDragging ? "grabbing" : "grab", + }; + + return ( +
+ {children} +
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a17a827c5..9d629f1e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,15 @@ importers: '@common/config': specifier: workspace:* version: link:../../packages/common/config + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.2.3) '@fontsource-variable/noto-sans-kr': specifier: ^5.2.5 version: 5.2.9 @@ -1735,6 +1744,28 @@ packages: resolution: {integrity: sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==} engines: {node: '>=18'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: ^19.2.1 + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: ^19.2.1 + react-dom: ^19.2.1 + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: ^19.2.1 + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: ^19.2.1 + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -4999,6 +5030,7 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: @@ -7268,6 +7300,31 @@ snapshots: gonzales-pe: 4.3.0 node-source-walk: 7.0.1 + '@dnd-kit/accessibility@3.1.1(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -10388,7 +10445,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -10439,7 +10496,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 From c9b2ef35b7b01cd885a25b54e6d3103ea3374bf5 Mon Sep 17 00:00:00 2001 From: bicochan Date: Thu, 5 Feb 2026 09:24:28 +0900 Subject: [PATCH 002/126] =?UTF-8?q?[cm]=20fix:=20hydration=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cm/src/pages/dashboard/ui/banner-list.tsx | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/apps/cm/src/pages/dashboard/ui/banner-list.tsx b/apps/cm/src/pages/dashboard/ui/banner-list.tsx index 865540672..d3ae54610 100644 --- a/apps/cm/src/pages/dashboard/ui/banner-list.tsx +++ b/apps/cm/src/pages/dashboard/ui/banner-list.tsx @@ -1,9 +1,13 @@ "use client"; -import { useRef } from "react"; +import { useRef, useState, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; -import { getImageUrlByAtflCd, getBannerList, type Banner } from "../api/banner-service"; +import { + getImageUrlByAtflCd, + getBannerList, + type Banner, +} from "../api/banner-service"; import { getCookie } from "@/shared/lib"; import { @@ -21,7 +25,12 @@ type BannerImageProps = { }; const BannerList: React.FC = () => { - const siteId = getCookie("site-id-key") ?? ""; + const [siteId, setSiteId] = useState(""); + + useEffect(() => { + const id = getCookie("site-id-key") ?? ""; + setSiteId(id); + }, []); const BannerImage = ({ atflCd, lnkgUrlAddr }: BannerImageProps) => { const { data: imageUrl, isLoading } = useQuery({ @@ -91,6 +100,19 @@ const BannerList: React.FC = () => { return maybe.status ?? maybe.response?.status; }; + // siteId가 없을 때 + if (!siteId) { + return ( +
+
+
+ 사이트 ID가 없습니다 +
+
+
+ ); + } + // 401 에러 처리 if (bannerError) { const is401Error = @@ -133,21 +155,8 @@ const BannerList: React.FC = () => { ); } - // siteId가 없을 때 - if (!siteId) { - return ( -
-
-
- 사이트 ID가 없습니다 -
-
-
- ); - } - return ( -
+
Date: Fri, 6 Feb 2026 16:33:49 +0900 Subject: [PATCH 003/126] =?UTF-8?q?[cm]=20feat:=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=ED=8C=A8=EB=84=90=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=EC=97=90=20=EB=93=9C=EB=9E=98=EA=B7=B8=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/lib/hooks/use-panel-order.ts | 37 +++--- .../cm/src/pages/dashboard/ui/banner-list.tsx | 8 ++ .../src/pages/dashboard/ui/dashboard-page.tsx | 110 +++++++++++------- .../pages/dashboard/ui/datastatus-list.tsx | 8 +- .../src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx | 15 ++- .../pages/dashboard/ui/fo-wqlt-Infl-effl.tsx | 15 ++- .../cm/src/pages/dashboard/ui/notice-list.tsx | 15 ++- .../cm/src/pages/dashboard/ui/reuse-chart.tsx | 15 ++- .../src/pages/dashboard/ui/reuse-status.tsx | 15 ++- apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx | 15 ++- .../src/pages/dashboard/ui/sn-ppl-status.tsx | 16 ++- .../src/pages/dashboard/ui/sortable-panel.tsx | 46 ++++++-- .../src/pages/dashboard/ui/support-list.tsx | 8 +- 13 files changed, 237 insertions(+), 86 deletions(-) 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} From 80036ed876c5e0c475a435ea8e102f2626aadd3f Mon Sep 17 00:00:00 2001 From: rsone001 Date: Tue, 17 Mar 2026 10:03:35 +0900 Subject: [PATCH 004/126] =?UTF-8?q?feat:=20=ED=8E=8C=ED=94=84=EC=9E=A5=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/review/PumpingStationRvw.tsx | 2 +- .../column/review/PumpingStationRvwColumn.ts | 8 ++-- .../util/column/review/SewagePipeRvwColumn.ts | 47 +++++++++++-------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx index 346a9ef33..fc5d8c2c6 100644 --- a/apps/st/src/components/statistics/review/PumpingStationRvw.tsx +++ b/apps/st/src/components/statistics/review/PumpingStationRvw.tsx @@ -85,7 +85,7 @@ const PumpingStationRvw = () => { return (
- +
diff --git a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts index 3c22c517c..54dbfcedd 100644 --- a/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts +++ b/apps/st/src/components/util/column/review/PumpingStationRvwColumn.ts @@ -88,10 +88,10 @@ export const rawPumpingStationInfoList = { ] }, { name: 'pumpstNm', fieldName: 'pumpstNm', header: { text: '펌프장명' }, mergeCells: true }, - { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } }, // bscFctNm -> swtrmtFcltNm - { name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분\n(합류/분류)' } }, // pumpCls -> pumpSeNm - { name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '위치' } }, // addr -> impdFcltPstnCn - { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동년월일' } }, // rnDt -> oprtngStrtYmd + { name: 'swtrmtFcltNm', fieldName: 'swtrmtFcltNm', header: { text: '처리장명' } }, + { name: 'pumpSeNm', fieldName: 'pumpSeNm', header: { text: '구분\n(합류/분류)' } }, + { name: 'impdFcltPstnCn', fieldName: 'impdFcltPstnCn', header: { text: '위치' } }, + { name: 'oprtngStrtYmd', fieldName: 'oprtngStrtYmd', header: { text: '가동년월일' } }, { name: 'planMltvArea', fieldName: 'planMltvArea', header: { text: '계획배수면적\n(km2)' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' diff --git a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts index ae445b22d..1adb4eadf 100644 --- a/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts +++ b/apps/st/src/components/util/column/review/SewagePipeRvwColumn.ts @@ -1,8 +1,13 @@ import { ValueType } from "realgrid"; -const baseYear = new Date().getFullYear(); +import dayjs from 'dayjs'; import { footerIntArrayByDivision, footerDecimal3ArrayByDivision, footerAvgDecimalArrayByDivision } from "@/components/util/grid/footerSummary"; import type { GridView } from "@/components/type/DataGrid.type"; + +const currentYear = dayjs().year() - 1; +const startYear = 1996; +const totalCount = currentYear - startYear + 1 + 1; + // 하수도 통계 관리 02. 하수관로 const rawList = [ { @@ -248,7 +253,7 @@ const rawList = [ } ] }, - { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' }, footer: footerIntArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: 'prcsFcltCpct', fieldName: 'prcsFcltCpct', header: { text: '하수처리\n시설용량\n(㎥/일)' },valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'prcsZoneArea', fieldName: 'prcsZoneArea', header: { text: '합계' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, { name: 'cbsysPrcsZoneArea', fieldName: 'cbsysPrcsZoneArea', header: { text: '합류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, { name: 'sprsysPrcsZoneArea', fieldName: 'sprsysPrcsZoneArea', header: { text: '분류식' }, footer: footerDecimal3ArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.000' }, @@ -273,19 +278,19 @@ const rawList = [ { name: 'addrGroup', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + { column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] }, 'sggNm', 'admdstNm' ] }, 'swtrmtFcltNm','prcsFcltCpct', 'displayYear', - { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] }, // name 변경됨 + { name: 'areaGroup', header: { text: '처리구역면적(㎢)' }, items: ['prcsZoneArea', 'cbsysPrcsZoneArea', 'sprsysPrcsZoneArea'] }, 'sprsysVartnRt', - { name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] }, // name 변경됨 + { name: 'drainGroup', header: { text: '배수설비(개소)' }, items: ['mltvEqpmCnt', 'cbsysMltvEqpmCnt', 'sprsysMltvEqpmCnt'] }, { name: 'pipeGroup', header: { text: '하수관로' }, items: [ 'wholFcltPrlg', - { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] }, // name 변경됨 + { name: 'trunkGroup', header: { text: '차집/오수간선(m)' }, items: ['wholIcptgAtlfcPrlg', 'icptgPrlg', 'atlfcsPrlg'] }, { name: 'siteGroup', header: { text: '토실/토구(개소)' }, items: [ @@ -383,18 +388,18 @@ const rawList = [ { name: 'lyngPrlgSum', fieldName: 'lyngPrlg', header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, { name: 'dmltnPrlgSum', fieldName: 'dmltnPrlg', header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - /* 동적 연도 컬럼 (1996년이전[yy30] ~ 2025년[yy1]) */ - ...Array.from({ length: 30 }, (_, i) => { - const dtoNum = 30 - i; // 30, 29, ..., 1 + + ...Array.from({ length: totalCount }, (_, i) => { + const dtoNum = totalCount - i; - const wholField = `yy${dtoNum}PrlgWhol`; + const wholField = `yy${dtoNum}PrlgWhol`; const lyngField = `yy${dtoNum}LyngPrlg`; const dmltnField = `yy${dtoNum}DmltnPrlg`; - + return [ - { name: `${wholField}_col`, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `${lyngField}_col`, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, - { name: `${dmltnField}_col`, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } + { name: wholField, fieldName: wholField, header: { text: '계' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: lyngField, fieldName: lyngField, header: { text: '매설연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' }, + { name: dmltnField, fieldName: dmltnField, header: { text: '철거연장(m)' }, footer: footerAvgDecimalArrayByDivision, valueType: ValueType.NUMBER, numberFormat: '#,##0.0' } ]; }).flat(), @@ -404,15 +409,17 @@ const rawList = [ 'displayYear', { name: 'summaryGroup', header: { text: '총계' }, items: ['pipeByPrstWholSum','lyngPrlgSum', 'dmltnPrlgSum'] }, - ...Array.from({ length: 30 }, (_, i) => { - const dtoNum = 30 - i; - const yearLabel = i === 0 ? '1996년이전' : `${1995 + i}년`; - const wholField = `yy${dtoNum}PrlgWhol`; + ...Array.from({ length: totalCount }, (_, i) => { + + const dtoNum = totalCount - i; + const yearLabel = i === 0 + ? '1996년이전' + : `${startYear + (i - 1)}년`; return { name: `group_${yearLabel}`, header: { text: yearLabel }, - items: [`${wholField}_col`, `yy${dtoNum}LyngPrlg_col`, `yy${dtoNum}DmltnPrlg_col`] + items: [`yy${dtoNum}PrlgWhol`, `yy${dtoNum}LyngPrlg`, `yy${dtoNum}DmltnPrlg`] }; }), 'rmrk' @@ -512,7 +519,7 @@ const rawList = [ name: 'addrGroup', header: { text: '행정구역' }, items: [ - { column: 'ctpvNm', footerUserSpans: [{ colspan: 4 }, { colspan: 4 }] }, + { column: 'ctpvNm', footerUserSpans: [{ colspan: 6 }, { colspan: 6 }] }, 'sggNm', 'admdstNm', ] From 685ba15baa519b54871b7ae5939bb08fc2c8f16b Mon Sep 17 00:00:00 2001 From: rsone001 Date: Tue, 17 Mar 2026 10:07:08 +0900 Subject: [PATCH 005/126] =?UTF-8?q?st:=20feat:=20=EC=9D=BC=EB=B0=98?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/module/SearchForm.tsx | 4 +- .../GeneralVerificationManagement.tsx | 271 +++++--- .../generalVerification/DetailComponent.tsx | 93 +++ .../generalVerification/RegistDialog.tsx | 613 ++++++++++++++++++ .../management/GeneralVerificationColumn.tsx | 45 +- apps/st/src/entities/code/index.ts | 1 + .../entities/code/ui/code-select-with-all.tsx | 48 ++ .../api/general-verification-service.ts | 66 ++ .../model/general-verification-interface.ts | 91 ++- ...neral-verification-master-submit-schema.ts | 28 + apps/st/src/shared/config/api.ts | 1 + 11 files changed, 1144 insertions(+), 117 deletions(-) create mode 100644 apps/st/src/components/statistics/verification/generalVerification/DetailComponent.tsx create mode 100644 apps/st/src/components/statistics/verification/generalVerification/RegistDialog.tsx create mode 100644 apps/st/src/entities/code/ui/code-select-with-all.tsx create mode 100644 apps/st/src/entities/statistics/api/general-verification-service.ts create mode 100644 apps/st/src/entities/statistics/model/general-verification-master-submit-schema.ts diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 54cdfe486..5bc809787 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -229,12 +229,12 @@ const SearchForm = (props: SearchFormProps = {}) => {
{mergedProps.fctName &&
-