diff --git a/apps/cm/.env.local b/apps/cm/.env.local index 1ef6a3045..da63ebdfd 100644 --- a/apps/cm/.env.local +++ b/apps/cm/.env.local @@ -7,4 +7,5 @@ NEXT_PUBLIC_SERVER_API_URL=http://dev.onasys.co.kr:8000 NEXT_PUBLIC_LINK_BACKEND_API_HOST=http://rsone.iptime.org:10014 NEXT_PUBLIC_TINY_API_KEY=t9nc4z6inquvvd5pq1vea8wcp7lb01qmqseh6egms9vpaju2 -NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/ \ No newline at end of file +# NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/ +NEXT_PUBLIC_SERVER_API_URL2=http://192.168.1.222:8883/ \ No newline at end of file diff --git a/apps/cm/middleware.ts b/apps/cm/middleware.ts index ac1e5464b..3fb378cf9 100644 --- a/apps/cm/middleware.ts +++ b/apps/cm/middleware.ts @@ -14,11 +14,11 @@ const serverApiUrl = const serverApiUrl2 = process.env.NEXT_PUBLIC_SERVER_API_URL2 ?? serverApiUrl; - const serverApiUrl3 = - process.env.NEXT_PUBLIC_SERVER_API_URL3 ?? serverApiUrl; + // const serverApiUrl3 = + // process.env.NEXT_PUBLIC_SERVER_API_URL3 ?? serverApiUrl; - const serverApiUrl4 = - process.env.NEXT_PUBLIC_SERVER_API_URL4 ?? serverApiUrl; + // const serverApiUrl4 = + // process.env.NEXT_PUBLIC_SERVER_API_URL4 ?? serverApiUrl; const serverApiUrl5 = process.env.NEXT_PUBLIC_SERVER_API_URL5 ?? serverApiUrl; @@ -33,19 +33,6 @@ export function middleware(req: NextRequest) { `[Request]\n Original Pathname: ${pathname}`, ]; - if (pathname.startsWith("/server")) { - const queryParams = searchParams.toString(); - const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ - queryParams ? `?${queryParams}` : "" - }`; - - logMessages.push(` Rewritten URL: ${newUrl}`); - logMessages.push("\n--------------------------------\n"); - console.log(logMessages.join("\n")); - - return NextResponse.rewrite(new URL(newUrl)); - } - if (pathname.startsWith("/attach-service")) { const requestHeaders = new Headers(req.headers); const token = req.cookies.get(CLAIM_NAME)?.value; @@ -57,7 +44,7 @@ export function middleware(req: NextRequest) { logMessages.push(` Rewritten URL: ${newUrl}`); logMessages.push("\n--------------------------------\n"); - console.log(logMessages.join("\n")); + // console.log(logMessages.join("\n")); return NextResponse.rewrite(new URL(newUrl), { headers: requestHeaders, @@ -87,7 +74,7 @@ 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 = `${serverApiUrl3}${pathname}`; + const newUrl = `${serverApiUrl2}${pathname}`; if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); @@ -101,11 +88,12 @@ export function middleware(req: NextRequest) { }); } - if (pathname.startsWith("/operation-service")) { + if (pathname.startsWith("/server/operation-service")) { 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 = `${serverApiUrl4}${pathname}`; + const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}`; +console.log("==========================================" + newUrl); if (token) requestHeaders.set(CLAIM_NAME, token); if (userId) requestHeaders.set(AUTH_USER_ID, userId); @@ -163,11 +151,25 @@ export function middleware(req: NextRequest) { logMessages.push(` User Service Rewritten URL: ${newUrl}`); logMessages.push("\n--------------------------------\n"); - console.log(logMessages.join("\n")); + // console.log(logMessages.join("\n")); return NextResponse.rewrite(new URL(newUrl)); } + if (pathname.startsWith("/server")) { + const queryParams = searchParams.toString(); + const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${ + queryParams ? `?${queryParams}` : "" + }`; + + logMessages.push(` Rewritten URL: ${newUrl}`); + logMessages.push("\n--------------------------------\n"); + // console.log(logMessages.join("\n")); + + return NextResponse.rewrite(new URL(newUrl)); + } + + if (pathname === "/") { return NextResponse.redirect(new URL("/dashboard", req.url)); } @@ -178,5 +180,11 @@ export function middleware(req: NextRequest) { } export const config = { - matcher: ["/server/:path*", "/attach-service/:path*", "/user-service/:path*", "/"], + matcher: ["/server/:path*", + "/attach-service/:path*", + "/user-service/:path*", + "/sewerage-service/:path*", + "/reuse-service/:path*", + "/operation-service/:path*", + "/"], }; diff --git a/apps/cm/src/pages/dashboard/api/fo-service.ts b/apps/cm/src/pages/dashboard/api/fo-service.ts new file mode 100644 index 000000000..3468cdbb1 --- /dev/null +++ b/apps/cm/src/pages/dashboard/api/fo-service.ts @@ -0,0 +1,55 @@ +// apps/cm/src/pages/dashboard/api/fo-service.ts +import { client } from "@/shared/api"; +import { createStore } from "zustand/vanilla"; +import { ENDPOINT } from "@/shared/config"; +import type { ReuseStatusContent, OperationService } from "../model/fo-interface"; + +export type ReuseState = { + reuseStatus: ReuseStatusContent[] | null; +}; + +export type ReuseStateActions = { + setReuseStatus: (reuseStatus: ReuseStatusContent[] | null) => void; +}; + +export type ReuseStore = ReuseState & ReuseStateActions; + +export const defaultInitState: ReuseState = { + reuseStatus: null, +}; + +export const createReuseStore = (initState: ReuseState = defaultInitState) => + createStore()((set) => ({ + ...initState, + setReuseStatus: (reuseStatus) => set(() => ({ reuseStatus })), + })); + +// 전역으로 공유할 store 인스턴스 +export const reuseStore = createReuseStore(); + +export class OperationApiService implements OperationService { + // 여러 컴포넌트에서 호출해도 서버 호출은 1번만 되도록 캐싱 + public async getReuseStatus(): Promise { + const { reuseStatus, setReuseStatus } = reuseStore.getState(); + if (reuseStatus && reuseStatus.length > 0) { + return reuseStatus; + } + + const response = await client.get<{ + status: number; + data: ReuseStatusContent[]; + count: number; + }>(ENDPOINT.OPERATION_SERVICE.REUSE_STATUS); + + const list = (response.data.data ?? []).map((item) => ({ + ...item, + reclaimed: Number(item.reclaimed), + rainwater: Number(item.rainwater), + })); + + 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 new file mode 100644 index 000000000..c3cf754f7 --- /dev/null +++ b/apps/cm/src/pages/dashboard/model/fo-interface.ts @@ -0,0 +1,10 @@ +export interface ReuseStatusContent { + yr: string; + sewage: number; + reclaimed: number; + rainwater: number; +} + +export interface OperationService { + getReuseStatus(): Promise; +} \ No newline at end of file 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 3f4e7410d..7e92b3653 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 @@ -1,3 +1,4 @@ +"use client"; /** * 수계별 시설운영 개소수 및 가동률 */ 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 723080648..3320a1a87 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 @@ -1,3 +1,4 @@ +"use client"; /** * 수계별 유입ㆍ방류수질 분석 */ diff --git a/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx b/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx index 2541b955e..0a9879c19 100644 --- a/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx +++ b/apps/cm/src/pages/dashboard/ui/reuse-chart.tsx @@ -1,9 +1,13 @@ +"use client"; /** * 물재이용 차트 */ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; +import { useStore } from "zustand"; 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 { Chart as ChartJS, CategoryScale, @@ -31,14 +35,29 @@ ChartJS.register( -const ReuseChart: React.FC = () => { - // 임시 데이터 (이미지 기반 추정치) - const data = { - labels: ['2021', '2022', '2023', '2024'], +const ReuseChart: React.FC = () => { + const [activeIndex, setActiveIndex] = useState(0); + const intervalRef = useRef(null); + const reuseStatusData = useStore( + reuseStore, + (state) => state.reuseStatus) as ReuseStatusContent[]; + + useEffect(() => { + OperationApi.getReuseStatus().catch((e) => { + console.error("물재이용 차트 데이터 조회 실패", e); + }); + }, []); + + const labels = reuseStatusData?.map((r) => r.yr); + const sewageData = reuseStatusData?.map((r) => r.sewage); + const reclaimedData = reuseStatusData?.map((r) => r.reclaimed); + const rainwaterData = reuseStatusData?.map((r) => r.rainwater); + + const commonChartData = { datasets: [ { label: '재이용량', - data: [1125000, 1138000, 1135000, 1168000], // 이미지의 대략적인 값 + data: sewageData, // 기본은 하수처리수 borderColor: '#00D4FF', // 선 색상 (밝은 하늘색) backgroundColor: 'rgba(0, 212, 255, 0.1)', // 채우기 색상 (투명도 있는 하늘색) borderWidth: 3, @@ -52,9 +71,8 @@ const ReuseChart: React.FC = () => { }, ], }; - - const options: ChartOptions<'line'> = { - responsive: true, + + const commonChartOptions: ChartOptions<'line'> = { maintainAspectRatio: false, plugins: { legend: { @@ -104,17 +122,88 @@ const ReuseChart: React.FC = () => { }, }; + const chartItems = [ + { + title: "하수처리수 재이용량 (m³)", + data: { + ...commonChartData, + labels, + datasets: [{ ...commonChartData.datasets[0], data: sewageData }], + }, + options: commonChartOptions, + }, + { + title: "중수도 재이용량 (m³)", + data: { + ...commonChartData, + labels, + datasets: [{ ...commonChartData.datasets[0], data: reclaimedData }], + }, + options: commonChartOptions, + }, + { + title: "빗물 재이용량 (m³)", + data: { + ...commonChartData, + labels, + datasets: [{ ...commonChartData.datasets[0], data: rainwaterData }], + }, + options: commonChartOptions, + }, + ]; + + const startSlider = () => { + intervalRef.current = setInterval(() => { + setActiveIndex(prevIndex => (prevIndex + 1) % chartItems.length); + }, 20000); + }; + + const stopSlider = () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + + useEffect(() => { + startSlider(); + return () => stopSlider(); + }, []); + + const handleIndicatorClick = (index: number) => { + stopSlider(); + setActiveIndex(index); + startSlider(); + }; - return (
-

하수처리수 재이용량 (m³)

+

{chartItems[activeIndex]?.title}

-
-
- +
+
+ {chartItems.map((chart, index) => ( +
+ } /> +
+ ))} +
+ +
+ {chartItems.map((_, index) => ( +
diff --git a/apps/cm/src/pages/dashboard/ui/reuse-status.tsx b/apps/cm/src/pages/dashboard/ui/reuse-status.tsx index 835bf3170..a6ea4ee8c 100644 --- a/apps/cm/src/pages/dashboard/ui/reuse-status.tsx +++ b/apps/cm/src/pages/dashboard/ui/reuse-status.tsx @@ -2,44 +2,34 @@ /** * 물재이용 현황 */ -import React from 'react'; +import React, { useEffect } from 'react'; +import { useStore } from "zustand"; import { Button } from "@common/components"; - -interface ReuseData { - year: string; - reuseAmount: number; - wasteWaterAmount: number; - rainfallAmount: number; -} +import type { ReuseStatusContent } from "../model/fo-interface"; +import { OperationApi, reuseStore} from "../api/fo-service"; const ReuseStatus: React.FC = () => { - // 임시 데이터 - const data: ReuseData[] = [ - { - year: "'21", - reuseAmount: 1126995, - wasteWaterAmount: 254805510, - rainfallAmount: 14049225, - }, - { - year: "'22", - reuseAmount: 1136754, - wasteWaterAmount: 243457789, - rainfallAmount: 15128093, - }, - { - year: "'23", - reuseAmount: 1137539, - wasteWaterAmount: 222475997, - rainfallAmount: 13944733, - }, - { - year: "'24", - reuseAmount: 1168627, - wasteWaterAmount: 265625918, - rainfallAmount: 13305310, - }, - ]; + // const [reuseStatusData, setReuseStatusData] = useState([]); + const reuseStatusData = useStore( + reuseStore, + (state) => state.reuseStatus ) as ReuseStatusContent[]; + + useEffect(() => { + // const fetchData = async () => { + // try { + // const data = await OperationApi.getReuseStatus(); + // setReuseStatusData(data || []); // API 응답이 null/undefined일 경우 빈 배열로 설정 + // } catch (error) { + // console.error('물재이용 현황 데이터 조회에 실패했습니다.', error); + // } + // }; + // fetchData(); + // 여러 컴포넌트에서 호출해도 실제 API는 1번만 + OperationApi.getReuseStatus().catch((e) => { + console.error("물재이용 현황 데이터 조회 실패", e); + }); + }, []); + // 숫자 포맷 함수 (천단위 쉼표) const formatNumber = (num: number): string => { @@ -72,19 +62,19 @@ const ReuseStatus: React.FC = () => { - {data.map((row, index) => ( + {reuseStatusData && reuseStatusData.length > 0 && reuseStatusData?.map((row, index) => ( - {row.year} + {row.yr} - {formatNumber(row.reuseAmount)} + {formatNumber(row.sewage)} - {formatNumber(row.wasteWaterAmount)} + {formatNumber(row.reclaimed)} - {formatNumber(row.rainfallAmount)} + {formatNumber(row.rainwater)} ))} 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 afc2d3166..d691427b8 100644 --- a/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx +++ b/apps/cm/src/pages/dashboard/ui/sn-ppl-status.tsx @@ -1,5 +1,4 @@ "use client"; - /** * 오수관로 및 우수관로 현황 */ diff --git a/apps/cm/src/shared/config/api.ts b/apps/cm/src/shared/config/api.ts index 25c8b88c9..60e78c79d 100644 --- a/apps/cm/src/shared/config/api.ts +++ b/apps/cm/src/shared/config/api.ts @@ -73,6 +73,9 @@ export const ENDPOINT = { PPL_COUNTS: "/sewerage-service/api/v1/ms/pplCounts", OLD_PPL_COUNTS: "/sewerage-service/api/v1/ms/oldPplCounts", }, + OPERATION_SERVICE: { + REUSE_STATUS: "/operation-service/api/v1/dashBoard/main/reuse/status", + }, IC_SERVICE: { GROUPING_STATISTICS: "/ic-service/api/v1/nti/grouping-statistics", },