cm:물재이용 api 호출

This commit is contained in:
ljpark
2025-12-09 20:16:35 +09:00
parent 34b54c70de
commit bee0ccad72
10 changed files with 236 additions and 79 deletions
+2 -1
View File
@@ -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/
# NEXT_PUBLIC_SERVER_API_URL2=http://con.mind-one.co.kr:11083/
NEXT_PUBLIC_SERVER_API_URL2=http://192.168.1.222:8883/
+31 -23
View File
@@ -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*",
"/"],
};
@@ -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<ReuseStore>()((set) => ({
...initState,
setReuseStatus: (reuseStatus) => set(() => ({ reuseStatus })),
}));
// 전역으로 공유할 store 인스턴스
export const reuseStore = createReuseStore();
export class OperationApiService implements OperationService {
// 여러 컴포넌트에서 호출해도 서버 호출은 1번만 되도록 캐싱
public async getReuseStatus(): Promise<ReuseStatusContent[]> {
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();
@@ -0,0 +1,10 @@
export interface ReuseStatusContent {
yr: string;
sewage: number;
reclaimed: number;
rainwater: number;
}
export interface OperationService {
getReuseStatus(): Promise<ReuseStatusContent[]>;
}
@@ -1,3 +1,4 @@
"use client";
/**
* 수계별 시설운영 개소수 및 가동률
*/
@@ -1,3 +1,4 @@
"use client";
/**
* 수계별 유입ㆍ방류수질 분석
*/
+103 -14
View File
@@ -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<NodeJS.Timeout | null>(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 (
<div className="card card-border">
<div className="card-header text-foreground">
<h3 className="text-2xl font-medium">하수처리수 재이용량 (m³)</h3>
<h3 className="text-2xl font-medium">{chartItems[activeIndex]?.title}</h3>
<Button variant="outline" size="sm" className="text-white h-8 py-0 ml-auto">더보기</Button>
</div>
<div className="h-60 p-4">
<div style={{ width: '100%', height: '100%' }}>
<Line data={data} options={options} />
<div className="h-60 p-4 relative overflow-hidden">
<div
className="flex transition-transform duration-500 ease-in-out h-full"
style={{ transform: `translateX(-${activeIndex * 100}%)` }}
>
{chartItems.map((chart, index) => (
<div key={index} className="w-full flex-shrink-0 h-full">
<Line data={chart.data} options={chart.options as ChartOptions<'line'>} />
</div>
))}
</div>
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex space-x-2">
{chartItems.map((_, index) => (
<button
key={index}
onClick={() => handleIndicatorClick(index)}
className={`w-3 h-3 rounded-full cursor-pointer transition-colors ${
activeIndex === index ? 'bg-blue-500' : 'bg-gray-300'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
</div>
</div>
+30 -40
View File
@@ -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<ReuseStatusContent[]>([]);
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 = () => {
</tr>
</thead>
<tbody>
{data.map((row, index) => (
{reuseStatusData && reuseStatusData.length > 0 && reuseStatusData?.map((row, index) => (
<tr key={index} className="hover:bg-gray-50">
<td className="bg-blue-100 font-semibold text-center py-1 px-3 border border-gray-300">
{row.year}
{row.yr}
</td>
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
{formatNumber(row.reuseAmount)}
{formatNumber(row.sewage)}
</td>
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
{formatNumber(row.wasteWaterAmount)}
{formatNumber(row.reclaimed)}
</td>
<td className="text-center py-1 px-3 border border-gray-300 bg-white">
{formatNumber(row.rainfallAmount)}
{formatNumber(row.rainwater)}
</td>
</tr>
))}
@@ -1,5 +1,4 @@
"use client";
/**
* 오수관로 및 우수관로 현황
*/
+3
View File
@@ -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",
},