cm: 하수관로현황

This commit is contained in:
ljpark
2025-12-10 13:45:44 +09:00
parent bee0ccad72
commit b440e50cc4
8 changed files with 284 additions and 82 deletions
+5 -3
View File
@@ -52,11 +52,14 @@ export function middleware(req: NextRequest) {
}
if (pathname.startsWith("/sewerage-service")) {
if (pathname.startsWith("/server/sewerage-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 = `${serverApiUrl2}${pathname}`;
// const decodedQuery = toDecodedQueryString(searchParams);
const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}${searchParams ? `?${searchParams}` : ""}`;
// const newUrl = `${serverApiUrl2}${pathname}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
@@ -93,7 +96,6 @@ export function middleware(req: NextRequest) {
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
const newUrl = `${serverApiUrl2}${pathname.replace("/server/", "")}`;
console.log("==========================================" + newUrl);
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
+10 -2
View File
@@ -14,8 +14,14 @@ export type ReuseStateActions = {
export type ReuseStore = ReuseState & ReuseStateActions;
const initialData: 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: null,
reuseStatus: initialData,
};
export const createReuseStore = (initState: ReuseState = defaultInitState) =>
@@ -30,8 +36,10 @@ export const reuseStore = createReuseStore();
export class OperationApiService implements OperationService {
// 여러 컴포넌트에서 호출해도 서버 호출은 1번만 되도록 캐싱
public async getReuseStatus(): Promise<ReuseStatusContent[]> {
// API 호출 시에는 항상 최신 데이터를 가져오도록 캐싱 로직을 제거하거나 수정할 수 있습니다.
// 여기서는 기존 캐싱 로직을 유지하되, 초기 데이터가 아닌 경우에만 API를 호출하도록 합니다.
const { reuseStatus, setReuseStatus } = reuseStore.getState();
if (reuseStatus && reuseStatus.length > 0) {
if (reuseStatus && reuseStatus.length > 0 && reuseStatus !== initialData) {
return reuseStatus;
}
@@ -0,0 +1,69 @@
// apps/cm/src/pages/dashboard/api/sn-service.ts
import { client } from "@/shared/api";
import { createStore } from "zustand/vanilla";
import { ENDPOINT } from "@/shared/config";
import type { PplLengthContent, SewerageService } 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,
};
export const createSnStore = (initState: SnState = defaultInitState) =>
createStore<SnStore>()((set) => ({
...initState,
setSnStatus: (snStatus) => set(() => ({ snStatus })),
}));
// 전역으로 공유할 store 인스턴스
// export const snStore = createSnStore();
export class SewerageApiService implements SewerageService {
// 기간별 관로 총길이
public async getPplLength(instlYmdFrom: string, instlYmdTo: string): Promise<PplLengthContent[]> {
const response = await client.get<{
PplLengthList: PplLengthContent[];
}>(ENDPOINT.SEWERAGE_SERVICE.PPL_LENGTH, {
params: {
instlYmdFrom,
instlYmdTo,
},
});
const list = (response.data.PplLengthList ?? []).map((item) => ({
...item,
sumPrlgVl: Number(item.sumPrlgVl),
}));
return list;
}
// 기간별 용도별 관로 총길이 : useCd - SBA004 오수관, SBA003 우수관
public async getPplUseLength(instlYmdFrom: string, instlYmdTo: string, useCd: string): Promise<PplLengthContent[]> {
const response = await client.get<{
PplLengthList: PplLengthContent[];
}>(ENDPOINT.SEWERAGE_SERVICE.PPL_USE_LENGTH, {
params: {
instlYmdFrom,
instlYmdTo,
useCd,
},
});
const list = (response.data.PplLengthList ?? []).map((item) => ({
...item,
sumPrlgVl: Number(item.sumPrlgVl),
}));
return list;
}
}
export const SewerageApi = new SewerageApiService();
@@ -0,0 +1,9 @@
export interface PplLengthContent {
zoneNm: string;
sumPrlgVl: number;
}
export interface SewerageService {
getPplLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
getPplUseLength(ymdFrom: string, ymdTo: string, useCd: string): Promise<PplLengthContent[]>;
}
@@ -15,16 +15,6 @@ const ReuseStatus: React.FC = () => {
(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);
});
+77 -41
View File
@@ -2,6 +2,7 @@
/**
* 노후 하수관로 현황
*/
import React, { useEffect, useState } from "react";
import {
Chart as ChartJS,
CategoryScale,
@@ -14,44 +15,84 @@ 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";
interface ChartDataSet {
label: string;
data: (number | undefined)[];
backgroundColor: string;
borderRadius?: number;
}
// ChartJS 컴포넌트 등록
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend
);
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
export default function SnOldPpl() {
// 차트 데이터
const labels = ["한강", "낙동강", "영산강", "금강"];
const [oldPpl2025Data, setOldPpl2025Data] = useState<PplLengthContent[]>([]);
const [oldPpl2530Data, setOldPpl2530Data] = useState<PplLengthContent[]>([]);
const [oldPpl30Data, setOldPpl30Data] = useState<PplLengthContent[]>([]);
const [chartData, setChartData] = useState<{
labels: string[];
datasets: ChartDataSet[];
}>({ labels: [], datasets: [] });
const datasets = [
{
label: '20-25년',
data: [3859, 1066, 1929, 2828],
backgroundColor: '#3B82F6', // 파란색
borderRadius: 4,
},
{
label: '25년-30년',
data: [3153, 1420, 1204, 2877],
backgroundColor: '#EF4444', // 빨간색
borderRadius: 4,
},
{
label: '30년이상',
data: [9404, 1835, 1821, 8115],
backgroundColor: '#F87171', // 연한 빨간색/분홍색
borderRadius: 4,
},
];
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 },
"30년이상": { from: "19010101", to: `${currentYear - 30}1231`, setter: setOldPpl30Data },
};
Object.entries(ranges).forEach(([label, range]) => {
SewerageApi.getPplLength(range.from, range.to)
.then(range.setter)
.catch((e) => console.error(`${label} 데이터 조회 실패`, e));
});
}, []);
useEffect(() => {
const allData = [...oldPpl2025Data, ...oldPpl2530Data, ...oldPpl30Data];
if (allData.length === 0) return;
const allZones = Array.from(new Set(allData.map(d => d.zoneNm)));
const createDataset = (label: string, data: PplLengthContent[], color: string) => {
const dataMap = new Map(data.map(item => [item.zoneNm, item.sumPrlgVl]));
console.log(dataMap);
return {
label,
data: allZones.map(zone => dataMap.get(zone) ?? 0),
backgroundColor: color,
borderRadius: 4,
};
};
const newDatasets: ChartDataSet[] = [];
if (oldPpl2025Data.length > 0) {
newDatasets.push(createDataset('20-25년', oldPpl2025Data, '#3B82F6'));
}
if (oldPpl2530Data.length > 0) {
newDatasets.push(createDataset('25-30년', oldPpl2530Data, '#EF4444'));
}
if (oldPpl30Data.length > 0) {
newDatasets.push(createDataset('30년이상', oldPpl30Data, '#F87171'));
}
setChartData({
labels: allZones,
datasets: newDatasets,
});
}, [oldPpl2025Data, oldPpl2530Data, oldPpl30Data]);
// 각 지역별 총 길이 계산
const totalLengths = [44251, 33786, 11888, 14878];
const totalLengths = chartData.labels.map((label, i) => {
return chartData.datasets.reduce((sum, dataset) => {
const value = dataset.data[i];
return sum + (typeof value === 'number' ? value : 0);
}, 0);
});
// 차트 옵션 설정
const options: ChartOptions<"bar"> = {
@@ -104,30 +145,25 @@ export default function SnOldPpl() {
const ctx = chart.ctx;
// 각 데이터셋의 마지막 데이터셋의 인덱스
const lastDatasetIndex = datasets.length - 1;
console.log(lastDatasetIndex);
if (chartData.datasets.length === 0) return;
// const lastMeta = chart.getDatasetMeta(chartData.datasets.length - 1);
// 텍스트 스타일 설정
ctx.font = '11px Arial';
ctx.fillStyle = '#333';
ctx.textAlign = 'center';
// 각 지역별로 총 길이 표시
labels.forEach((_, i) => {
chartData.labels.forEach((_, i) => {
const xPos = chart.scales.x ? chart.scales.x.getPixelForTick(i) : 0;
const yPos = chart.chartArea.top; // 차트 영역 최상단 위에 표시
// 막대 위에 총 길이 텍스트 표시
if (totalLengths[i]) ctx.fillText(`총 ${totalLengths[i].toLocaleString()}km`, xPos, yPos);
if (totalLengths[i]) ctx.fillText(`총 ${totalLengths[i]?.toLocaleString()}km`, xPos, yPos);
});
}
}
};
const data = {
labels,
datasets,
};
return (
<div className="card card-border">
<div className="card-header text-foreground">
@@ -136,7 +172,7 @@ export default function SnOldPpl() {
</div>
<div className="h-70 p-4 flex items-center justify-center">
<div style={{ width: "100%", maxWidth: 400, height: 131 }}>
<Bar options={options} data={data} />
<Bar options={options} data={chartData} />
</div>
</div>
</div>
+112 -26
View File
@@ -2,7 +2,7 @@
/**
* 오수관로 및 우수관로 현황
*/
import React from "react";
import React, { useEffect, useState } from "react";
import {
Chart as ChartJS,
CategoryScale,
@@ -15,37 +15,123 @@ 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";
interface ChartDataSet {
label: string;
data: (number | undefined)[];
backgroundColor: string;
borderColor?: string;
}
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
export default function SnPplStatus() {
const labels = ["한강", "낙동강", "영산강", "금강"];
const [sewageData, setSewageData] = useState<PplLengthContent[]>([]);
const [stormwaterData, setStormwaterData] = useState<PplLengthContent[]>([]);
const [totalData, setTotalData] = useState<PplLengthContent[]>([]);
const [chartData, setChartData] = useState<{
labels: string[];
datasets: ChartDataSet[];
}>({ labels: [], datasets: [] });
// 데이터: [기타, 오수관로, 우수관로]
const datasets = [
{
label: "기타",
data: [15481, 6959, 1938, 1967],
backgroundColor: "rgba(255, 178, 150, 0.9)", // 연한 주황(파스텔톤)
borderColor: "rgba(255,178,150,1)",
},
{
useEffect(() => {
const ymdFrom = "19010101";
const ymdTo = "20241231";
SewerageApi.getPplUseLength(ymdFrom, ymdTo, "SBA004")
.then((apiResult) => {
// getPplUseLength의 결과(apiResult)를
// setSewageData 함수의 인자로 넣어서 호출합니다.
// console.log(apiResult);
setSewageData(apiResult);
})
.catch((e) => console.error("오수관 데이터 조회 실패", e));
SewerageApi.getPplUseLength(ymdFrom, ymdTo, "SBA003")
.then(setStormwaterData)
.catch((e) => console.error("우수관 데이터 조회 실패", e));
SewerageApi.getPplLength(ymdFrom, ymdTo)
.then((apiResult) => {
// getPplLength 결과(apiResult)를
// setTotalData 함수의 인자로 넣어서 호출합니다.
// console.log(apiResult);
setTotalData(apiResult);
})
.catch((e) => console.error("전체 관로 데이터 조회 실패", e));
}, []);
// 데이터가 변경될 때마다 차트를 다시 계산
useEffect(() => {
// 오수관 또는 우수관 데이터가 없으면 차트를 그리지 않음
if (sewageData.length === 0 && stormwaterData.length === 0) return;
const allZones = new Set([
...sewageData.map(d => d.zoneNm),
...stormwaterData.map(d => d.zoneNm),
...totalData.map(d => d.zoneNm)
]);
const labels = Array.from(allZones);
const dataByZone: { [key: string]: { total: number; sewage: number; stormwater: number } } = {};
labels.forEach(zone => {dataByZone[zone] = { total: 0, sewage: 0, stormwater: 0 }; });
totalData.forEach(item => {
if (!dataByZone[item.zoneNm]) {
dataByZone[item.zoneNm] = { total: 0, sewage: 0, stormwater: 0 };
}
dataByZone[item.zoneNm]!.total = item.sumPrlgVl;
});
sewageData.forEach(item => {
if (!dataByZone[item.zoneNm]) {
dataByZone[item.zoneNm] = { total: 0, sewage: 0, stormwater: 0 };
}
dataByZone[item.zoneNm]!.sewage = item.sumPrlgVl;
});
stormwaterData.forEach(item => {
if (!dataByZone[item.zoneNm]) {
dataByZone[item.zoneNm] = { total: 0, sewage: 0, stormwater: 0 };
}
dataByZone[item.zoneNm]!.stormwater = item.sumPrlgVl;
});
const datasets: ChartDataSet[] = [];
// 오수관로 데이터셋
datasets.push({
label: "오수관로",
data: [15878, 16336, 5670, 8073],
backgroundColor: "rgba(180, 240, 190, 0.95)", // 연한 초록(파스텔톤)
borderColor: "rgba(180,240,190,1)",
},
{
data: labels.map(zone => dataByZone[zone]?.sewage),
backgroundColor: "rgba(180, 240, 190, 0.95)",
});
// 우수관로 데이터셋
datasets.push({
label: "우수관로",
data: [12892, 10491, 4280, 4838],
backgroundColor: "rgba(170, 210, 245, 0.95)", // 연한 파랑(파스텔톤)
borderColor: "rgba(170,210,245,1)",
},
];
data: labels.map(zone => dataByZone[zone]?.stormwater),
backgroundColor: "rgba(170, 210, 245, 0.95)",
});
// 전체 데이터가 도착했다면 '기타' 데이터셋을 계산하고 추가
if (totalData.length > 0) {
datasets.unshift({ // '기타'를 맨 아래에 쌓기 위해 unshift 사용
label: "기타",
data: labels.map(zone => {
const zoneData = dataByZone[zone] ?? { total: 0, sewage: 0, stormwater: 0 };
return zoneData.total - zoneData.sewage - zoneData.stormwater;
}),
backgroundColor: "rgba(255, 178, 150, 0.9)",
});
}
setChartData({ labels, datasets });
}, [sewageData, stormwaterData, totalData]);
// 총합(라벨당) 계산
const totals = labels.map((_, i) =>
datasets.reduce((s, ds) => s + (ds.data[i] as number), 0),
const totals = chartData.labels.map((_, i) =>
chartData.datasets.reduce((s, ds) => s + (ds.data[i] as number), 0)
);
const options: ChartOptions<"bar"> = {
@@ -78,7 +164,7 @@ export default function SnPplStatus() {
onComplete: function (animation) {
const chart = animation.chart;
const ctx = chart.ctx;
const lastMeta = chart.getDatasetMeta(datasets.length - 1);
const lastMeta = chart.getDatasetMeta(chartData.datasets.length - 1);
ctx.save();
ctx.fillStyle = "#111";
ctx.font = "gray bold 12px Arial";
@@ -98,7 +184,7 @@ export default function SnPplStatus() {
},
};
const data = { labels, datasets };
//const data = { labels, datasets };
return (
<div className="card card-border">
@@ -108,7 +194,7 @@ export default function SnPplStatus() {
</div>
<div className="h-60 p-4">
<div style={{ width: "100%", maxWidth: 400, height: 150 }}>
<Bar options={options} data={data} />
<Bar options={options} data={chartData} />
</div>
</div>
</div>
+2
View File
@@ -70,6 +70,8 @@ export const ENDPOINT = {
POST_EVALUATION: "/board-service/api/v1/pstEvl",
},
SEWERAGE_SERVICE: {
PPL_LENGTH: "/sewerage-service/api/v1/ms/pplLength",
PPL_USE_LENGTH: "/sewerage-service/api/v1/ms/pplUseLength",
PPL_COUNTS: "/sewerage-service/api/v1/ms/pplCounts",
OLD_PPL_COUNTS: "/sewerage-service/api/v1/ms/oldPplCounts",
},