From 75248dc48f4f246430b9b5a5aa2ae79b325e1086 Mon Sep 17 00:00:00 2001 From: ljpark Date: Wed, 10 Dec 2025 17:19:02 +0900 Subject: [PATCH] =?UTF-8?q?dm:=20=EC=88=98=EA=B3=84=EB=B3=84=EA=B0=9C?= =?UTF-8?q?=EC=86=8C=EC=88=98=20=EA=B0=80=EB=8F=99=EB=A5=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cm/middleware.ts | 3 +- apps/cm/src/pages/dashboard/api/fo-service.ts | 103 ++++++++++++++++-- .../src/pages/dashboard/model/fo-interface.ts | 7 ++ .../src/pages/dashboard/ui/basIn-select.tsx | 11 +- .../src/pages/dashboard/ui/fo-fclt-cnt-rt.tsx | 57 ++++++++-- apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx | 6 +- apps/cm/src/shared/config/api.ts | 2 + 7 files changed, 164 insertions(+), 25 deletions(-) diff --git a/apps/cm/middleware.ts b/apps/cm/middleware.ts index 181d5f613..9ec06daa2 100644 --- a/apps/cm/middleware.ts +++ b/apps/cm/middleware.ts @@ -95,7 +95,8 @@ export function middleware(req: NextRequest) { const requestHeaders = new Headers(req.headers); const token = req.cookies.get(CLAIM_NAME)?.value; const userId = req.cookies.get(AUTH_USER_ID)?.value; - const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}`; + // const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}`; + const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}${searchParams ? `?${searchParams}` : ""}`; if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); diff --git a/apps/cm/src/pages/dashboard/api/fo-service.ts b/apps/cm/src/pages/dashboard/api/fo-service.ts index 4c14c0d26..2e0f6e7ae 100644 --- a/apps/cm/src/pages/dashboard/api/fo-service.ts +++ b/apps/cm/src/pages/dashboard/api/fo-service.ts @@ -2,44 +2,127 @@ import { client } from "@/shared/api"; import { createStore } from "zustand/vanilla"; import { ENDPOINT } from "@/shared/config"; -import type { ReuseStatusContent, OperationService } from "../model/fo-interface"; +import type { OperFcltContent, ReuseStatusContent, OperationService } from "../model/fo-interface"; + +export type OperFcltState = { + operFcltStatus: OperFcltContent[] | null; +}; +export type OperFcltStateActions = { + setOperFcltStatus: (operFcltStatus: OperFcltContent[] | null) => void; +}; +export type OperFcltStore = OperFcltState & OperFcltStateActions; export type ReuseState = { reuseStatus: ReuseStatusContent[] | null; }; - export type ReuseStateActions = { setReuseStatus: (reuseStatus: ReuseStatusContent[] | null) => void; }; - export type ReuseStore = ReuseState & ReuseStateActions; -const initialData: ReuseStatusContent[] = [ +export type YearState = { + year: string | null; +}; +export type YearStateActions = { + setYear: (year: string | null) => void; +}; +export type YearStore = YearState & YearStateActions; + +export const defaultInitOperFcltState: OperFcltState = { + operFcltStatus: null, +}; +export const createOperFcltStore = (initState: OperFcltState = defaultInitOperFcltState) => + createStore()((set) => ({ + ...initState, + setOperFcltStatus: (operFcltStatus) => set(() => ({ operFcltStatus })), + })); + + +const initialReuseStatusData: ReuseStatusContent[] = [ { yr: '2021', sewage: 7335803, reclaimed: 254805510.3, rainwater: 14049224.5 }, { yr: '2022', sewage: 7378907, reclaimed: 243457788.8, rainwater: 15128093.4 }, { yr: '2023', sewage: 7537151, reclaimed: 222475997, rainwater: 13944732.5 }, { yr: '2024', sewage: 7479136, reclaimed: 265625917.7, rainwater: 13305309.5 }, ]; -export const defaultInitState: ReuseState = { - reuseStatus: initialData, +export const defaultInitReuseState: ReuseState = { + reuseStatus: initialReuseStatusData, }; - -export const createReuseStore = (initState: ReuseState = defaultInitState) => +export const createReuseStore = (initState: ReuseState = defaultInitReuseState) => createStore()((set) => ({ ...initState, setReuseStatus: (reuseStatus) => set(() => ({ reuseStatus })), })); +export const defaultYearInitState: YearState = { + year: null, +}; +export const createYearStore = (initState: YearState = defaultYearInitState) => + createStore()((set) => ({ + ...initState, + setYear: (year) => set(() => ({ year })), + })); + +// 전역으로 공유할 store 인스턴스 +export const operFcltStore = createOperFcltStore(); // 전역으로 공유할 store 인스턴스 export const reuseStore = createReuseStore(); +// 전역으로 공유할 store 인스턴스 +export const yearStore = createYearStore(); export class OperationApiService implements OperationService { + + public async getStandardYear(): Promise { + const { year, setYear } = yearStore.getState(); + if (year) { + return year; + } + + const response = await client.get<{ + status: number; + data: string; + count: number; + }>(ENDPOINT.OPERATION_SERVICE.MAIN_YEAR); + + const newYear = response.data.data || '2024'; + setYear(newYear); + + return newYear; + } + + // 수계별 시설운영 개소수 및 가동률 + public async getDashOperCntRt(year: string): Promise { + const { operFcltStatus, setOperFcltStatus } = operFcltStore.getState(); + // 간단한 캐싱: 데이터가 이미 있으면 API 호출 생략 + if (operFcltStatus && operFcltStatus.length > 0) { + return operFcltStatus; + } + + const response = await client.get<{ + status: number; + data: OperFcltContent[]; + count: number; + }>(ENDPOINT.OPERATION_SERVICE.OPER_CNTRT, { + params: { + year, + }, + }); + + const list = (response.data.data ?? []).map((item) => ({ + ...item, + cnt: Number(item.cnt), + fctCpc: Number(item.fctCpc), + })); + + setOperFcltStatus(list); + return list; + } + // 여러 컴포넌트에서 호출해도 서버 호출은 1번만 되도록 캐싱 public async getReuseStatus(): Promise { // API 호출 시에는 항상 최신 데이터를 가져오도록 캐싱 로직을 제거하거나 수정할 수 있습니다. // 여기서는 기존 캐싱 로직을 유지하되, 초기 데이터가 아닌 경우에만 API를 호출하도록 합니다. const { reuseStatus, setReuseStatus } = reuseStore.getState(); - if (reuseStatus && reuseStatus.length > 0 && reuseStatus !== initialData) { + if (reuseStatus && reuseStatus.length > 0 && reuseStatus !== defaultInitReuseState.reuseStatus) { return reuseStatus; } @@ -58,6 +141,8 @@ export class OperationApiService implements OperationService { setReuseStatus(list); return list; } + + } export const OperationApi = new OperationApiService(); diff --git a/apps/cm/src/pages/dashboard/model/fo-interface.ts b/apps/cm/src/pages/dashboard/model/fo-interface.ts index c3cf754f7..6d57ae6bd 100644 --- a/apps/cm/src/pages/dashboard/model/fo-interface.ts +++ b/apps/cm/src/pages/dashboard/model/fo-interface.ts @@ -5,6 +5,13 @@ export interface ReuseStatusContent { rainwater: number; } +export interface OperFcltContent { + rvbsn: string; // 유역 + cnt: number; // 개소수 + fctCpc: number; // 가동률 (용량) + gubun: string; // 구분 +} + export interface OperationService { getReuseStatus(): Promise; } \ No newline at end of file diff --git a/apps/cm/src/pages/dashboard/ui/basIn-select.tsx b/apps/cm/src/pages/dashboard/ui/basIn-select.tsx index 52b2e58a3..ca077da20 100644 --- a/apps/cm/src/pages/dashboard/ui/basIn-select.tsx +++ b/apps/cm/src/pages/dashboard/ui/basIn-select.tsx @@ -9,13 +9,16 @@ import { SelectValue, } from "@common/components"; -const BasinSelect: React.FC = () => { - const [selectedBasin, setSelectedBasin] = useState("한강유역"); +interface BasinSelectProps { + selectedBasin: string; + onValueChange: (value: string) => void; +} - const basins = ["한강유역", "금강유역", "낙동강유역", "영산강유역"]; +const BasinSelect: React.FC = ({ selectedBasin, onValueChange }) => { + const basins = ["한강", "금강", "낙동강", "영산강"]; 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 7e92b3653..05f39cca1 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 @@ -2,18 +2,59 @@ /** * 수계별 시설운영 개소수 및 가동률 */ -import React from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; +import { useStore } from "zustand"; import BasinSelect from "./basIn-select"; import { Doughnut } from "react-chartjs-2"; +// import type { OperFcltContent } from "../model/fo-interface"; +import { OperationApi, operFcltStore } from "../api/fo-service"; const FoFcltCntRt: React.FC = () => { + const [year, setYear] = useState(null); + const [selectedBasin, setSelectedBasin] = useState("한강"); + const operFcltStatus = useStore(operFcltStore, (state) => state.operFcltStatus); + useEffect(() => { + const fetchData = async () => { + try { + const standardYear = await OperationApi.getStandardYear(); + setYear(standardYear); + await OperationApi.getDashOperCntRt(standardYear); + } catch (e) { + console.error("시설운영 데이터 조회 실패", e); + } + }; + fetchData(); + }, []); + + const displayData = useMemo(() => { + if (!operFcltStatus) { + return { facilityCount: 0, operatingRate: 0, chartData: [0, 100] }; + } + + const basinData = operFcltStatus.filter(d => d.rvbsn === selectedBasin); + const operatingData = basinData.find(d => d.gubun === 'R'); + const nonOperatingData = basinData.find(d => d.gubun === 'A'); + + const facilityCount = operatingData?.cnt ?? 0; + const operatingCapacity = operatingData?.fctCpc ?? 0; + const nonOperatingCapacity = nonOperatingData?.fctCpc ?? 0; + const totalCapacity = operatingCapacity + nonOperatingCapacity; + + const operatingRate = totalCapacity > 0 ? Math.round((operatingCapacity / totalCapacity) * 100) : 0; + + return { + facilityCount, + operatingRate, + chartData: [operatingCapacity, nonOperatingCapacity], + }; + }, [operFcltStatus, selectedBasin]); return (

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

- +
@@ -21,11 +62,11 @@ const FoFcltCntRt: React.FC = () => {
-

개소수 (2024년도)

+

개소수 ({year}년도)

- 3,567 개소 + {displayData.facilityCount.toLocaleString()} 개소
@@ -34,14 +75,14 @@ const FoFcltCntRt: React.FC = () => {
-

가동률 (2024년도)

+

가동률 ({year}년도)

{ ctx.fillStyle = '#00DFE5'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillText('75%', centerX, centerY + 10); + ctx.fillText(`${displayData.operatingRate}%`, centerX, centerY + 10); // 라벨 텍스트 ctx.font = '10px Arial'; ctx.fillStyle = '#666'; - ctx.fillText('가동률', centerX, centerY + 30); + ctx.fillText('가동률', centerX, centerY + 28); ctx.restore(); } }]} 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 14c84a0f2..5bb36ffe6 100644 --- a/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx +++ b/apps/cm/src/pages/dashboard/ui/sn-old-ppl.tsx @@ -112,7 +112,7 @@ export default function SnOldPpl() { ticks: { stepSize: 2000, callback: function (value) { - return value + 'km'; + return Number(value).toLocaleString() + 'km'; } }, grid: { @@ -133,7 +133,7 @@ export default function SnOldPpl() { tooltip: { callbacks: { label: function (context) { - return `${context.dataset.label}: ${context.parsed.y}km`; + return `${context.dataset.label}: ${Number(context.parsed.y).toLocaleString()}km`; } } }, @@ -170,7 +170,7 @@ export default function SnOldPpl() {

노후 하수관로 현황

-
+
diff --git a/apps/cm/src/shared/config/api.ts b/apps/cm/src/shared/config/api.ts index d5c812576..3ac14a983 100644 --- a/apps/cm/src/shared/config/api.ts +++ b/apps/cm/src/shared/config/api.ts @@ -76,6 +76,8 @@ export const ENDPOINT = { OLD_PPL_COUNTS: "/sewerage-service/api/v1/ms/oldPplCounts", }, OPERATION_SERVICE: { + MAIN_YEAR: "/operation-service/api/v1/dashBoard/main/year", + OPER_CNTRT: "/operation-service/api/v1/dashBoard/main/oper/cntrt", REUSE_STATUS: "/operation-service/api/v1/dashBoard/main/reuse/status", }, IC_SERVICE: {