cm: 수계별 유입ㆍ방류수질

This commit is contained in:
ljpark
2025-12-11 14:20:38 +09:00
parent 11c83dd893
commit dad78579cc
5 changed files with 168 additions and 91 deletions
+54 -21
View File
@@ -2,8 +2,10 @@
import { client } from "@/shared/api";
import { createStore } from "zustand/vanilla";
import { ENDPOINT } from "@/shared/config";
import type { OperFcltContent, ReuseStatusContent, OperationService } from "../model/fo-interface";
import type { OperWqltContent, OperFcltContent, ReuseStatusContent, OperationService
} from "../model/fo-interface";
//수계별 시설운영 개소수 및 가동률
export type OperFcltState = {
operFcltStatus: OperFcltContent[] | null;
};
@@ -11,23 +13,6 @@ 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;
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,
};
@@ -37,7 +22,26 @@ export const createOperFcltStore = (initState: OperFcltState = defaultInitOperFc
setOperFcltStatus: (operFcltStatus) => set(() => ({ operFcltStatus })),
}));
//수계별 유입ㆍ방류수질 분석
export type OperWqltState = {
operWqltStatus: OperWqltContent[] | null;
};
export type OperWqltStateActions = {
setOperWqltStatus: (operWqltStatus: OperWqltContent[] | null) => void;
};
export type OperWqltStore = OperWqltState & OperWqltStateActions;
export const defaultInitOperWqltState: OperWqltState = {
operWqltStatus: null,
};
// 물재이용 현황
export type ReuseState = {
reuseStatus: ReuseStatusContent[] | null;
};
export type ReuseStateActions = {
setReuseStatus: (reuseStatus: ReuseStatusContent[] | null) => void;
};
export type ReuseStore = ReuseState & ReuseStateActions;
const initialReuseStatusData: ReuseStatusContent[] = [
{ yr: '2021', sewage: 7335803, reclaimed: 254805510.3, rainwater: 14049224.5 },
{ yr: '2022', sewage: 7378907, reclaimed: 243457788.8, rainwater: 15128093.4 },
@@ -53,6 +57,15 @@ export const createReuseStore = (initState: ReuseState = defaultInitReuseState)
setReuseStatus: (reuseStatus) => set(() => ({ reuseStatus })),
}));
// 기준년도 조회
export type YearState = {
year: string | null;
};
export type YearStateActions = {
setYear: (year: string | null) => void;
};
export type YearStore = YearState & YearStateActions;
export const defaultYearInitState: YearState = {
year: null,
};
@@ -64,13 +77,12 @@ export const createYearStore = (initState: YearState = defaultYearInitState) =>
// 전역으로 공유할 store 인스턴스
export const operFcltStore = createOperFcltStore();
// 전역으로 공유할 store 인스턴스
export const reuseStore = createReuseStore();
// 전역으로 공유할 store 인스턴스
export const yearStore = createYearStore();
export class OperationApiService implements OperationService {
// 기준년도 조회
public async getStandardYear(): Promise<string> {
const { year, setYear } = yearStore.getState();
if (year) {
@@ -117,7 +129,27 @@ export class OperationApiService implements OperationService {
return list;
}
// 여러 컴포넌트에서 호출해도 서버 호출은 1번만 되도록 캐싱
// 수계별 시설운영 개소수 및 가동률
public async getDashOperWqlt(year: string): Promise<OperWqltContent[]> {
const response = await client.get<{
status: number;
data: OperWqltContent[];
count: number;
}>(ENDPOINT.OPERATION_SERVICE.OPER_WQLT, {
params: {
year,
},
});
const list = (response.data.data ?? []).map((item) => ({
...item,
}));
return list;
}
// 물재이용 현황
public async getReuseStatus(): Promise<ReuseStatusContent[]> {
// API 호출 시에는 항상 최신 데이터를 가져오도록 캐싱 로직을 제거하거나 수정할 수 있습니다.
// 여기서는 기존 캐싱 로직을 유지하되, 초기 데이터가 아닌 경우에만 API를 호출하도록 합니다.
@@ -146,3 +178,4 @@ export class OperationApiService implements OperationService {
}
export const OperationApi = new OperationApiService();
@@ -1,10 +1,3 @@
export interface ReuseStatusContent {
yr: string;
sewage: number;
reclaimed: number;
rainwater: number;
}
export interface OperFcltContent {
rvbsn: string; // 유역
cnt: number; // 개소수
@@ -12,6 +5,46 @@ export interface OperFcltContent {
gubun: string; // 구분
}
export interface OperWqltContent {
rvbsn: string; // 유역명
yr: string; // 연도
sumInflBod: number; // 유입수 BOD 합계
sumInflToc: number; // 유입수 TOC 합계
sumInflSs: number; // 유입수 SS 합계
sumInflTp: number; // 유입수 TP 합계
sumInflTn: number; // 유입수 TN 합계
sumEfflBod: number; // 유출수 BOD 합계
sumEfflToc: number; // 유출수 TOC 합계
sumEfflSs: number; // 유출수 SS 합계
sumEfflTp: number; // 유출수 TP 합계
sumEfflTn: number; // 유출수 TN 합계
avgInflBod: number; // 기간 평균 유입수 BOD
avgInflToc: number; // 기간 평균 유입수 TOC
avgInflSs: number; // 기간 평균 유입수 SS
avgInflTp: number; // 기간 평균 유입수 TP
avgInflTn: number; // 기간 평균 유입수 TN
avgEfflBod: number; // 기간 평균 유출수 BOD
avgEfflToc: number; // 기간 평균 유출수 TOC
avgEfflSs: number; // 기간 평균 유출수 SS
avgEfflTp: number; // 기간 평균 유출수 TP
avgEfflTn: number; // 기간 평균 유출수 TN
}
export interface ReuseStatusContent {
yr: string;
sewage: number;
reclaimed: number;
rainwater: number;
}
export interface OperationService {
getStandardYear(): Promise<string> ;
getDashOperCntRt(year: string): Promise<OperFcltContent[]>;
getDashOperWqlt(year: string): Promise<OperWqltContent[]>;
getReuseStatus(): Promise<ReuseStatusContent[]>;
}
@@ -6,6 +6,7 @@ import React, { useEffect, useState, useMemo } from 'react';
import { useStore } from "zustand";
import BasinSelect from "./basIn-select";
import { Doughnut } from "react-chartjs-2";
import type { Chart as ChartJS } from "chart.js";
// import type { OperFcltContent } from "../model/fo-interface";
import { OperationApi, operFcltStore } from "../api/fo-service";
@@ -31,24 +32,23 @@ const FoFcltCntRt: React.FC = () => {
if (!operFcltStatus) {
return { facilityCount: 0, operatingRate: 0, chartData: [0, 100] };
}
console.log(operFcltStatus);
const basinData = operFcltStatus.filter(d => d.rvbsn === selectedBasin); // 선택된 유역의 데이터만 filter
const operatingData = basinData.find(d => d.gubun === 'R'); // 선택 유역의 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;
const nonOperatingRate = totalCapacity > 0 ? Math.round((nonOperatingCapacity / totalCapacity) * 100) : 0;
return {
const result = {
facilityCount,
operatingRate,
chartData: [operatingRate, nonOperatingRate],
chartData: [operatingCapacity, nonOperatingCapacity],
};
console.log(result);
return result;
}, [operFcltStatus, selectedBasin]);
return (
@@ -106,12 +106,14 @@ const FoFcltCntRt: React.FC = () => {
}
}
}}
key={displayData.operatingRate} // Key를 변경하여 차트 강제 업데이트
plugins={[{
id: 'centerText',
afterDraw: (chart) => {
afterDraw: (chart: ChartJS) => {
const ctx = chart.ctx;
const centerX = chart.width / 2;
const centerY = chart.height / 2;
const rate = displayData.operatingRate ?? 0;
// 가동률 텍스트
ctx.save();
@@ -119,7 +121,7 @@ const FoFcltCntRt: React.FC = () => {
ctx.fillStyle = '#00DFE5';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(`${displayData.operatingRate}%`, centerX, centerY + 10);
ctx.fillText(`${rate}%`, centerX, centerY + 10);
// 라벨 텍스트
ctx.font = '10px Arial';
@@ -2,57 +2,66 @@
/**
* 수계별 유입ㆍ방류수질 분석
*/
import React, { useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { Line } from 'react-chartjs-2';
import BasinSelect from "./basIn-select";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
type ChartOptions,
CategoryScale, LinearScale, PointElement, LineElement,
Title, Tooltip, Legend, type ChartOptions,
} from 'chart.js';
import type { OperWqltContent } from "../model/fo-interface";
import { OperationApi } from "../api/fo-service";
// Chart.js 등록
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
type WaterQualityType = 'BOD' | 'TOC' | 'SS' | 'TP' | 'TN';
interface ChartData {
labels: string[];
inflowData: number[];
inflowAverage: number[];
effluentData: number[];
effluentAverage: number[];
}
const FoWqltInflEffl: React.FC = () => {
// const [year, setYear] = useState<string | null>(null);
const [selectedBasin, setSelectedBasin] = useState<string>("한강");
const [selectedTab, setSelectedTab] = useState<WaterQualityType>('BOD');
// 데이터 (실제로는 API에서 가져올 데이터)
const bodData: ChartData = {
labels: ['2020', '2021', '2022', '2023', '2024'],
inflowData: [2255.6, 2282.2, 2304.3, 5156.6, 4620.4],
inflowAverage: [2255.6, 2282.2, 2304.3, 2304.3, 2304.3],
effluentData: [55, 47.5, 52, 79.7, 83.9],
effluentAverage: [55, 47.5, 52, 52, 52],
};
const [selectedTab, setSelectedTab] = useState<WaterQualityType>('BOD');
const [wqltData, setWqltData] = useState<OperWqltContent[]>([]);
const tabs: WaterQualityType[] = ['BOD', 'TOC', 'SS', 'TP', 'TN'];
useEffect(() => {
const fetchData = async () => {
try {
const standardYear = await OperationApi.getStandardYear();
const data = await OperationApi.getDashOperWqlt(standardYear);
setWqltData(data);
console.log(wqltData);
} catch (e) {
console.error("시설 유입ㆍ방류수질 데이터 조회 실패", e);
}
};
fetchData();
}, []);
const chartData = useMemo(() => {
const filteredData = wqltData
.filter(d => d.rvbsn === selectedBasin) // 해당 유역
.sort((a, b) => Number(a.yr) - Number(b.yr));
const wqltKey = selectedTab.charAt(0).toUpperCase() + selectedTab.slice(1).toLowerCase();
const inflowKey = `sumInfl${wqltKey}` as keyof OperWqltContent;
const effluentKey = `sumEffl${wqltKey}` as keyof OperWqltContent;
const inflowAvgKey = `avgInfl${wqltKey}` as keyof OperWqltContent;
const effluentAvgKey = `avgEffl${wqltKey}` as keyof OperWqltContent;
const labels = filteredData.map(d => d.yr ?? '');
const inflowData = filteredData.map(d => d[inflowKey] as number);
const effluentData = filteredData.map(d => d[effluentKey] as number);
const inflowAvg = filteredData[0]?.[inflowAvgKey] as number ?? 0;
const effluentAvg = filteredData[0]?.[effluentAvgKey] as number ?? 0;
return { labels, inflowData, effluentData, inflowAvg, effluentAvg };
}, [wqltData, selectedBasin, selectedTab]);
console.log(chartData);
// 차트 공통 옵션
// const getChartOptions = (yAxisLabel: string): ChartOptions<'line'> => ({
const getChartOptions = (): ChartOptions<'line'> => ({
@@ -124,11 +133,11 @@ const FoWqltInflEffl: React.FC = () => {
// 유입 데이터 차트 설정
const inflowChartData = {
labels: bodData.labels,
labels: chartData.labels,
datasets: [
{
label: '유입BOD (mg/L)',
data: bodData.inflowData,
label: `유입${selectedTab} (mg/L)`,
data: chartData.inflowData,
borderColor: '#00D4FF',
backgroundColor: '#00D4FF',
borderWidth: 1,
@@ -138,25 +147,25 @@ const FoWqltInflEffl: React.FC = () => {
},
{
label: '유입 평균 (mg/L)',
data: bodData.inflowAverage,
data: Array(chartData.labels.length).fill(chartData.inflowAvg),
borderColor: '#FF1493',
backgroundColor: '#FF1493',
borderWidth: 1,
borderDash: [10, 5],
pointRadius: 2,
pointRadius: 0,
pointHoverRadius: 4,
tension: 0,
},
],
};
// 발류 데이터 차트 설정
// 방류 데이터 차트 설정
const effluentChartData = {
labels: bodData.labels,
labels: chartData.labels,
datasets: [
{
label: '발류BOD (mg/L)',
data: bodData.effluentData,
label: `방류${selectedTab} (mg/L)`,
data: chartData.effluentData,
borderColor: '#00FF88',
backgroundColor: '#00FF88',
borderWidth: 1,
@@ -165,13 +174,13 @@ const FoWqltInflEffl: React.FC = () => {
tension: 0.4,
},
{
label: '발류 평균 (mg/L)',
data: bodData.effluentAverage,
label: '방류 평균 (mg/L)',
data: Array(chartData.labels.length).fill(chartData.effluentAvg),
borderColor: '#FFB800',
backgroundColor: '#FFB800',
borderWidth: 1,
borderDash: [10, 5],
pointRadius: 1,
pointRadius: 0,
pointHoverRadius: 4,
tension: 0,
},
@@ -209,15 +218,14 @@ const FoWqltInflEffl: React.FC = () => {
</div>
{/* 차트 영역 */}
<div style={{ display: 'flex', flexDirection: 'column',flex: 1, minHeight: 0 }}>
<div style={{ display: 'flex', flexDirection: 'row', flex: 1, minHeight: 0 }}>
{/* 유입 차트 */}
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>
{/* <Line data={inflowChartData} options={getChartOptions('mg/L')} /> */}
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
<Line data={inflowChartData} options={getChartOptions()} />
</div>
{/* 발류 차트 */}
<div style={{ flex: 1, position: 'relative', minHeight: 0 }}>
{/* 방류 차트 */}
<div style={{ flex: 1, position: 'relative', minWidth: 0 }}>
<Line data={effluentChartData} options={getChartOptions()} />
</div>
</div>
+1
View File
@@ -78,6 +78,7 @@ export const ENDPOINT = {
OPERATION_SERVICE: {
MAIN_YEAR: "/operation-service/api/v1/dashBoard/main/year",
OPER_CNTRT: "/operation-service/api/v1/dashBoard/main/oper/cntrt",
OPER_WQLT: "/operation-service/api/v1/dashBoard/main/oper/wqlt",
REUSE_STATUS: "/operation-service/api/v1/dashBoard/main/reuse/status",
},
IC_SERVICE: {