From 8bee745fe82e45594d56047b50f112d936246bf3 Mon Sep 17 00:00:00 2001 From: ljpark Date: Sat, 13 Dec 2025 18:23:15 +0900 Subject: [PATCH] =?UTF-8?q?cm:sn-old-ppl=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/src/pages/dashboard/api/sn-service.ts | 67 +++- .../pages/dashboard/lib/korea-basin-map.tsx | 108 ++----- apps/cm/src/pages/dashboard/model/map-data.ts | 65 ++++ .../src/pages/dashboard/model/sn-interface.ts | 53 +++ .../src/pages/dashboard/ui/dashboard-page.tsx | 1 - .../pages/dashboard/ui/dashboard-page2.tsx | 303 ------------------ apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx | 17 +- 7 files changed, 201 insertions(+), 413 deletions(-) create mode 100644 apps/cm/src/pages/dashboard/model/map-data.ts delete mode 100644 apps/cm/src/pages/dashboard/ui/dashboard-page2.tsx diff --git a/apps/cm/src/pages/dashboard/api/sn-service.ts b/apps/cm/src/pages/dashboard/api/sn-service.ts index 6e41b0724..789b46a9f 100644 --- a/apps/cm/src/pages/dashboard/api/sn-service.ts +++ b/apps/cm/src/pages/dashboard/api/sn-service.ts @@ -1,31 +1,69 @@ // apps/cm/src/pages/dashboard/api/sn-service.ts import { client } from "@/shared/api"; -import { createStore } from "zustand/vanilla"; +import { create } from "zustand"; import { ENDPOINT } from "@/shared/config"; import type { PplLengthContent, SewerageService } from "../model/sn-interface"; +import type { OldPplChartData } from "../model/sn-interface"; export type SnState = { snStatus: PplLengthContent[] | null; }; - export type SnStateActions = { setSnStatus: (snStatus: PplLengthContent[] | null) => void; }; - export type SnStore = SnState & SnStateActions; -export const defaultInitState: SnState = { - snStatus: null, +const initialOldPplChartData: OldPplChartData={ + labels: [ + "금강", + "한강", + "낙동강", + "영산강" + ], + datasets: [ + { + label: "20-25년", + data: [ + 1140, + 3181, + 3100, + 1662 + ], + backgroundColor: "#3B82F6", + borderRadius: 4 + }, + { + label: "25-30년", + data: [ + 1391, + 3615, + 1814, + 1411 + ], + backgroundColor: "#EF4444", + borderRadius: 4 + }, + { + label: "30년이상", + data: [ + 2014, + 10303, + 9831, + 2000 + ], + backgroundColor: "#F87171", + borderRadius: 4 + } + ] }; - -export const createSnStore = (initState: SnState = defaultInitState) => - createStore()((set) => ({ - ...initState, - setSnStatus: (snStatus) => set(() => ({ snStatus })), - })); - -// 전역으로 공유할 store 인스턴스 -// export const snStore = createSnStore(); +type OldPplStoreState = { + chartData: OldPplChartData; + setChartData: (chartData: OldPplChartData) => void; +}; +export const useOldPplStore = create((set) => ({ + chartData: initialOldPplChartData, + setChartData: (chartData) => set(() => ({ chartData })), +})); export class SewerageApiService implements SewerageService { // 기간별 관로 총길이 @@ -43,6 +81,7 @@ export class SewerageApiService implements SewerageService { ...item, sumPrlgVl: Number(item.sumPrlgVl), })); + return list; } diff --git a/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx index 0f1346388..0234b4a4f 100644 --- a/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx +++ b/apps/cm/src/pages/dashboard/lib/korea-basin-map.tsx @@ -6,80 +6,17 @@ import React, { useEffect, useRef, useState } from "react"; import { useStore } from "zustand"; import styles from "./korea-basin-map.module.css"; // 추가 import { basinStore } from "../api/fo-service"; +import { basinKeyMap,MANUAL_LABEL_POS } from "../model/map-data"; +import { BASIN_DATA, BASIN_IDS,svcInfo } from "../model/map-data"; // import { operFcltStore } from "../api/fo-service"; -import { - Tabs, - // TabsContent, - TabsList, - TabsTrigger, -} from "@common/components"; +// import {Button} from "@common/components"; const SVG_URL = "/images/region_map.svg"; // public 폴더에 region_map.svg 위치 필요 - // 2. 유역 키를 매핑하는 로직 (예: "한강" -> "han") -export const basinKeyMap: { [key: string]: string } = { - "한강": "han", - "금강": "geum", - "낙동강": "nakdong", - "영산강": "yeongsan", -}; -// --- 라벨 좌표(수동 고정값) : inset 적용 영역 기준 퍼센트 또는 px 사용 --- -// - 퍼센트: { x:0~1, y:0~1 } (권장: 반응형에 유리) -// - 픽셀: { px:number, py:number } (inset 좌상단 원점 기준) -const MANUAL_LABEL_POS = { - han: { x: 0.52, y: 0.14 }, - geum: { x: 0.28, y: 0.47 }, - nakdong: { x: 0.64, y: 0.47 }, - yeongsan: { x: 0.18, y: 0.77 }, -}; - -const BASIN_DATA = { - han: { - name: "한강", - colTitle: "시설수", - rows: [ - ["500m³ 미만", 100000000], - ["500m³ 이상", 100000000], - ], - }, - geum: { - name: "금강", - colTitle: "시설수", - rows: [ - ["500m³ 미만", 100000000], - ["500m³ 이상", 100000000], - ], - }, - nakdong: { - name: "낙동강", - colTitle: "시설수", - rows: [ - ["500m³ 미만", 100000000], - ["500m³ 이상", 100000000], - ], - }, - yeongsan: { - name: "영산강", - colTitle: "시설수", - rows: [ - ["500m³ 미만", 100000000], - ["500m³ 이상", 100000000], - ], - }, -}; - -const BASIN_IDS = [ - { key: "han", labels: ["한강", "han"] }, - { key: "geum", labels: ["금강", "geum"] }, - { key: "nakdong", labels: ["낙동강", "nakdong"] }, - { key: "yeongsan", labels: ["영산강", "yeongsan"] }, -]; - function fmt(n: number) { return new Intl.NumberFormat("ko-KR").format(n); } - export function KoreaBasinMap() { const stageRef = useRef(null); const svgLayerRef = useRef(null); @@ -87,8 +24,8 @@ export function KoreaBasinMap() { const popupRef = useRef(null); const badgeRef = useRef(null); const rowsRef = useRef(null); - const colTitleRef = useRef(null); - const [tab, setTab] = useState("물재이용"); + const colTitleRef = useRef(null); + // const [services, setServices] = useState(svcInfo); const { selectedBasin, setSelectedBasin } = useStore(basinStore); // const { operFcltStatus, setSelectedBasin } = useStore(operFcltStore); const selectedBasinKey = basinKeyMap[selectedBasin]; @@ -395,27 +332,32 @@ export function KoreaBasinMap() { }; }, []); + // const handleSvcInfoClick = (clickedSvcId: string) => { + // setServices(prevServices => + // prevServices.map(svc => ({ + // ...svc, + // active: svc.svcId === clickedSvcId, + // })) + // ); + // }; // 스타일은 필요에 따라 tailwind, css 등으로 커스터마이즈하세요. return (

유역별 정보

- -
- - - 물재이용 - - - 하수관로 - - - 운영실태 - - -
-
+ {/* + {services.map((svc) => ( + + ))} + */}
; getPplUseLength(ymdFrom: string, ymdTo: string, useCd: string): Promise; +} + +export interface ChartDataSet { + label: string; + data: (number | undefined)[]; + backgroundColor: string; + borderRadius?: number; +} +export interface OldPplChartData { + labels: string[]; + datasets: ChartDataSet[]; +} + +type SnOldPplItem = { + name: string; + colTitle: string[]; + rows: number[]; +}; + +type SnOldPplLength = { + [key: string]: SnOldPplItem; +}; + +export function buildSnOldPplLength( + chartData: { + labels: string[]; + datasets: { label: string; data: number[] }[]; + }, + totalLengths: number[] +): SnOldPplLength { + const colTitle = ["총길이", ...chartData.datasets.map((ds) => ds.label)]; + + const result: SnOldPplLength = {}; + + chartData.labels.forEach((basinName, index) => { + const key = basinKeyMap[basinName]; // geum, han, nakdong, yeongsan + if (!key) return; // 매핑 안되면 스킵 + const total = totalLengths[index] ?? 0; + const datasetValues = chartData.datasets.map( + (ds) => ds.data[index] ?? 0, + ); + result[key] = { + name: basinName, + colTitle, + rows: [ + total, // 총길이 (undefined면 0) + ...datasetValues, // 각 연차 데이터 (undefined면 0) + ], + }; + }); + + return result; } \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx index abb0a3ae5..f8d41ed22 100644 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/cm/src/pages/dashboard/ui/dashboard-page.tsx @@ -17,7 +17,6 @@ import { Tooltip, ArcElement, } from "chart.js"; -// import { Doughnut } from "react-chartjs-2"; import SnPplStatus from "./sn-ppl-status"; import SnOldPpl from "./sn-old-ppl"; import FoFcltCntRt from "./fo-fclt-cnt-rt"; diff --git a/apps/cm/src/pages/dashboard/ui/dashboard-page2.tsx b/apps/cm/src/pages/dashboard/ui/dashboard-page2.tsx deleted file mode 100644 index 0bcce9a6a..000000000 --- a/apps/cm/src/pages/dashboard/ui/dashboard-page2.tsx +++ /dev/null @@ -1,303 +0,0 @@ -"use client"; -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; -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 { - Chart as ChartJS, - CategoryScale, - LinearScale, - PointElement, - LineElement, - BarElement, - Filler, - Legend, - Tooltip, - ArcElement, -} from "chart.js"; -import { Doughnut } from "react-chartjs-2"; - -// Chart.js 등록 -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - BarElement, - Filler, - Legend, - Tooltip, - ArcElement -); - -interface UserInfo { - email?: string; - userId?: string; - userName?: string; - twoFactorEnabled?: boolean; -} - -function DashboardPage() { - const [userInfo, setUserInfo] = useState(null); - const [error, setError] = useState(""); - const router = useRouter(); - - useEffect(() => { - const fetchUserInfo = async () => { - console.log("fetchUserInfo called"); - - try { - // tokenManager를 통해 토큰 확인 - const accessToken = tokenManager.getToken("Authorization"); - const userId = tokenManager.getToken("token-id"); - - if (!accessToken || !userId) { - console.log("No tokens found, redirecting to login"); - setError("로그인이 필요합니다."); - router.push("/auth/login"); - return; - } - - console.log("Fetching user info with userId:", userId); - - // client를 사용하여 API 호출 (자동 토큰 처리) - const response = await client.get( - `${ENDPOINT.USER_SERVICE.USERS}/${userId}`, - ); - - if (response.data) { - console.log("User info received:", response.data); - setUserInfo(response.data); - } else { - setError("사용자 정보를 불러올 수 없습니다."); - } - } catch (error) { - console.error("Error fetching user info:", error); - if (error instanceof Error && error.message.includes("401")) { - setError("인증이 만료되었습니다. 다시 로그인해주세요."); - tokenManager.clearAuthToken(); - router.push("/auth/login"); - } else { - setError("네트워크 오류가 발생했습니다."); - } - } - }; - - fetchUserInfo(); - }, [router]); - - const handleLogout = () => { - console.log("Logging out..."); - // localStorage 정리 - localStorage.removeItem("tempToken"); - // tokenManager를 통해 쿠키 정리 - tokenManager.clearAuthToken(); - router.push("/auth/logout"); - }; - - const handleChangePassword = () => { - router.push("/otp?next=change-password"); - }; - - return ( -
-
- {/* 첫번째 라인 */} -
- {/* 첫번쨰 열 */} -
- {/* 전국 하수처리시설 가동률 */} -
-
-

전국 하수처리시설 가동률

-
-
- { - const ctx = chart.ctx; - const centerX = chart.width / 2; - const centerY = chart.height / 2; - - // 가동률 텍스트 - ctx.save(); - ctx.font = 'bold 24px Arial'; - ctx.fillStyle = '#00DFE5'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText('75%', centerX, centerY + 10); - - // 라벨 텍스트 - ctx.font = '12px Arial'; - ctx.fillStyle = '#666'; - ctx.fillText('가동률', centerX, centerY + 30); - ctx.restore(); - } - }]} - /> -
-
- - {/* 오수관로 및 우수관로 현황 */} -
-
-

오수관로 및 우수관로 현황

-
-
-
- - {/* 하수처리수 재이용 */} -
-
-

하수처리수 재이용

-
-
-
-
- - {/* 두번쨰 열 */} -
- {/* 하수처리시설(개소) */} -
-
-

하수처리시설(개소)

-
-
-
-
-

500㎥ 미만(개소)

-
-
3,567 개소
-
-
-
-

500㎥ 이상(개소)

-
-
767 개소
-
-
기준년월 : 2025.09
-
-
- - {/* 노후하수관로현황 */} -
-
-

노후하수관로현황

-
-
-
-
- - {/* 재이용시설(개소) */} -
-
-

재이용시설(개소)

-
-
-
-
-
- - {/* 지도영역 */} -
-
-
-

유역별 정보

-
- -
-
- - {/* 사용자 정보 카드 */} -
-

사용자 정보

- {error &&
{error}
} - {userInfo ? ( -
-
- 아이디: {userInfo.userId} -
-
- 이메일: {userInfo.email || userInfo.userId} -
-
- 이름: {userInfo.userName || "-"} -
-
- 2FA 활성화:{" "} - {userInfo.twoFactorEnabled ? "예" : "아니오"} -
-
- ) : ( - !error &&
사용자 정보를 불러오는 중...
- )} - - -
-
- - {/* 두번째 라인 */} -
-
-
-

지원현황

-
-
- -
-
-
-
-

자료관 자료현황

-
-
-
-
-
-
-

공지사항

-
-
-
-
- - {/* 배너 영역 */} -
- -
-
-
-
- ); -} - -export { DashboardPage }; 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 a4e00907a..dfcea9149 100644 --- a/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx +++ b/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx @@ -15,15 +15,10 @@ import { } from "chart.js"; import { Bar } from "react-chartjs-2"; import { Button } from "@common/components"; -import type { PplLengthContent } from "../model/sn-interface"; -import { SewerageApi } from "../api/sn-service"; +import type { PplLengthContent, ChartDataSet } from "../model/sn-interface"; +import { SewerageApi, useOldPplStore } from "../api/sn-service"; + -interface ChartDataSet { - label: string; - data: (number | undefined)[]; - backgroundColor: string; - borderRadius?: number; -} // ChartJS 컴포넌트 등록 ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); @@ -31,13 +26,11 @@ export default function SnOldPpl() { const [oldPpl2025Data, setOldPpl2025Data] = useState([]); const [oldPpl2530Data, setOldPpl2530Data] = useState([]); const [oldPpl30Data, setOldPpl30Data] = useState([]); - const [chartData, setChartData] = useState<{ - labels: string[]; - datasets: ChartDataSet[]; - }>({ labels: [], datasets: [] }); + const {chartData, setChartData} = useOldPplStore(); useEffect(() => { const currentYear = new Date().getFullYear(); + const ranges = { "20-25년": { from: `${currentYear - 25 + 1}0101`, to: `${currentYear - 20}1231`, setter: setOldPpl2025Data }, "25-30년": { from: `${currentYear - 30 + 1}0101`, to: `${currentYear - 25}1231`, setter: setOldPpl2530Data },