cm: 노후 하수관로 현황 수정

This commit is contained in:
2026-09-29 11:17:52 +09:00
parent f5331d4c50
commit 9bb1069d26
3 changed files with 105 additions and 81 deletions
+97 -33
View File
@@ -35,26 +35,7 @@ const initialUsePplChartData: OldPplChartData = {
};
const initialOldPplChartData: OldPplChartData = {
labels: ["금강", "한강", "낙동강", "영산강"],
datasets: [
{
label: "20-25년",
data: [1140, 3181, 3100, 1662],
backgroundColor: "#3B82F6",
borderRadius: 4
},
{
label: "25-30년",
data: [1391, 3615, 1814, 1411],
backgroundColor: "#EF4444",
borderRadius: 4
},
{
label: "30년이상",
data: [2014, 10303, 9831, 2000],
backgroundColor: "#F87171",
borderRadius: 4
}
]
datasets: []
};
type OldPplStoreState = {
chartData: OldPplChartData;
@@ -72,10 +53,10 @@ export const useUsePplStore = create<OldPplStoreState>((set) => ({
export const initailMapOldPplData: mapPplOldType = {
colTitle: { stofcbrCount: "우수토실개소수", totalLen: "총연장" },
data: [
{ name: "금강", totalLen: 4545, stofcbrCount: 0 },
{ name: "한강", totalLen: 17099, stofcbrCount: 0 },
{ name: "낙동강", totalLen: 14745, stofcbrCount: 0 },
{ name: "영산강", totalLen: 5073, stofcbrCount: 0 },
{ name: "금강", totalLen: 0, stofcbrCount: 0 },
{ name: "한강", totalLen: 0, stofcbrCount: 0 },
{ name: "낙동강", totalLen: 0, stofcbrCount: 0 },
{ name: "영산강", totalLen: 0, stofcbrCount: 0 },
]
};
type mapPplOldState = {
@@ -99,6 +80,93 @@ export const useMapOldPplStore = create<mapPplOldState>((set) => ({
})),
}));
const DEFAULT_BASIN_ORDER = ["금강", "한강", "낙동강", "영산강"];
const YEAR_GROUP_START_COLOR = "#3B82F6";
const YEAR_GROUP_END_COLOR = "#EF4444";
/**
* 시작색과 종료색 사이를 RGB 기준으로 보간해 구간 수만큼 색상을 생성합니다.
* 구간이 하나면 시작색을, 둘 이상이면 첫 구간은 파란색·마지막 구간은 빨간색으로 고정합니다.
*/
function buildYearGroupColors(count: number): string[] {
if (count <= 0) return [];
if (count === 1) return [YEAR_GROUP_START_COLOR];
const parseHex = (color: string) => [
Number.parseInt(color.slice(1, 3), 16),
Number.parseInt(color.slice(3, 5), 16),
Number.parseInt(color.slice(5, 7), 16),
];
const toHex = (value: number) => Math.round(value).toString(16).padStart(2, "0");
const start = parseHex(YEAR_GROUP_START_COLOR);
const end = parseHex(YEAR_GROUP_END_COLOR);
return Array.from({ length: count }, (_, index) => {
const ratio = index / (count - 1);
return `#${start.map((channel, channelIndex) => {
const endChannel = end[channelIndex] ?? channel;
return toHex(channel + (endChannel - channel) * ratio);
}).join("")}`;
});
}
/**
* 별도 정렬순서가 없는 MV 계약에서 구간명의 첫 숫자를 기준으로 표시순서를 결정합니다.
* 숫자가 없는 비정형 구간은 숫자 구간 뒤에 한글 오름차순으로 배치합니다.
*/
function compareYearGroups(left: string, right: string): number {
const leftNumber = left.match(/\d+/)?.[0];
const rightNumber = right.match(/\d+/)?.[0];
if (leftNumber !== undefined && rightNumber !== undefined) {
const numberDifference = Number(leftNumber) - Number(rightNumber);
if (numberDifference !== 0) return numberDifference;
} else if (leftNumber !== undefined) {
return -1;
} else if (rightNumber !== undefined) {
return 1;
}
return left.localeCompare(right, "ko");
}
/**
* MV 응답의 yearGroup 목록으로 노후관로 차트 데이터를 동적으로 생성합니다.
* 구간명이나 구간 개수가 바뀌어도 고정 매핑 없이 동일한 로직을 사용합니다.
*/
export function buildOldPplChartData(items: PplLengthContent[]): OldPplChartData {
const responseZones = [...new Set(items.map((item) => item.zoneNm).filter(Boolean))];
const labels = [
...DEFAULT_BASIN_ORDER,
...responseZones.filter((zone) => !DEFAULT_BASIN_ORDER.includes(zone)).sort((left, right) => left.localeCompare(right, "ko")),
];
const yearGroups = [
...new Set(
items
.map((item) => item.yearGroup?.trim())
.filter((yearGroup): yearGroup is string => Boolean(yearGroup)),
),
].sort(compareYearGroups);
const colors = buildYearGroupColors(yearGroups.length);
const lengthByZoneAndGroup = new Map<string, number>();
items.forEach((item) => {
const yearGroup = item.yearGroup?.trim();
if (!item.zoneNm || !yearGroup) return;
lengthByZoneAndGroup.set(`${item.zoneNm}\u0000${yearGroup}`, Number(item.sumPrlgVl) || 0);
});
return {
labels,
datasets: yearGroups.map((yearGroup, index) => ({
label: yearGroup,
data: labels.map((zone) => lengthByZoneAndGroup.get(`${zone}\u0000${yearGroup}`) ?? 0),
backgroundColor: colors[index] ?? YEAR_GROUP_START_COLOR,
borderRadius: 4,
})),
};
}
/**
* OldPplChartData를 지도 팝업 데이터 형태인 mapPplOldItem[]으로 변환합니다.
* @param chartData 차트 데이터
@@ -108,15 +176,11 @@ export function transformChartDataToMapData(chartData: OldPplChartData): mapPplO
const mapData: mapPplOldItem[] = [];
const { labels, datasets } = chartData;
const pplLen2025Data = datasets.find(d => d.label === '20-25년')?.data || [];
const pplLen2530Data = datasets.find(d => d.label === '25-30년')?.data || [];
const pplLen30Data = datasets.find(d => d.label === '30년이상')?.data || [];
labels.forEach((label, index) => {
const pplLen2025 = pplLen2025Data[index] || 0;
const pplLen2530 = pplLen2530Data[index] || 0;
const pplLen30 = pplLen30Data[index] || 0;
const totalLen = pplLen2025 + pplLen2530 + pplLen30;
const totalLen = datasets.reduce((sum, dataset) => {
const value = dataset.data[index];
return sum + (typeof value === "number" ? value : 0);
}, 0);
mapData.push({
name: label,
@@ -181,7 +245,7 @@ export class SewerageApiService implements SewerageService {
}));
return list;
}
// 유역별 노후도별 관로 길이(km) MV조회 : yearGroup - 20년~25년미만,25년~30년미만,30년이상
// 유역별 노후도별 관로 길이(km) MV조회: yearGroup은 MV가 제공하는 동적 구간명
public async getMvOldPplLength(): Promise<PplLengthContent[]> {
const response = await client.get<{
PplLengthList: PplLengthContent[];
@@ -33,10 +33,10 @@ const mapOperFcltData: mapOperFcltType = {
const mapPplOldData: mapPplOldType = {
colTitle: {stofcbrCount: "우수토실개소수", totalLen: "총연장" },
data: [
{name: "금강", totalLen: 4545, stofcbrCount: 0},
{name: "한강", totalLen: 17099, stofcbrCount: 0},
{name: "낙동강", totalLen: 14745, stofcbrCount: 0},
{name: "영산강", totalLen: 5073, stofcbrCount: 0},
{name: "금강", totalLen: 0, stofcbrCount: 0},
{name: "한강", totalLen: 0, stofcbrCount: 0},
{name: "낙동강", totalLen: 0, stofcbrCount: 0},
{name: "영산강", totalLen: 0, stofcbrCount: 0},
]
};
const mapReuseMapData: mapReuseMapType = {
+4 -44
View File
@@ -15,8 +15,8 @@ import {
} from "chart.js";
import { Bar } from "react-chartjs-2";
import { Button } from "@common/components";
import type { PplLengthContent, ChartDataSet, OldPplChartData } from "../model/sn-interface";
import { SewerageApi, useOldPplStore,useMapOldPplStore, transformChartDataToMapData } from "../api/sn-service";
import type { PplLengthContent } from "../model/sn-interface";
import { buildOldPplChartData, SewerageApi, useOldPplStore,useMapOldPplStore, transformChartDataToMapData } from "../api/sn-service";
import { useDragHandle } from "./sortable-panel";
@@ -41,45 +41,7 @@ export default function SnOldPpl() {
useEffect(() => {
if (oldPplLenData.length === 0) return;
// 유역 목록 추출 (중복 제거)
const allZones = [...new Set(oldPplLenData.map(d => d.zoneNm))];
// yearGroup 매핑: API 응답값 -> 차트 label
const yearGroupMap: { [key: string]: { label: string; color: string } } = {
"20년~25년미만": { label: "20-25년", color: "#3B82F6" },
"25년~30년미만": { label: "25-30년", color: "#EF4444" },
"30년이상": { label: "30년이상", color: "#F87171" },
};
// 유역별, 노후년도별 데이터 구조화
const dataByZone: { [zone: string]: { [yearGroup: string]: number } } = {};
allZones.forEach(zone => {
dataByZone[zone] = {};
});
oldPplLenData.forEach(item => {
let zoneData = dataByZone[item.zoneNm];
if (!zoneData) {
zoneData = {};
dataByZone[item.zoneNm] = zoneData;
}
if (item.yearGroup) {
zoneData[item.yearGroup] = item.sumPrlgVl;
}
});
// 데이터셋 생성
const newDatasets: ChartDataSet[] = Object.entries(yearGroupMap).map(([yearGroup, { label, color }]) => ({
label,
data: allZones.map(zone => dataByZone[zone]?.[yearGroup] ?? 0),
backgroundColor: color,
borderRadius: 4,
}));
const newChartData: OldPplChartData = {
labels: allZones,
datasets: newDatasets,
};
const newChartData = buildOldPplChartData(oldPplLenData);
setChartData(newChartData);
@@ -110,9 +72,7 @@ export default function SnOldPpl() {
y: {
beginAtZero: true,
min: 0,
max: 10000,
ticks: {
stepSize: 2000,
callback: function (value) {
return Number(value).toLocaleString() + 'km';
}
@@ -192,4 +152,4 @@ export default function SnOldPpl() {
</div>
</div>
);
}
}