This commit is contained in:
yschoi
2025-09-09 10:39:27 +09:00
46 changed files with 1892 additions and 1885 deletions
+82 -253
View File
@@ -1,237 +1,43 @@
'use client';
import { regionData, regionIdMap } from '@/app/common/comFunction';
import { useDmStore } from '@/app/store';
import type { DmStatusDataType } from '@/app/store/wrStatus/type';
import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components';
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
import React, { useEffect, useRef, useState, useCallback } from 'react';
interface RegionData {
name: string;
x: number;
y: number;
values: number[];
interface WrStatusChartProps {
chartForm: string;
chartConfig: ChartConfig;
chartData: any;
chartTitle: string;
chartClassName?: string;
graphSize?: number;
yFieldText? : string | undefined;
yFieldText2? : string | undefined;
showLegend? : boolean;
setXAxisYearTick ?: boolean;
}
interface TableData {
region: string;
sewage: string;
rainwater: string;
reclaimed: string;
thermal: string;
}
export default function WrStatusPage() {
const WrStatusPage: React.FC = () => {
const { dmStatus, getDmStatusData} = useDmStore((state) => state);
const { statusData,facilityTotals ,chartData} = dmStatus;
const [selectedRegion, setSelectedRegion] = useState<string | null>(null);
const [hoveredRegion, setHoveredRegion] = useState<string | null>(null);
const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null);
const [chartOption, setChartOption] = useState<WrStatusChartProps>({} as WrStatusChartProps)
const svgContainerRef = useRef<HTMLDivElement>(null);
const [chartData] = useState<any[]>([
{
label : '서울',
하수 : 4,
빗물 : 816,
중수도 : 371,
온배수 : 0
},
{
label : '부산',
하수 : 13,
빗물 : 68,
중수도 : 20,
온배수 : 1
},
{
label : '대구',
하수 : 9,
빗물 : 294,
중수도 : 14,
온배수 : 2
},
{
label : '인천',
하수 : 17,
빗물 : 165,
중수도 : 35,
온배수 : 15
},
{
label : '광주',
하수 : 3,
빗물 : 74,
중수도 : 15,
온배수 : 15
},
{
label : '대전',
하수 : 2,
빗물 : 52,
중수도 : 11,
온배수 : 20
},
{
label : '울산',
하수 : 8,
빗물 : 111,
중수도 : 19,
온배수 : 30
},
{
label : '세종',
하수 : 9,
빗물 : 65,
중수도 : 0,
온배수 : 0
},
{
label : '경기',
하수 : 176,
빗물 : 1009,
중수도 : 252,
온배수 : 40
},
{
label : '강원',
하수 : 69,
빗물 : 69,
중수도 : 22,
온배수 : 0
},
{
label : '충북',
하수 : 38,
빗물 : 17,
중수도 : 91,
온배수 : 0
},
{
label : '충남',
하수 : 67,
빗물 : 54,
중수도 : 188,
온배수 : 0
},
{
label : '전북',
하수 : 55,
빗물 : 33,
중수도 : 220,
온배수 : 0
},
{
label : '전남',
하수 : 83,
빗물 : 16,
중수도 : 75,
온배수 : 0
},
{
label : '경북',
하수 : 81,
빗물 : 37,
중수도 : 70,
온배수 : 0
},
{
label : '경남',
하수 : 75,
빗물 : 27,
중수도 : 140,
온배수 : 0
},
{
label : '제주',
하수 : 8,
빗물 : 47,
중수도 : 64,
온배수 : 0
}
]);
const chartConfig = chartData.reduce((config, element) => {
config[element.label] = {
label: element.label,
color : "hsl(var(--chart-1))"
};
return config;
}, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig;
const chartOption = {
chartForm : CHART_FORM.BAR,
chartConfig : chartConfig,
chartData : chartData,
chartTitle : '',
chartClassName : "h-[300px] w-full",
graphSize : 40,
yFieldText : '시설(개소)',
showLegend : true
}
const colorLegend = [
{ label: '하수', color: '#FF6B6B', key: 'sewage' },
{ label: '빗물', color: '#4ECDC4', key: 'rainwater' },
{ label: '중수도', color: '#45B7D1', key: 'reclaimed' },
{ label: '온배수', color: '#FED16A', key: 'thermal' },
];
const facilityTotalsColor = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A'];
const regionIdMap: Record<string, string> = {
서울: '서울특별시',
부산: '부산광역시',
대구: '대구광역시',
인천: '인천광역시',
광주: '광주광역시',
대전: '대전광역시',
울산: '울산광역시',
세종: '세종특별자치시',
경기: '경기도',
강원: '강원특별자치도',
충북: '충청북도',
충남: '충청남도',
전북: '전라북도',
전남: '전라남도',
경북: '경상북도',
경남: '경상남도',
제주: '제주특별자치도'
};
const tableData: TableData[] = [
{ region: '전국', sewage: '717(1,137,538)', rainwater: '3544(13,945)', reclaimed: '1018(222,476)', thermal: '123(492)' },
{ region: '서울특별시', sewage: '4(114,908)', rainwater: '816(430)', reclaimed: '371(341)', thermal: '0' },
{ region: '부산광역시', sewage: '13(146,197)', rainwater: '68(335)', reclaimed: '20(512)', thermal: '1(18)' },
{ region: '대구광역시', sewage: '9(102,380)', rainwater: '294(133)', reclaimed: '14(44)', thermal: '2(12)' },
{ region: '인천광역시', sewage: '17(57,691)', rainwater: '165(642)', reclaimed: '35(7,859)', thermal: '15(32)' },
{ region: '광주광역시', sewage: '3(28,738)', rainwater: '74(23)', reclaimed: '15(450)', thermal: '15(6)' },
{ region: '대전광역시', sewage: '2(11,966)', rainwater: '52(2)', reclaimed: '11(492)', thermal: '20(100)' },
{ region: '울산광역시', sewage: '8(36,076)', rainwater: '111(39)', reclaimed: '19(28,904)', thermal: '30(130)' },
{ region: '세종특별자치시', sewage: '9(2,555)', rainwater: '65(16)', reclaimed: '0(0)', thermal: '0' },
{ region: '경기도', sewage: '176(313,298)', rainwater: '1009(4,735)', reclaimed: '252(77,096)', thermal: '40(143)' },
{ region: '강원특별자치도', sewage: '69(19,677)', rainwater: '69(4)', reclaimed: '22(447)', thermal: '0' },
{ region: '충청북도', sewage: '38(15,607)', rainwater: '17(91)', reclaimed: '91(2,211)', thermal: '0' },
{ region: '충청남도', sewage: '67(41,368)', rainwater: '54(77)', reclaimed: '188(50,434)', thermal: '0' },
{ region: '전라북도', sewage: '55(42,671)', rainwater: '33(199)', reclaimed: '220(9,453)', thermal: '0' },
{ region: '전라남도', sewage: '83(23,560)', rainwater: '16(885)', reclaimed: '75(15,412)', thermal: '0' },
{ region: '경상북도', sewage: '81(129,081)', rainwater: '37(341)', reclaimed: '70(27,818)', thermal: '0' },
{ region: '경상남도', sewage: '75(50,101)', rainwater: '27(244)', reclaimed: '140(343)', thermal: '0' },
{ region: '제주특별자치도', sewage: '8(1,666)', rainwater: '47(5,746)', reclaimed: '64(660)', thermal: '0' },
];
const regionData: RegionData[] = useMemo(() => [
{ name: '서울', x: 170, y: 140, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '인천', x: 120, y: 150, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '경기', x: 210, y: 210, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '강원', x: 330, y: 120, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '충북', x: 260, y: 280, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '충남', x: 100, y: 320, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '세종', x: 180, y: 305, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '대전', x: 195, y: 360, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '전북', x: 170, y: 450, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '전남', x: 160, y: 600, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '광주', x: 120, y: 530, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '경북', x: 400, y: 320, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '경남', x: 310, y: 530, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '대구', x: 350, y: 450, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '울산', x: 450, y: 500, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '부산', x: 420, y: 550, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '제주', x: 60, y: 750, values: [0.3, 0.3, 0.2, 0.2] },
], []);
const handleMarkerClick = (regionId: string) => {
if (selectedRegion === regionId) {
@@ -241,7 +47,6 @@ const WrStatusPage: React.FC = () => {
}
};
const createDonutMarker = useCallback((svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => {
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A'];
const radius = 26;
@@ -252,7 +57,6 @@ const WrStatusPage: React.FC = () => {
console.warn(`Invalid region name: ${name}`);
return;
}
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('transform', `translate(${x}, ${y})`);
g.setAttribute('id', `marker-${name}`);
@@ -336,6 +140,13 @@ const WrStatusPage: React.FC = () => {
svg.appendChild(g);
}, [handleMarkerClick, regionIdMap]);
useEffect(() => {
getDmStatusData('2023');
},[])
useEffect(() => {
const loadSVG = async () => {
try {
@@ -393,6 +204,31 @@ const WrStatusPage: React.FC = () => {
});
}
}, [selectedRegion]);
useEffect(() => {
if(chartData.length>0){
const chartConfig = chartData.reduce((config, element) => {
config[element.label] = {
label: element.label,
color : "hsl(var(--chart-1))"
};
return config;
}, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig;
const Option = {
chartForm : CHART_FORM.BAR,
chartConfig : chartConfig,
chartData : chartData,
chartTitle : '',
chartClassName : "h-[300px] w-full",
graphSize : 40,
yFieldText : '시설(개소)',
showLegend : true
}
setChartOption(Option)
}else{
setChartOption({} as WrStatusChartProps)
}
}, [chartData]);
return (
<div className="flex h-screen bg-[#F2F2F2] text-gray-800">
@@ -401,7 +237,6 @@ const WrStatusPage: React.FC = () => {
{/* 상단 제목 */}
<div className="mb-6 flex justify-between items-center">
<h2 className="text-2xl font-bold">물재이용 현황</h2>
<div className="text-sm text-gray-500">관리자 ▼</div>
</div>
{/* 콘텐츠 본문 */}
@@ -439,11 +274,11 @@ const WrStatusPage: React.FC = () => {
<div className="font-semibold mb-1">{hoveredRegion}</div>
<div className="space-y-1">
{colorLegend.map(({ label, color, key }) => (
<div key={key} className="flex items-center space-x-2">
<div key={key+'' +color} className="flex items-center space-x-2">
<span className="text-[12px] inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: color }}></span>
<span className="text-[12px]">
{label}:{' '}
{tableData.find(d => d.region === hoveredRegion)?.[key as keyof TableData] ?? '-'}
{statusData?.find(d => d.region === hoveredRegion)?.[key as keyof DmStatusDataType] || '-'}
</span>
</div>
))}
@@ -453,22 +288,12 @@ const WrStatusPage: React.FC = () => {
{/* 범례 */}
<div className="absolute bottom-2 right-2 bg-white rounded-lg shadow-md p-4 text-sm space-y-2">
<div className="flex items-center space-x-2">
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FF6B6B'}}></span>
<span className="text-[12px] text-gray-900">하수재이용시설 (총 717개소)</span>
</div>
<div className="flex items-center space-x-2">
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#4ECDC4'}}></span>
<span className="text-[12px] text-gray-900">빗물이용시설 (총 3,730개소)</span>
</div>
<div className="flex items-center space-x-2">
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#45B7D1'}}></span>
<span className="text-[12px] text-gray-900">중수도시설 (총 1,074개소)</span>
</div>
<div className="flex items-center space-x-2">
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FED16A'}}></span>
<span className="text-[12px] text-gray-900">온배수시설 (총 201개소)</span>
</div>
{facilityTotals?.map((item, index) => (
<div key ={'fcltTotal' +index}className="flex items-center space-x-2">
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor:facilityTotalsColor[index]}}></span>
<span className="text-[12px] text-gray-900">{item}</span>
</div>
)) }
</div>
</div>
</div>
@@ -479,10 +304,10 @@ const WrStatusPage: React.FC = () => {
{/* 재이용시설현황 표 */}
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden">
{/* 타이틀 + 버튼 영역 */}
<div className="flex justify-between items-center border-b border-[#DBE2E6] px-4 py-3">
<div className="flex justify-between items-center border-b border-[#436d85] px-4 py-3">
<div>
<h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3>
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황(개소)</p>
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황[개소(천m³/년)]</p>
</div>
</div>
<div className="p-4">
@@ -493,14 +318,18 @@ const WrStatusPage: React.FC = () => {
<tr>
<th className="text-[12px] font-semibold px-4 py-2">번호</th>
<th className="text-[12px] font-semibold px-4 py-2">시도</th>
<th className="text-[12px] font-semibold px-4 py-2">하수(이용량,천𝓂³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">하수재이용시설</th>
<th className="text-[12px] font-semibold px-4 py-2">빗물이용시설</th>
<th className="text-[12px] font-semibold px-4 py-2">중수도시설</th>
<th className="text-[12px] font-semibold px-4 py-2">온배수시설</th>
{/* <th className="text-[12px] font-semibold px-4 py-2">하수(이용량,천𝓂³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">빗물(이용량,천𝓂³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">중수도(이용량,천𝓂³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">온배수(이용량,천𝓂³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">온배수(이용량,천𝓂³/년)</th> */}
</tr>
</thead>
<tbody className="divide-y">
{tableData.map((row, index) => (
{statusData?.map((row, index) => (
<tr key={index} className="hover:bg-gray-50">
<td className="text-[12px] px-4 py-2 text-center">
{index+1}
@@ -526,16 +355,18 @@ const WrStatusPage: React.FC = () => {
</div>
<div className="p-4 bg-[#f9fbfd]">
<div className="h-[305px] text-gray-500">
<MainChart
chartForm={chartOption.chartForm}
chartConfig={chartOption.chartConfig}
chartData={chartOption.chartData}
chartTitle={chartOption.chartTitle}
chartClassName={chartOption.chartClassName}
graphSize={chartOption.graphSize}
yFieldText={chartOption.yFieldText}
showLegend={chartOption.showLegend}
/>
{chartOption?
<MainChart
chartForm={chartOption.chartForm}
chartConfig={chartOption.chartConfig}
chartData={chartOption.chartData}
chartTitle={chartOption.chartTitle}
chartClassName={chartOption.chartClassName}
graphSize={chartOption.graphSize}
yFieldText={chartOption.yFieldText}
showLegend={chartOption.showLegend}
/>
:<></>}
</div>
</div>
</div>
@@ -545,5 +376,3 @@ const WrStatusPage: React.FC = () => {
</div>
);
};
export default WrStatusPage;
+74
View File
@@ -0,0 +1,74 @@
import type { DmStatusChartType, DmStatusDataType, RegionData, } from "../store/wrStatus/type";
export const regionData:RegionData[] = [
{ name: '서울', x: 170, y: 140, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '인천', x: 120, y: 150, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '경기', x: 210, y: 210, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '강원', x: 330, y: 120, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '충북', x: 260, y: 280, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '충남', x: 100, y: 320, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '세종', x: 180, y: 305, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '대전', x: 195, y: 360, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '전북', x: 170, y: 450, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '전남', x: 160, y: 600, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '광주', x: 120, y: 530, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '경북', x: 400, y: 320, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '경남', x: 310, y: 530, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '대구', x: 350, y: 450, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '울산', x: 450, y: 500, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '부산', x: 420, y: 550, values: [0.3, 0.3, 0.2, 0.2] },
{ name: '제주', x: 60, y: 750, values: [0.3, 0.3, 0.2, 0.2] },
]
export const regionIdMap: Record<string, string> = {
서울: '서울특별시',
부산: '부산광역시',
대구: '대구광역시',
인천: '인천광역시',
광주: '광주광역시',
대전: '대전광역시',
울산: '울산광역시',
세종: '세종특별자치시',
경기: '경기도',
강원: '강원특별자치도',
충북: '충청북도',
충남: '충청남도',
전북: '전라북도',
전남: '전라남도',
경북: '경상북도',
경남: '경상남도',
제주: '제주특별자치도'
};
const makeRegion = (region: string) => {
if (region === '서울특별시') return '서울'
if (region === '부산광역시') return '부산'
if (region === '대구광역시') return '대구'
if (region === '인천광역시') return '인천'
if (region === '광주광역시') return '광주'
if (region === '대전광역시') return '대전'
if (region === '울산광역시') return '울산'
if (region === '세종특별자치시') return '세종'
if (region === '경기도') return '경기'
if (region === '강원특별자치도') return '강원'
if (region === '충청북도') return '충북'
if (region === '충청남도') return '충남'
if (region === '전라북도') return '전북'
if (region === '전라남도') return '전남'
if (region === '경상북도') return '경북'
if (region === '경상남도') return '경남'
if (region === '제주특별자치도') return '제주'
return region;
}
export const changeStatusMapData= (data: DmStatusDataType[]) : DmStatusChartType[]=> {
return data.map((item) => ({
label: makeRegion(item.region??''),
하수: item.sewage.split('(')[0]??'',
빗물: item.rainwater.split('(')[0]??'',
중수도: item.reclaimed.split('(')[0] ??'',
온배수: item.thermal.split('(')[0] ??'',
}));
}
+38 -19
View File
@@ -1,29 +1,48 @@
import { createStore } from 'zustand/vanilla';
import { CommonReducer, CommonState, type CommonStore, type CommonType } from './common';
import { SdmReducer, SdmState, type SdmStore, type SdmType } from './sdmMng';
import { dmMapState, type DmMapType, type DmMapStore, DmMapReducer } from './map';
import { tokenManager } from '@/src/entities/auth/lib/token-manager';
import { createStore } from "zustand/vanilla";
import {
CommonReducer,
CommonState,
type CommonStore,
type CommonType,
} from "./common";
import { SdmReducer, SdmState, type SdmStore, type SdmType } from "./sdmMng";
import {
dmMapState,
type DmMapType,
type DmMapStore,
DmMapReducer,
} from "./map";
import { tokenManager } from "@/src/entities/auth/lib/token-manager";
import {
DmStatusReducer,
dmStatusState,
type DmStatusStore,
type DmStatusType,
} from "./wrStatus";
export type DmStore = CommonStore & SdmStore &DmMapStore ;
export type DmState = CommonType & SdmType &DmMapType;
export type DmStore = CommonStore & SdmStore & DmMapStore & DmStatusStore;
export type DmState = CommonType & SdmType & DmMapType & DmStatusType;
export const StoreZus = (initState: DmState= {
...CommonState,
...SdmState,
...dmMapState,
}) => {
export const StoreZus = (
initState: DmState = {
...CommonState,
...SdmState,
...dmMapState,
...dmStatusState,
}
) => {
return createStore<DmStore>()((set) => {
if (typeof window !== "undefined") {
tokenManager.setToken("site-id-key", 'DM');
tokenManager.setToken("site-theme-key", 'green');
tokenManager.setToken("site-id-key", "DM");
tokenManager.setToken("site-theme-key", "green");
}
return {
...initState,
...CommonReducer(set),
...SdmReducer(set),
...DmMapReducer(set)
}
})
}
...DmMapReducer(set),
...DmStatusReducer(set),
};
});
};
+63
View File
@@ -0,0 +1,63 @@
import { changeStatusMapData } from "@/app/common/comFunction";
import { fetchType, getFetch } from "@/components/ui/api";
import type {
DmStatusDataType,
DmStatusChartType,
} from "./type";
export type DmStatusActions = {
getDmStatusData(year: string): void;
};
export type DmStatusType = {
dmStatus: {
statusData: DmStatusDataType[];
facilityTotals: string[];
chartData: DmStatusChartType[];
};
};
export const dmStatusState: DmStatusType = {
dmStatus: {
statusData: [],
facilityTotals: [],
chartData: [],
},
};
export type DmStatusStore = DmStatusType & DmStatusActions;
export const DmStatusReducer = (set: any): DmStatusActions => {
return {
getDmStatusData: async (year: string) => {
const data = await getFetch(fetchType.WR, "/daemin/" + year);
let isData:DmStatusDataType[] =[]
if(data.data&&data.data.length>0){
isData=data.data
}else{
fetch(`data/wrStatusData.json`).then((res) => res.json()).then((json) => isData=json)//만약 백엔드에서 못가져올경우 방어책
}
const orgin: DmStatusDataType[] = isData;
const chartData = changeStatusMapData(orgin);
const facilityTotals = [
"하수재이용시설" + "(총" + orgin[0]?.sewage.split("(")[0] + "개소)",
"빗물재이용시설" + "(총" + orgin[0]?.rainwater.split("(")[0] + "개소)",
"중수도재이용시설" +
"(총" +
orgin[0]?.reclaimed.split("(")[0] +
"개소)",
"온배수재이용시설" + "(총" + orgin[0]?.thermal.split("(")[0] + "개소)",
];
set((state: DmStatusType) => ({
...state,
dmStatus: {
...state.dmStatus,
statusData: data.data,
chartData: chartData,
facilityTotals: facilityTotals,
},
}));
},
};
};
+32
View File
@@ -0,0 +1,32 @@
export type DmStatusDataType = {
region: string;
sewage: string;
rainwater: string;
reclaimed: string;
thermal: string;
};
export type DmStatusFcltType = {
sewageFclt: string;
rainwaterFclt: string;
reclaimedFclt: string;
thermalFclt: string;
};
export type DmStatusChartType = {
label: string,
하수: string,
빗물: string,
중수도: string,
온배수: string,
}
export type RegionData = {
name: string;
x: number;
y: number;
values: number[];
}
+20
View File
@@ -0,0 +1,20 @@
[
{ "region": "전국", "sewage": "717(1,137,538)", "rainwater": "3544(13,945)", "reclaimed": "1018(222,476)", "thermal": "123(492)" },
{ "region": "서울특별시", "sewage": "4(114,908)", "rainwater": "816(430)", "reclaimed": "371(341)", "thermal": "0" },
{ "region": "부산광역시", "sewage": "13(146,197)", "rainwater": "68(335)", "reclaimed": "20(512)", "thermal": "1(18)" },
{ "region": "대구광역시", "sewage": "9(102,380)", "rainwater": "294(133)", "reclaimed": "14(44)", "thermal": "2(12)" },
{ "region": "인천광역시", "sewage": "17(57,691)", "rainwater": "165(642)", "reclaimed": "35(7,859)", "thermal": "15(32)" },
{ "region": "광주광역시", "sewage": "3(28,738)", "rainwater": "74(23)", "reclaimed": "15(450)", "thermal": "15(6)" },
{ "region": "대전광역시", "sewage": "2(11,966)", "rainwater": "52(2)", "reclaimed": "11(492)", "thermal": "20(100)" },
{ "region": "울산광역시", "sewage": "8(36,076)", "rainwater": "111(39)", "reclaimed": "19(28,904)", "thermal": "30(130)" },
{ "region": "세종특별자치시", "sewage": "9(2,555)", "rainwater": "65(16)", "reclaimed": "0(0)", "thermal": "0" },
{ "region": "경기도", "sewage": "176(313,298)", "rainwater": "1009(4,735)", "reclaimed": "252(77,096)", "thermal": "40(143)" },
{ "region": "강원특별자치도", "sewage": "69(19,677)", "rainwater": "69(4)", "reclaimed": "22(447)", "thermal": "0" },
{ "region": "충청북도", "sewage": "38(15,607)", "rainwater": "17(91)", "reclaimed": "91(2,211)", "thermal": "0" },
{ "region": "충청남도", "sewage": "67(41,368)", "rainwater": "54(77)", "reclaimed": "188(50,434)", "thermal": "0" },
{ "region": "전라북도", "sewage": "55(42,671)", "rainwater": "33(199)", "reclaimed": "220(9,453)", "thermal": "0" },
{ "region": "전라남도", "sewage": "83(23,560)", "rainwater": "16(885)", "reclaimed": "75(15,412)", "thermal": "0" },
{ "region": "경상북도", "sewage": "81(129,081)", "rainwater": "37(341)", "reclaimed": "70(27,818)", "thermal": "0" },
{ "region": "경상남도", "sewage": "75(50,101)", "rainwater": "27(244)", "reclaimed": "140(343)", "thermal": "0" },
{ "region": "제주특별자치도", "sewage": "8(1,666)", "rainwater": "47(5,746)", "reclaimed": "64(660)", "thermal": "0" }
]
@@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'
import InputSection from '../InputSection';
import { handleInputChange } from '../common';
import { getData } from "@/components/lib/api";
import useStore from '../../../../store/useStore';
import useStore from '@/store/useStore';
// 하수관로
const inputColumn = [
+6 -28
View File
@@ -8,37 +8,15 @@ import SearchModal from "./searchModal/searchModal";
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth";
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
import useSearchModalStore from "@/store/useSearchModalStore";
// 타입 정의 추가
export type LyrStngProps = {
lyr_sn: string;
lyr_nm: string;
upper_lyr_sn: string;
drawOrdr: string;
flterSql: string;
gobjTy: string;
lyrTpNm: string;
useYn: string;
minViewSize: string;
iconNm: string;
lblAtrbNm: string;
fontNm: string;
fontIntnColor: string;
fontExtnColor: string;
fontExtnWdth: string;
lnWdth: string;
lnColor: string;
polgnColor: string;
initEprssYn: string;
clickPsblYn: string;
};
const Map = () => {
const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore();
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태
const gisMaps = useRef<any | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부
const [isPlaying, setIsPlaying] = useState(false);
@@ -148,12 +126,12 @@ const Map = () => {
searchView={SearchModal}
detailView={FeatureClickEventPopup}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
setFeatureData={setFeatureData}
isOpen={isOpen}
setIsOpen={setIsOpen}
isSearchModalOpen={isSearchModalOpen}
setIsSearchModalOpen={setIsSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
searchBar={<ProcSearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} />}
waterDepth={showWaterDepth ? WaterDepth : undefined}
@@ -1,15 +1,13 @@
import React from 'react';
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { SelectBox } from '@common/business_components/ui';
import { Button, Label } from '@common/components';
import { Label } from '@common/components';
import SwagePpl from './swagePpl';
import SwagePrcsFclt from './swagePrcsFclt';
import SwagePrcsZone from './swagePrcsZone';
import SwageMhl from './swageMhl';
import SwagePmstn from './swagePmstn';
import Stofcbr from './stofcbr';
import { useSearchModal, type SearchModalProps } from './useSearchModal';
import useSearchModalStore from '../../../store/useSearchModalStore';
import useSearchModalStore from '@/store/useSearchModalStore';
// 기본정보 '구분' 선택 목록
@@ -22,7 +20,7 @@ export const BASIC_TYPES = [
{ val: "tb_ms_stofcbr", text: "우수토실" },
];
const BasicTab: React.FC<SearchModalProps> = (props) => {
const BasicTab: React.FC = () => {
const { basicType, setBasicType } = useSearchModalStore();
return (
@@ -45,12 +43,12 @@ const BasicTab: React.FC<SearchModalProps> = (props) => {
</div>
</div>
{/* 조회 조건 */}
{basicType === "tb_ms_swage_ppl" && (<SwagePpl {...props} />)} {/* 하수관로 */}
{basicType === "tb_il_swage_prcs_fclt" && (<SwagePrcsFclt {...props} />)} {/* 하수처리시설 */}
{basicType === "tb_ms_swage_prcs_zone" && (<SwagePrcsZone {...props} />)} {/* 하수처리구역 */}
{basicType === "tb_ms_swage_mhl" && (<SwageMhl {...props} />)} {/* 하수맨홀 */}
{basicType === "tb_ms_swage_pmstn" && (<SwagePmstn {...props} />)} {/* 하수펌프장 */}
{basicType === "tb_ms_stofcbr" && (<Stofcbr {...props} />)} {/* 우수토실 */}
{basicType === "tb_ms_swage_ppl" && (<SwagePpl />)} {/* 하수관로 */}
{basicType === "tb_il_swage_prcs_fclt" && (<SwagePrcsFclt />)} {/* 하수처리시설 */}
{basicType === "tb_ms_swage_prcs_zone" && (<SwagePrcsZone />)} {/* 하수처리구역 */}
{basicType === "tb_ms_swage_mhl" && (<SwageMhl />)} {/* 하수맨홀 */}
{basicType === "tb_ms_swage_pmstn" && (<SwagePmstn />)} {/* 하수펌프장 */}
{basicType === "tb_ms_stofcbr" && (<Stofcbr />)} {/* 우수토실 */}
</div>
);
};
@@ -1,82 +1,22 @@
import React from 'react';
import React, { use } from 'react';
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { SelectBox } from '@common/business_components/ui';
import { Button, Input, Label } from '@common/components';
import useStore from '../../../store/useStore';
import useSearchModalStore from '../../../store/useSearchModalStore';
import { useSearchModal, type SearchModalProps } from './useSearchModal';
// 운영정보 '구분' 선택 목록
export const WORK_TYPES = [
{ val: "['200', '201', '204']", text: "보수 및 갱신" },
{ val: "['100']", text: "점검 및 진단" },
{ val: "['203']", text: "청소 및 준설" },
];
// 운영정보 - 사업 현황
export const operationTableHeadlist = [
{ id: 'rowNumber', title: '일련번호' },
{ id: "bizInfoNm", title: "사업명" },
{ id: "flfmtBgngYmd", title: "수행시작일자" },
{ id: "flfmtEndYmd", title: "수행종료일자" },
{ id: "flfmtYr", title: "수행년도" },
{ id: "bizTypeCd", title: "사업유형" },
{ id: "bizAmt", title: "사업금액" },
];
// 운영정보 - 보수 및 갱신 결과표
export const operationMaintenanceTableHeadlist = [
{ id: 'rowNumber', title: '일련번호' },
{ id: "fcltMngNo", title: "관리번호" },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: "usgCd", title: "용도" },
// { id: "lgalDrblyr", title: "내용연수" },
// { id: "acqsPrmpc", title: "취득금액" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmtVl", title: "관경(mm)" },
{ id: "prlgVl", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
// { id: "instlYmd", title: "사업구분" },
];
// 운영정보 - 청소 및 준설 결과표
export const operationCleaningTableHeadlist = [
{ id: 'rowNumber', title: '일련번호' },
{ id: "fcltMngNo", title: "관리번호" },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: "usgCd", title: "용도" },
// { id: "lgalDrblyr", title: "내용연수" },
// { id: "acqsPrmpc", title: "취득금액" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmtVl", title: "관경(mm)" },
{ id: "prlgVl", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
];
// 운영정보 - 점검 및 진단 결과표
export const operationInspectionTableHeadlist = [
{ id: 'rowNumber', title: '일련번호' },
{ id: "fcltMngNo", title: "관리번호" },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: "usgCd", title: "용도" },
// { id: "riskEval", title: "위험평가" },
// { id: "acqsPrmpc", title: "취득금액" },
// { id: "lgalDrblyr", title: "내용연수" },
// { id: "remLife", title: "잔존수명" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmtVl", title: "관경(mm)" },
{ id: "prlgVl", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
{ id: "strc_stts_grd", title: "구조적 상태등급" },
{ id: "oper_stts_grd", title: "운영적 상태등급" },
];
import useSearchModal from './useSearchModal';
import useOperationTab from './useOperationTab';
const OperationTab: React.FC<SearchModalProps> = (props) => {
const OperationTab: React.FC = () => {
const { BIZ_TY_CD } = useStore();
const {
workType, setWorkType,
var1, setVar1, var2, setVar2,
yearFilter, setYearFilter,
} = useSearchModalStore();
const { handleSearch } = useSearchModal(props);
const { BIZ_TY_CD } = useStore();
const { handleSearch } = useSearchModal();
const { WORK_TYPES } = useOperationTab();
const BIZ_TY_CD_SELECT_ARRAY = BIZ_TY_CD.map(item => ({
val: item.lwpoCd,
@@ -1,108 +0,0 @@
'use client'
import React, { useEffect, useState } from 'react';
import LoadingOverlay from '@/components/ui/LoadingBar';
import { getFileDownload, getDcFiles, useDcFiles } from './searchModalReportApi';
import { type ReportDataFile } from './searchModalReportApi';
import { useReportFileModalStore } from './searchModalReportApi'; // 모달 상태 관리 스토어
import { UiTable } from '@common/business_components/ui';
import { UiModal } from "@common/business_components";
type ModalOpenProp = {
clsfSystmSn: string, // 자료관 기본계획 일련번호
}
// 하수도정비 기본계획 보고서 파일목록
export const headlist = [
{ id: "no", title: "NO" },
{ id: "fileTitle", title: "파일명" },
{ id: "regDt", title: "등록일자" },
];
const ReportFiles: React.FC<ModalOpenProp> = ({ clsfSystmSn }: ModalOpenProp) => {
const [isLoading, setIsLoading] = useState(false);
const [isResetPage, setIsResetPage] = useState<boolean>(false);
const [isResetSelection, setIsResetSelection] = useState<boolean>(false);
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useReportFileModalStore(); // 모달 상태 관리 스토어
const [files, setFiles] = useState<ReportDataFile[]>([]);
const [curPage, setCurPage] = useState(1);
const filesOrigin = useDcFiles(clsfSystmSn); // 자료관 기본계획 일련번호에 해당하는 파일 목록을 가져오기 위한 훅
useEffect(() => {
if (isModalOpen === true) {
getFiles(); // 파일 목록 조회
} else {
setFiles([]); // 모달이 닫힐 때 파일 목록 초기화
setCurPage(1); // 페이지를 1로 초기화
setIsResetPage(true); // 페이지를 초기화
setIsResetSelection(true); // 선택 초기화
}
}, [filesOrigin, isModalOpen, curPage]);
// 파일 목록 조회
const getFiles = async () => {
setIsLoading(true); // 로딩 시작
try {
//let data = await getDcFiles(clsfSystmSn);
let data = filesOrigin;
data = data?.slice((curPage - 1) * 10, curPage * 10); // paging
data = data?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 }));
setFiles(data);
} catch (error) {
console.error("파일 목록 조회 에러:", error);
} finally {
setIsLoading(false); // 로딩 종료
}
};
// cellClick 핸들러
const handleCellClick = (index: number|undefined) => {
if (typeof index === "number" && files[index]) {
const fileName = files[index].fileTitle;
const filePath = files[index].filePath;
if (files[index].fileSn !== "") {
getFileDownload(files[index].fileSn, fileName, filePath);
}
}
};
const renderBody = () => (
<>
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<UiTable
headName={(() => {
return (<>조회 결과 ({filesOrigin.length.toLocaleString('ko-KR')})건</>);
})()
}
tableData={files}
headlist={headlist}
pageSize={10}
total={filesOrigin.length}
cellClick={handleCellClick}
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
/>
</div>
</>
);
return (
<UiModal
open={isModalOpen}
onOpenChange={setIsModalOpen}
headerTitle=""
bodyClass={`
min-h-[50vh] h-[570px]
min-w-[35vw] w-[400px]
max-w-none max-h-none
overflow-visible
z-[5000]
flex items-start
`}
body={renderBody()}
noFooterYn='Y'
/>
);
};
export default React.memo(ReportFiles);
@@ -0,0 +1,96 @@
'use client'
import React, { useEffect, useState } from 'react';
import LoadingOverlay from '@/components/ui/LoadingBar';
import useReportTab from './useReportTab';
import { useReportTabStore } from '@/store/useReportTabStore';
import { UiTable } from '@common/business_components/ui';
import { UiModal } from "@common/business_components";
import useSearchModalStore from '@/store/useSearchModalStore';
type ModalOpenProp = { clsfSystmSn: string } // 자료관 기본계획 일련번호
const ReportFilesModal: React.FC<ModalOpenProp> = ({ clsfSystmSn }: ModalOpenProp) => {
// 하수도정비 기본계획 보고서 파일목록
const REPORT_FILES_HEADLIST = [
{ id: "no", title: "NO" },
{ id: "fileTitle", title: "파일명" },
{ id: "regDt", title: "등록일자" },
];
const {
curPage, setCurPage,
isLoading,
isResetPage,
isResetSelection,
setIsResetPage,
setIsResetSelection
} = useSearchModalStore();
const {
isOpen: isModalOpen,
files,
dcFiles: filesOrigin,
setIsOpen: setIsModalOpen,
setFiles,
fetchDcFiles
} = useReportTabStore();
const { getFiles, handleCellClick } = useReportTab();
useEffect(() => {
fetchDcFiles(clsfSystmSn);
}, [clsfSystmSn]);
useEffect(() => {
if (isModalOpen === true) {
getFiles(); // 파일 목록 조회
} else {
setFiles([]); // 모달이 닫힐 때 파일 목록 초기화
setCurPage(1); // 페이지를 1로 초기화
setIsResetPage(true); // 페이지를 초기화
setIsResetSelection(true); // 선택 초기화
}
}, [filesOrigin, isModalOpen, curPage]);
const renderBody = () => (
<>
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<UiTable
headName={(() => {
return (<>조회 결과 ({filesOrigin.length.toLocaleString('ko-KR')})건</>);
})()
}
tableData={files}
headlist={REPORT_FILES_HEADLIST}
pageSize={10}
total={filesOrigin.length}
cellClick={handleCellClick}
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
/>
</div>
</>
);
return (
<UiModal
open={isModalOpen}
onOpenChange={setIsModalOpen}
headerTitle=""
bodyClass={`
min-h-[50vh] h-[570px]
min-w-[35vw] w-[400px]
max-w-none max-h-none
overflow-visible
z-[5000]
flex items-start
`}
body={renderBody()}
noFooterYn='Y'
/>
);
};
export default React.memo(ReportFilesModal);
@@ -4,11 +4,11 @@ import { Button, Label } from '@common/components';
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { SelectBox, UiTable } from '@common/business_components/ui';
import LoadingOverlay from '@/components/ui/LoadingBar';
import { addFileName, flattenFolders, getFileDownload, useDcTree, reportGubuns } from './searchModalReportApi';
import useStore from '../../../store/useStore';
import useReportTab from './useReportTab';
import useStore from '@/store/useStore';
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
import { useReportFileModalStore } from './searchModalReportApi'; // 모달 상태 관리 스토어
import ReportFiles from './reportFiles'; // 파일목록모달
import { useReportTabStore } from '@/store/useReportTabStore';
import ReportFilesModal from './reportFilesModal'; // 파일목록모달
// 하수도정비 기본계획 보고서
@@ -42,13 +42,20 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
const [ctpvCd, setCtpvCd] = useState('0');
const [sggCd, setSggCd] = useState('0');
const [reportGubun, setReportGubun] = useState("0");
// const [gubun, setGubun] = useState("");
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useReportFileModalStore(); // 모달 상태 관리 스토어
const [clsfSystmSn, setClsfSystmSn] = useState(''); // 자료관 기본계획 일련번호
const { ctpv, sgg } = useStore();
// 하수정비기본계획 report
const reportOriginData = useDcTree(""); //:Folder[]
const { ctpv, sgg } = useStore();
const {
isOpen: isReportFilesModalOpen,
setIsOpen: setIsReportFilesModalOpen,
dcTree: reportOriginData,
fetchDcTree
} = useReportTabStore(); // 모달 상태 관리 스토어
const { REPORT_TYPES, flattenFolders, addFileName } = useReportTab();
useEffect(() => {
fetchDcTree(""); // 하수정비기본계획 report
}, []);
useEffect(() => {
// filterParams 초기화
@@ -105,12 +112,9 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
const handleReportCellClick = (index: number | undefined) => {
if (typeof index === "number" && tableData[index]) {
const reportData = tableData[index];
const fileName = reportData.fileName;
const filePath = reportData.filePath;
if (reportData.fileSn !== "") {
//getFileDownload(reportData.fileSn, fileName, filePath);
setIsModalOpen(true); // 모달 열기
setClsfSystmSn(reportData.clsfSystmSn); // 자료관 기본계획 일련번호 설정
setIsReportFilesModalOpen(true); // 모달 열기
}
}
};
@@ -133,15 +137,16 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.text);
}
if (reportGubun !== "0") { // 보고서구분검색
const reportGubunOption = reportGubuns.find((n) => n.val === reportGubun);
const reportGubunOption = REPORT_TYPES.find((n) => n.val === reportGubun);
filterReportData = filterReportData.filter(item => item.reportGubun === reportGubunOption?.text);
}
if (yearFilter.start != "" && yearFilter.end == "") //승인년도검색
const { start, end } = yearFilter;
if (start && !end) //승인년도검색
filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" : null);
if (yearFilter.start == "" && yearFilter.end != "")
else if (!start && end)
filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear <= yearFilter.end + "년" : null);
if (yearFilter.start != "" && yearFilter.end != "") {
else if (start && end) {
filterReportData = filterReportData.filter(item => {
return (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" && item.approvalYear <= yearFilter.end + "년" : null;
});
@@ -194,7 +199,7 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
<SelectBox className="w-44 mr-8"
selectClass="rounded-lg bg-white"
label=""
selectArray={[...reportGubuns]}
selectArray={[...REPORT_TYPES]}
value={reportGubun} onSelectValue={val => setReportGubun(String(val))}
selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2'
/>
@@ -233,7 +238,7 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
</div>
</div>
{/* 모달 컴포넌트 */}
{isModalOpen && <ReportFiles clsfSystmSn={clsfSystmSn} />}
{isReportFilesModalOpen && <ReportFilesModal clsfSystmSn={clsfSystmSn} />}
</>
);
};
@@ -1,37 +1,24 @@
import React, { useEffect, useMemo } from 'react';
import LoadingOverlay from '@/components/ui/LoadingBar';
import { UiModal, UITab, UiTable } from '@common/business_components/ui';
import { Button } from '@common/components/ui';
import axios from 'axios';
import {
ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2,
MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES,
} from '../components/common/table';
import { getData } from "@/components/lib/api";
import BasicTab, { BASIC_TYPES } from './basicTab';
import OperationTab, { operationCleaningTableHeadlist, operationInspectionTableHeadlist, operationMaintenanceTableHeadlist, operationTableHeadlist, WORK_TYPES } from './operationTab';
import ReportTab from './reportTab';
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import React, { useMemo } from 'react';
import { Toaster } from "react-hot-toast";
import { useSearchModal, OPERATION_TABLE_SIZE, type SearchModalProps } from './useSearchModal';
import useSearchModalStore from '../../../store/useSearchModalStore';
import {
ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2
} from '../components/common/table';
import BasicTab, { BASIC_TYPES } from './basicTab';
import OperationTab from './operationTab';
import ReportTab from './reportTab';
import useSearchModal from './useSearchModal';
import useBasicTab from './useBasicTab';
import useOperationTab from './useOperationTab';
const SearchModal: React.FC<SearchModalProps> = (props) => {
const {
open, onOpenChange,
TABS,
handleSearch,
handleBasicCellClick,
handleOperationCellClick,
handleOperationResultCellClick,
basicExcelDownload,
operationExcelDownload,
operationResultExcelDownload,
viewMultipleFeature,
} = useSearchModal(props);
const SearchModal: React.FC = () => {
const {
isSearchModalOpen, setIsSearchModalOpen,
activeTab, basicType, workType,
tableData, detailTableData,
tableData, detailTableData,
tableCount, detailTableCount,
curPage, detailCurPage,
isLoading, isDetailLoading,
@@ -40,6 +27,21 @@ const SearchModal: React.FC<SearchModalProps> = (props) => {
setActiveTab,
setCurPage, setDetailCurPage,
} = useSearchModalStore();
const { TABS, viewMultipleFeature } = useSearchModal();
const { handleBasicCellClick, basicExcelDownload } = useBasicTab();
const {
WORK_TYPES,
OPERATION_TABLE_SIZE,
OPERATION_TABLE_HEADLIST,
MAINTENANACE_TABLE_HEADLIST,
CLEANING_TABLE_HEADLIST,
INSPECTION_TABLE_HEADLIST,
handleOperationCellClick,
handleOperationResultCellClick,
operationExcelDownload,
operationResultExcelDownload
} = useOperationTab();
const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => {
if (activeTab === 'basic') { // 기본정보
@@ -52,16 +54,16 @@ const SearchModal: React.FC<SearchModalProps> = (props) => {
return [];
}
if (activeTab === 'operation') return operationTableHeadlist; // 운영정보
if (activeTab === 'operation') return OPERATION_TABLE_HEADLIST; // 운영정보
return [];
}, [activeTab, basicType, workType]);
const dynamicHeadlist2: { id: string, title: string }[] = useMemo(() => {
if (activeTab === 'operation') {
if (workType === WORK_TYPES[0]?.val) return operationMaintenanceTableHeadlist; // 보수 및 갱신
if (workType === WORK_TYPES[1]?.val) return operationInspectionTableHeadlist; // 점검 및 진단
if (workType === WORK_TYPES[2]?.val) return operationCleaningTableHeadlist; // 청소 및 준설
if (workType === WORK_TYPES[0]?.val) return MAINTENANACE_TABLE_HEADLIST; // 보수 및 갱신
if (workType === WORK_TYPES[1]?.val) return CLEANING_TABLE_HEADLIST; // 점검 및 진단
if (workType === WORK_TYPES[2]?.val) return INSPECTION_TABLE_HEADLIST; // 청소 및 준설
}
return [];
@@ -79,20 +81,9 @@ const SearchModal: React.FC<SearchModalProps> = (props) => {
}))}
onValueChange={(value: string) => setActiveTab(value)}
/>
{/* 기본정보 탭 */}
{activeTab === 'basic' && (
<BasicTab {...props} />
)}
{/* 운영정보 탭 */}
{activeTab === 'operation' && (
<OperationTab {...props} />
)}
{/* 하수도정비기본계획보고서 탭 */}
{activeTab === 'report' && (
<ReportTab
open={open}
/>
)}
{activeTab === 'basic' && (<BasicTab />)} {/* 기본정보 탭 */}
{activeTab === 'operation' && (<OperationTab />)} {/* 운영정보 탭 */}
{activeTab === 'report' && (<ReportTab open={isSearchModalOpen} />)} {/* 하수도정비기본계획보고서 탭 */}
{/* 📋 결과 리스트 */}
@@ -192,8 +183,8 @@ const SearchModal: React.FC<SearchModalProps> = (props) => {
return (
<>
<UiModal
open={open}
onOpenChange={onOpenChange}
open={isSearchModalOpen}
onOpenChange={setIsSearchModalOpen}
bodyClass={`
h-[100dvh]
w-[60dvw]
@@ -1,198 +0,0 @@
import { useState, useEffect } from 'react';
import { getDcData } from '@/components/lib/api';
import { create } from 'zustand';
export type Folder = {
clsfSystmSn: string;
upClsfSystmSn: string;
clsfSystmNm: string;
children: Folder[];
};
export type FlattenedFolder = {
[key: string]: string | number | undefined;
};
export type SelectDataType = {
text: string;
val: string;
sido?: string; // 시도 코드 (선택 사항)
}
export type ReportDataFile = {
no: number;
fileTitle: string;
regDt: string;
fileSn: string;
filePath: string;
}
const isYear = (value: string): boolean => {
return /^[0-9]{4}년$/.test(value);
};
export const reportGubuns = [{ val: '0', text: '전체' }, { val: '1', text: '보고서' }
, { val: '2', text: '요약보고서' }, { val: '3', text: '부록' }, { val: '4', text: '기타' }];
/**
* Flatten a hierarchical folder structure into a flat array.
* @param data - The hierarchical folder data to flatten.
* @returns An array of flattened folders with their paths and levels.
*/
export function flattenFolders(data: Folder[]): FlattenedFolder[] {
const result: FlattenedFolder[] = [];
const exceptNodeNm = ["부분", "통합", "전체"];
const addNodeNm = ["보고서", "요약보고서", "부록", "기타"]; // level5
const columnNms: { [key: number]: string } = {
0: "root", 1: "sido", 2: "sigungu", 3: "approvalYear"
, 4: "plantZoneGubun", 5: "reportGubun", 7: "fileName", 8: "regDate"
};
function traverse(
node: Folder,
path: { [key: string]: string | number },
level: number
) {
if (level === 2 && isYear(node.clsfSystmNm)) {
level++;
}
if (level === 4 && addNodeNm.includes(node.clsfSystmNm)) {
level++; // 레벨을 증가시켜 하위값에 추가되도록 함.
}
const newPath = {
...path,
[`${columnNms[level]}`]: node.clsfSystmNm,
};
if (node.children.length === 0) {
newPath.no = result.length + 1;
newPath.clsfSystmSn = node.clsfSystmSn; // 파일 조회를 위한 고유번호
result.push(newPath);
} else {
if (level === 4) {
if (exceptNodeNm.includes(node.clsfSystmNm)) {
level--; // 제외할 노드 이름인 경우, 현재의 레벨에 하위값이 써지도록 함.
}
}
node.children.forEach((child) => {
traverse(child, newPath, level + 1);
});
}
}
if (data && data.length > 0 && data[0]?.clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리
data.forEach((root) => traverse(root, {}, 0));
} else {
data.forEach((root) => traverse(root, {}, 1));
}
return result;
}
export const getDcTree = async (clsfSystmNm: string) => {
const result = await getDcData("/api/v1/dc/clsf?clsfSystmNm=" + clsfSystmNm);
return result;
};
export const useDcTree = (clsfSystmNm: string) => {
const [data, setData] = useState([]);
useEffect(() => {
const fetch = async () => {
const result = await getDcTree(clsfSystmNm);
if (result) {
setData(result);
}
console.log("useDcTree호출");
console.log("clsfSystmNm:", clsfSystmNm, "data:", result);
};
fetch();
}, [clsfSystmNm]);
return data;
};
export const getDcFiles = async (clsfSystmSn: string) : Promise<ReportDataFile[]> => {
if (!clsfSystmSn) {
console.error("clsfSystmSn is required to get files.");
return [];
}
const queryString = 'clsfSystmSn=' + clsfSystmSn;
const result = await getDcData(`/api/v1/dc/file${queryString ? `?${queryString}` : ''}`);
return result;
};
export const useDcFiles = (clsfSystmNm: string) => {
const [data, setData] = useState<ReportDataFile[]>([]);
useEffect(() => {
const fetch = async () => {
const result = await getDcFiles(clsfSystmNm);
if (result) {
setData(result);
}
console.log("useDcFiles 호출");
console.log("clsfSystmNm:", clsfSystmNm, "data:", result);
};
fetch();
}, [clsfSystmNm]);
return data;
};
// 파일 이름 추가
export const addFileName = async (data: FlattenedFolder[]) => {
const fileReportData = await Promise.all(
data.map(async (item) => {
const fileArr: ReportDataFile[] = await getDcFiles(String(item.clsfSystmSn));
let result:FlattenedFolder;
if (fileArr.length > 0) {
const f = fileArr[0];
const fCount = fileArr.length;
result = { ...item, fileName: f?.fileTitle+" 외 "+fCount+"건", regDate: f?.regDt, fileSn: f?.fileSn, filePath: f?.filePath };
} else {
result = { ...item, fileName: "", regDate: "", fileSn: "", filePath: "" };
}
return result;
})
);
return fileReportData;
};
export const getDcFileprcs = async (fileSn: string) => {
const queryString = 'fileSn=' + fileSn;
const result = await getDcData(`/api/v1/dc/fileprcs${queryString ? `?${queryString}` : ''}`);
return result;
};
export const getDcFileDownload = async (filePath: string) => {
const queryString = 'filePath=' + encodeURIComponent(filePath);
const response = await fetch(`/dc-service/api/v1/dc/file/download?${queryString}`, {
method: 'GET',
});
if (!response.ok) {
throw new Error(`File download failed: ${response.status}`);
}
const blob = await response.blob(); // PDF, image 등은 blob으로 받아야 함
return blob;
};
export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터 임시link로 연결
const url = window.URL.createObjectURL(data);
const link = document.createElement('a');
link.href = url;
link.download = fileName; // 원하는 파일명 지정
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
}
export const getFileDownload = async (fileSn: string, fileName: string, filePath: string) => {
if (filePath === "") alert("filePath 정보 없으므로 다운로드 불가:filesn" + fileSn);
const blobData = await getDcFileDownload(filePath); // 실제 다운로드 API
fileDownload(blobData, fileName);
}
interface ModalStore {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}
export const useReportFileModalStore = create<ModalStore>((set) => ({
isOpen: false,
setIsOpen: (open: boolean) => set({ isOpen: open }),
}));
@@ -5,24 +5,16 @@ import { Button } from '@common/components';
import React from 'react';
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
import useSearchModalStore from '../../../store/useSearchModalStore';
import { useSearchModal, type SearchModalProps } from './useSearchModal';
import useSearchModal from './useSearchModal';
interface stofcbrCqlFiltersParams {
ctpvCd: string;
sggCd: string;
yearFilter: { start: string; end: string };
todayYear: string;
todayMonth: string;
todayDate: string;
getDateString: (year: string, month: string, day: string, defaultDate: string) => string;
}
export const stofcbrCqlFilters = (params: stofcbrCqlFiltersParams) => {
export const stofcbrCqlFilters = () => {
const {
ctpvCd, sggCd,
yearFilter,
todayYear, todayMonth, todayDate,
getDateString
} = params;
} = useSearchModalStore.getState();
const cqlFilters: string[] = [];
// 시도, 시군구
@@ -48,13 +40,13 @@ export const stofcbrCqlFilters = (params: stofcbrCqlFiltersParams) => {
return cqlFilters;
};
const Stofcbr: React.FC<SearchModalProps> = (props) => {
const Stofcbr: React.FC = () => {
const {
ctpvCd, setCtpvCd,
sggCd, setSggCd,
yearFilter, setYearFilter,
} = useSearchModalStore();
const { handleSearch } = useSearchModal(props);
const { handleSearch } = useSearchModal();
return (
@@ -7,21 +7,10 @@ import { MS_SWAGE_MHL_USE_CODES } from '../components/common/table';
import { SelectBox } from '@common/business_components/ui';
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
import useSearchModalStore from '../../../store/useSearchModalStore';
import { useSearchModal, type SearchModalProps } from './useSearchModal';
import useSearchModal from './useSearchModal';
interface swageMhlCqlFiltersParams {
ctpvCd: string;
sggCd: string;
yearFilter: { start: string; end: string };
start1: string;
end1: string;
var1: string;
todayYear: string;
todayMonth: string;
todayDate: string;
getDateString: (year: string, month: string, day: string, defaultDate: string) => string;
}
export const swageMhlCqlFilters = (params: swageMhlCqlFiltersParams) => {
export const swageMhlCqlFilters = () => {
const {
ctpvCd, sggCd,
yearFilter,
@@ -29,7 +18,7 @@ export const swageMhlCqlFilters = (params: swageMhlCqlFiltersParams) => {
var1,
todayYear, todayMonth, todayDate,
getDateString
} = params;
} = useSearchModalStore.getState();
const cqlFilters: string[] = [];
// 시도, 시군구
@@ -60,7 +49,7 @@ export const swageMhlCqlFilters = (params: swageMhlCqlFiltersParams) => {
return cqlFilters;
};
const SwageMhl: React.FC<SearchModalProps> = (props) => {
const SwageMhl: React.FC = () => {
const {
var1, setVar1,
start1, setStart1, end1, setEnd1,
@@ -68,7 +57,7 @@ const SwageMhl: React.FC<SearchModalProps> = (props) => {
sggCd, setSggCd,
yearFilter, setYearFilter,
} = useSearchModalStore();
const { handleSearch } = useSearchModal(props);
const { handleSearch } = useSearchModal();
return (
<div className="mb-3">
@@ -5,29 +5,17 @@ import { Button, Input, Label } from '@common/components';
import React from 'react';
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
import useSearchModalStore from '../../../store/useSearchModalStore';
import { useSearchModal, type SearchModalProps } from './useSearchModal';
import useSearchModal from './useSearchModal';
interface swagePmstnCqlFiltersParams {
ctpvCd: string;
sggCd: string;
yearFilter: { start: string; end: string };
start1: string;
start2: string;
end1: string;
end2: string;
todayYear: string;
todayMonth: string;
todayDate: string;
getDateString: (year: string, month: string, day: string, defaultDate: string) => string;
}
export const swagePmstnCqlFilters = (params: swagePmstnCqlFiltersParams) => {
export const swagePmstnCqlFilters = () => {
const {
ctpvCd, sggCd,
yearFilter,
start1, start2, end1, end2,
todayYear, todayMonth, todayDate,
getDateString
} = params;
} = useSearchModalStore.getState();
const cqlFilters: string[] = [];
// 시도, 시군구
@@ -63,7 +51,7 @@ export const swagePmstnCqlFilters = (params: swagePmstnCqlFiltersParams) => {
return cqlFilters;
};
const SwagePmstn: React.FC<SearchModalProps> = (props) => {
const SwagePmstn: React.FC = () => {
const {
start1, setStart1,
end1, setEnd1,
@@ -71,7 +59,7 @@ const SwagePmstn: React.FC<SearchModalProps> = (props) => {
sggCd, setSggCd,
yearFilter, setYearFilter,
} = useSearchModalStore();
const { handleSearch } = useSearchModal(props);
const { handleSearch } = useSearchModal();
return (
<div className="mb-3">
@@ -4,29 +4,14 @@ import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { SelectBox } from '@common/business_components/ui';
import { Button, Input, Label } from '@common/components';
import React from 'react';
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table';
import { MS_SWAGE_PPL_USE_CODES } from '../components/common/table';
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
import useSearchModalStore from '../../../store/useSearchModalStore';
import { useSearchModal, type SearchModalProps } from './useSearchModal';
import useSearchModal from './useSearchModal';
interface swagePplCqlFiltersParams {
var1: string;
var2: string;
ctpvCd: string;
sggCd: string;
yearFilter: { start: string; end: string };
start1: string;
start2: string;
end1: string;
end2: string;
todayYear: string;
todayMonth: string;
todayDate: string;
getDateString: (year: string, month: string, day: string, defaultDate: string) => string;
}
// 하수관로용 CQL 필터 생성
export const swagePplCqlFilters = (params: swagePplCqlFiltersParams) => {
export const swagePplCqlFilters = () => {
const {
var1, var2,
ctpvCd, sggCd,
@@ -34,7 +19,7 @@ export const swagePplCqlFilters = (params: swagePplCqlFiltersParams) => {
start1, start2, end1, end2,
todayYear, todayMonth, todayDate,
getDateString
} = params;
} = useSearchModalStore.getState();
const cqlFilters: string[] = [];
if (var1) cqlFilters.push(`usg_cd='${var1}'`);
@@ -69,14 +54,14 @@ export const swagePplCqlFilters = (params: swagePplCqlFiltersParams) => {
return cqlFilters;
};
const SwagePpl: React.FC<SearchModalProps> = (props) => {
const SwagePpl: React.FC = () => {
const {
var1, setVar1,
start1, setStart1, end1, setEnd1,
ctpvCd, setCtpvCd, sggCd, setSggCd,
yearFilter, setYearFilter,
} = useSearchModalStore();
const { handleSearch } = useSearchModal(props);
const { handleSearch } = useSearchModal();
return (
@@ -5,29 +5,18 @@ import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { Button, Input, Label } from '@common/components';
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
import useSearchModalStore from '../../../store/useSearchModalStore';
import { useSearchModal, type SearchModalProps } from './useSearchModal';
import useSearchModal from './useSearchModal';
interface swagePrcsFcltCqlFiltersParams {
ctpvName: string;
sggName: string;
yearFilter: { start: string; end: string };
start1: string;
end1: string;
todayYear: string;
todayMonth: string;
todayDate: string;
getDateString: (year: string, month: string, day: string, defaultDate: string) => string;
}
// 하수처리시설용 CQL 필터 생성
export const swagePrcsFcltCqlFilters = (params: swagePrcsFcltCqlFiltersParams) => {
export const swagePrcsFcltCqlFilters = () => {
const {
ctpvName, sggName,
yearFilter,
start1, end1,
todayYear, todayMonth, todayDate,
getDateString
} = params;
} = useSearchModalStore.getState();
const cqlFilters: string[] = [];
// 가동개시일자 범위
@@ -65,14 +54,14 @@ export const swagePrcsFcltCqlFilters = (params: swagePrcsFcltCqlFiltersParams) =
return cqlFilters;
};
const SwagePrcsFclt: React.FC<SearchModalProps> = (props) => {
const SwagePrcsFclt: React.FC = () => {
const {
start1, setStart1, end1, setEnd1,
ctpvCd, setCtpvCd, setCtpvName,
sggCd, setSggCd, setSggName,
yearFilter, setYearFilter,
} = useSearchModalStore();
const { handleSearch } = useSearchModal(props);
const { handleSearch } = useSearchModal();
return (
<div className="mb-3">
@@ -4,21 +4,15 @@ import React from 'react';
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
import { Button, Input, Label } from '@common/components';
import useSearchModalStore from '../../../store/useSearchModalStore';
import { useSearchModal, type SearchModalProps } from './useSearchModal';
import useSearchModal from './useSearchModal';
interface swagePrcsZoneCqlFiltersParams {
ctpvCd: string;
sggCd: string;
var1: string;
var2: string;
}
// 하수처리구역용 CQL 필터 생성
export const swagePrcsZoneCqlFilters = (params: swagePrcsZoneCqlFiltersParams) => {
export const swagePrcsZoneCqlFilters = () => {
const {
ctpvCd, sggCd,
var1, var2
} = params;
} = useSearchModalStore.getState();
const cqlFilters: string[] = [];
if (ctpvCd !== '' && ctpvCd !== '0') cqlFilters.push(`ctpv_cd='${ctpvCd?.trim()}'`);
if (sggCd !== '' && sggCd !== '0') cqlFilters.push(`sgg_cd='${sggCd}'`);
@@ -27,13 +21,13 @@ export const swagePrcsZoneCqlFilters = (params: swagePrcsZoneCqlFiltersParams) =
return cqlFilters;
};
const swagePrcsZone: React.FC<SearchModalProps> = (props) => {
const swagePrcsZone: React.FC = () => {
const {
var1, setVar1, var2, setVar2,
ctpvCd, setCtpvCd,
sggCd, setSggCd,
} = useSearchModalStore();
const { handleSearch } = useSearchModal(props);
const { handleSearch } = useSearchModal();
return (
<div className="mb-3">
@@ -0,0 +1,329 @@
import { useEffect, useState } from 'react';
import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui';
import axios from 'axios';
import {
MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES,
MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol
} from '../components/common/table';
import { getData } from "@/components/lib/api";
import { BASIC_TYPES } from './basicTab';
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import useStore from '../../../store/useStore';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { confirmToast } from '@/components/ui/toast'
import { toast } from "react-hot-toast";
import useSearchModalStore from '../../../store/useSearchModalStore';
import { swagePplCqlFilters } from './swagePpl';
import { swagePrcsFcltCqlFilters } from './swagePrcsFclt';
import { swagePrcsZoneCqlFilters } from './swagePrcsZone';
import { swageMhlCqlFilters } from './swageMhl';
import { swagePmstnCqlFilters } from './swagePmstn';
import { stofcbrCqlFilters } from './stofcbr';
export default function useBasicTab() {
const {
basicType,
tableData,
curPage,
todayYear, todayMonth, todayDate,
setIsSearchModalOpen,
setTableData,
setTableCount,
setIsLoading,
} = useSearchModalStore();
const { ctpv, sgg } = useStore();
const fetchBasicFilteredData = async () => {
let cqlFilters: string[] = [];
const queryParams = new URLSearchParams({
lyrTypeNm: basicType,
page: `${curPage - 1}`,
size: '10',
sort: 'string',
});
if (basicType === 'tb_ms_swage_ppl') cqlFilters = swagePplCqlFilters(); // 하수관로
else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = swagePrcsFcltCqlFilters(); // 하수처리시설
else if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = swagePrcsZoneCqlFilters(); // 하수처리구역
else if (basicType === 'tb_ms_swage_mhl') cqlFilters = swageMhlCqlFilters(); // 하수맨홀
else if (basicType === 'tb_ms_swage_pmstn') cqlFilters = swagePmstnCqlFilters(); // 하수펌프장
else if (basicType === 'tb_ms_stofcbr') cqlFilters = stofcbrCqlFilters(); // 우수토실
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
const res = await getData(`v1/fclts?${queryParams.toString()}`);
// const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`);
console.log('fetch res', res);
console.log('fetch res.data', res.data);
console.log('fetch res.count', res.count);
console.log('fetch res.length', res.length);
// 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가
const dataWithRowNumber = (res.data || []).map((item: any, index: number) => {
// 🎯 먼저 각 아이템을 포맷팅
const formattedItem = formatDisplayData(item);
let ctpvCd = item?.ctpvNm ?? formattedItem?.ctpvNm ?? '';
let sggCd = item?.sggNm ?? formattedItem?.sggNm ?? '';
if (!ctpvCd || !sggCd) {
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
if (!sggCd && code) sggCd = code.slice(0, 5);
}
const matchedMhlCode = MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim());
const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd);
const ctpvNm = ctpv.find(code => code.val.trim() === ctpvCd)?.text || item?.ctpvNm;
const sggNm = sgg.find(code => code.val.trim() === sggCd)?.text || item?.sggNm;
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
// 추가 처리 및 일련번호 추가
return {
...formattedItem,
rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item?.scsYn, // scsYn이 null이면 'N'으로 변경
mqltCd: matchedMhlCode ? matchedMhlCode.text : item?.mqltCd,
usgCd: matchedMhlUsgCode ? matchedMhlUsgCode.text : (usgCdObj ? usgCdObj.text : item?.usgCd),
ctpvNm: ctpvNm ?? '-',
sggNm: sggNm ?? '-',
swtrmt_zone_nm: item.swtrmtZoneNm || item.swtrmt_zone_nm || '-',
};
});
setTableData(dataWithRowNumber);
setTableCount(res.count || []);
};
// 기본정보 테이블 cellClick 핸들러
const handleBasicCellClick = async (index?: number | undefined, headId?: String) => {
if (typeof index === "number") {
setIsLoading(true);
const row = tableData[index];
const layerNm = row.lyrTypeNm;
const fid = row.fid;
const core = GisApp.Module.layer as any;
await core.showSearchResultsFromGeoJSON(layerNm, fid);
setIsLoading(false);
setIsSearchModalOpen(false); // SearchModal 닫기
}
};
// 기본정보 Excel 다운로드 핸들러
const basicExcelDownload = async () => {
if (tableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
toast.dismiss("global-confirm-toast");
confirmToast(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
onDownload ?? (() => { }),
"text-xl font-medium text-gray-800"
);
async function onDownload() {
setIsLoading(true);
try {
const queryParams = new URLSearchParams({
lyrTypeNm: basicType,
size: '10000',
});
let cqlFilters: string[] = [];
let headerMapping: { [key: string]: string } = {};
let headerRange: string = "A1:F1";
switch (basicType) {
case 'tb_ms_swage_ppl': {
// 하수관로
cqlFilters = swagePplCqlFilters()
headerMapping = MsSwagePplExcelCol;
headerRange = "A1:M1";
break;
}
case 'tb_il_swage_prcs_fclt': {
// 하수처리시설
cqlFilters = swagePrcsFcltCqlFilters();
headerMapping = SwtrmtFcltExcelCol;
headerRange = "A1:J1";
break;
}
case 'tb_ms_swage_prcs_zone': {
// 하수처리구역
cqlFilters = swagePrcsZoneCqlFilters();
headerMapping = SwtrmtPrcsZoneExcelCol;
headerRange = "A1:D1";
break;
}
case 'tb_ms_swage_mhl': {
// 하수맨홀
cqlFilters = swageMhlCqlFilters();
headerMapping = MsSwageMhlExcelCol;
headerRange = "A1:J1";
break;
}
case 'tb_ms_swage_pmstn': {
// 하수펌프장
cqlFilters = swagePmstnCqlFilters();
headerMapping = ExclnSewagePumpExcelCol;
headerRange = "A1:J1";
break;
}
case 'tb_ms_stofcbr': {
// 우수토실
cqlFilters = stofcbrCqlFilters();
headerMapping = stofcbrExcelCol;
headerRange = "A1:H1";
break;
}
}
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
const res = await getData(`v1/fclts?${queryParams.toString()}`);
// const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`);
console.log('onDownload res', res);
const mhlUsgCdMap = new Map(MS_SWAGE_MHL_USE_CODES.map(code => [code.val?.trim(), code.text]));
const pplUsgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val?.trim(), code.text]));
const ctpvMap = new Map(ctpv.map(code => [code.val?.trim(), code.text]));
const sggMap = new Map(sgg.map(code => [code.val?.trim(), code.text]));
const mappedContent = (res.data.data || []).map((item: any) => {
const formattedItem = formatDisplayData(item);
let ctpvCd = item?.ctpvCd ?? formattedItem?.ctpvCd ?? '';
let sggCd = item?.sggCd ?? formattedItem?.sggCd ?? '';
if (!ctpvCd || !sggCd) {
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
if (!sggCd && code) sggCd = code.slice(0, 5);
}
// trim 한 번만 수행
const trimmedUsgCd = item?.usgCd?.trim();
// const trimmedMqltCd = item?.mqltCd?.trim();
switch (basicType) {
case 'tb_ms_swage_ppl': {
// 하수관로
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
instMngNo: formattedItem.instMngNo ?? item.instMngNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? item.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? item.sggNm ?? '-',
swtrmt_zone_nm: formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
usgCd: pplUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-',
pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '-',
prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '-',
tmptDpth: formattedItem.tmptDpth ?? item.tmptDpth ?? '-',
tmptMhlMngNo: formattedItem.tmptMhlMngNo ?? item.tmptMhlMngNo ?? '-',
scendDpth: formattedItem.scendDpth ?? item.scendDpth ?? '-',
scendMhlMngNo: formattedItem.scendMhlMngNo ?? item.scendMhlMngNo ?? '-',
};
}
case 'tb_il_swage_prcs_fclt': {
// 하수처리시설
return {
swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-',
fcltCpct: formattedItem.fcltCpct ?? item.fcltCpct ?? '-',
rgnExpln: formattedItem.rgnExpln ?? item.rgnExpln ?? '-',
oprtngStrtYmd: formattedItem.oprtngStrtYmd ?? item.oprtngStrtYmd ?? '-',
laeNm: formattedItem.laeNm ?? item.laeNm ?? '-',
sareaNm: formattedItem.sareaNm ?? item.sareaNm ?? '-',
rvrNm: formattedItem.rvrNm ?? item.rvrNm ?? '-',
dschgWtstNm: formattedItem.dschgWtstNm ?? item.dschgWtstNm ?? '-',
swtrmtPpltnCnt: formattedItem.swtrmtPpltnCnt ?? item.swtrmtPpltnCnt ?? '-',
rprsMsrcCn: formattedItem.rprsMsrcCn ?? item.rprsMsrcCn ?? '-'
};
}
case 'tb_ms_swage_prcs_zone': {
// 하수처리구역
return {
swtrmtZoneNm: formattedItem.swtrmtZoneNm ?? item.swtrmtZoneNm ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-'
};
}
case 'tb_ms_swage_mhl': {
// 하수맨홀
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
usgCd: mhlUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-',
swageMhlDiamVl: formattedItem.swageMhlDiamVl ?? item.swageMhlDiamVl ?? '-',
wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-',
vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-',
altdVl: formattedItem.altdVl ?? item.altdVl ?? '-',
dpthlVl: formattedItem.dpthlVl ?? item.dpthlVl ?? '-'
};
}
case 'tb_ms_swage_pmstn': {
// 하수펌프장
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
swagePmstnNm: formattedItem.swagePmstnNm ?? item.swagePmstnNm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
dailyPrcsCpct: formattedItem.dailyPrcsCpct ?? item.dailyPrcsCpct ?? '-',
maxImpndCpct: formattedItem.maxImpndCpct ?? item.maxImpndCpct ?? '-',
altdVl: formattedItem.altdVl ?? item.altdVl ?? '-',
wtlvVl: formattedItem.wtlvVl ?? item.wtlvVl ?? '-',
exclnWpmpAbltVl: formattedItem.exclnWpmpAbltVl ?? item.exclnWpmpAbltVl ?? '-'
};
}
case 'tb_ms_stofcbr': {
// 우수토실
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
mapshtNo: formattedItem.mapshtNo ?? item.mapshtNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
worflwWallHgt: formattedItem.worflwWallHgt ?? item.worflwWallHgt ?? '-',
swageMhlMngNo: formattedItem.swageMhlMngNo ?? item.swageMhlMngNo ?? '-',
drtnAngl: formattedItem.drtnAngl ?? item.drtnAngl ?? '-'
};
}
}
});
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedContent; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
const fileName = `${label} 조회 결과_${todayYear}${todayMonth}${todayDate}`;
console.log(`excelData: ${excelData}, label: ${label}, fileName: ${fileName}`);
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: headerRange,
headerMapping: headerMapping
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
return { fetchBasicFilteredData, handleBasicCellClick, basicExcelDownload };
};
@@ -0,0 +1,439 @@
import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui';
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table';
import { getData } from "@/components/lib/api";
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { confirmToast } from '@/components/ui/toast'
import { toast } from "react-hot-toast";
import useSearchModalStore from '@/store/useSearchModalStore';
import useStore from '@/store/useStore';
export default function useOperationTab() {
const OPERATION_TABLE_SIZE = 5;
const {
workType,
tableData, detailTableData,
curPage, detailCurPage,
detailBizId,
var1, var2,
yearFilter,
todayYear, todayMonth, todayDate,
setIsSearchModalOpen,
setTableData, setDetailTableData,
setTableCount, setDetailTableCount,
setDetailCurPage,
setIsLoading, setIsDetailLoading,
setIsResetDetailPage,
setIsResetDetailSelection,
setDetailBizId,
getDateString
} = useSearchModalStore();
const { BIZ_TY_CD } = useStore();
const WORK_TYPES = [ // 운영정보 '구분' 선택 목록
{ val: "['200', '201', '204']", text: "보수 및 갱신" },
{ val: "['100']", text: "점검 및 진단" },
{ val: "['203']", text: "청소 및 준설" },
];
const OPERATION_TABLE_HEADLIST = [ // 운영정보 - 사업 현황
{ id: 'rowNumber', title: '일련번호' },
{ id: "bizInfoNm", title: "사업명" },
{ id: "flfmtBgngYmd", title: "수행시작일자" },
{ id: "flfmtEndYmd", title: "수행종료일자" },
{ id: "flfmtYr", title: "수행년도" },
{ id: "bizTypeCd", title: "사업유형" },
{ id: "bizAmt", title: "사업금액" },
];
const MAINTENANACE_TABLE_HEADLIST = [ // 운영정보 - 보수 및 갱신 결과표
{ id: 'rowNumber', title: '일련번호' },
{ id: "fcltMngNo", title: "관리번호" },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: "usgCd", title: "용도" },
// { id: "lgalDrblyr", title: "내용연수" },
// { id: "acqsPrmpc", title: "취득금액" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmtVl", title: "관경(mm)" },
{ id: "prlgVl", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
// { id: "instlYmd", title: "사업구분" },
];
const CLEANING_TABLE_HEADLIST = [ // 운영정보 - 청소 및 준설 결과표
{ id: 'rowNumber', title: '일련번호' },
{ id: "fcltMngNo", title: "관리번호" },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: "usgCd", title: "용도" },
// { id: "lgalDrblyr", title: "내용연수" },
// { id: "acqsPrmpc", title: "취득금액" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmtVl", title: "관경(mm)" },
{ id: "prlgVl", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
];
const INSPECTION_TABLE_HEADLIST = [ // 운영정보 - 점검 및 진단 결과표
{ id: 'rowNumber', title: '일련번호' },
{ id: "fcltMngNo", title: "관리번호" },
{ id: "swtrmt_zone_nm", title: "처리구역" },
{ id: "usgCd", title: "용도" },
// { id: "riskEval", title: "위험평가" },
// { id: "acqsPrmpc", title: "취득금액" },
// { id: "lgalDrblyr", title: "내용연수" },
// { id: "remLife", title: "잔존수명" },
{ id: "mqltCd", title: "재질" },
{ id: "pdmtVl", title: "관경(mm)" },
{ id: "prlgVl", title: "연장(m)" },
{ id: "instlYmd", title: "설치일자" },
{ id: "strc_stts_grd", title: "구조적 상태등급" },
{ id: "oper_stts_grd", title: "운영적 상태등급" },
];
const fetchOperationFilteredData = async () => {
const RequestDTO: Record<string, string> = {
page: `${curPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
};
if (var1) RequestDTO.bizInfoNm = var1;
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
// 수행기간 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
RequestDTO.fromflfmtEndYmd = startDate;
RequestDTO.toflfmtEndYmd = endDate;
// JSON.parse 사용으로 문자열 -> 배열 변환
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const params = new URLSearchParams(RequestDTO).toString();
const res = await getData(`v1/bizInfo?${params}`);
const mappedContent = (res.content || []).map((item: any, index: number) => {
const formattedItem = formatDisplayData(item);
const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd);
return {
...formattedItem,
rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가
bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd,
};
});
setTableData(mappedContent || []);
setTableCount(res.totalElements || 0);
setDetailTableData([]);
setDetailTableCount(0);
};
// 운영정보 테이블 cellClick 핸들러
const handleOperationCellClick = async (index?: number | undefined, headId?: String) => {
setIsResetDetailPage(true); // 페이지를 1로 초기화
setIsResetDetailSelection(true); // 선택 초기화
setIsResetDetailPage(true); // 디테일 페이지를 초기화
if (typeof index === "number") {
setIsDetailLoading(true);
setDetailBizId(tableData[index].bizId);
setDetailCurPage(1); // 상세 페이지 초기화
try {
const RequestDTO: Record<string, string> = {
page: `${detailCurPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
bizId: tableData[index % OPERATION_TABLE_SIZE].bizId,
};
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
const formattedItem = formatDisplayData(item);
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim());
return {
...formattedItem,
rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1,
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
};
});
setDetailTableData(mappedData);
setDetailTableCount(res.count || 0);
} catch (error) {
console.error('운영정보 cellClick 에러:', error);
} finally {
setIsDetailLoading(false);
setIsResetDetailPage(false);
setIsResetDetailSelection(false);
}
}
};
// 운영정보 디테일 테이블 cellClick 핸들러
const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => {
if (typeof index === "number") {
setIsLoading(true);
const row = detailTableData[index];
const layerNm = row.lyrTypeNm;
const fid = row.fid;
const core = GisApp.Module.layer as any;
await core.showSearchResultsFromGeoJSON(layerNm, fid);
setIsLoading(false);
setIsSearchModalOpen(false); // SearchModal 닫기
}
};
// 운영정보 디테일 테이블 페이지네이션 핸들러
const handleOperationPageNation = async (index?: number | undefined) => {
if (typeof index === "number") {
setIsDetailLoading(true);
try {
const RequestDTO: Record<string, string> = {
page: `${detailCurPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
bizId: String(detailBizId),
};
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
const formattedItem = formatDisplayData(item);
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim());
return {
...formattedItem,
rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1,
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
};
});
setDetailTableData(mappedData);
setDetailTableCount(res.count || 0);
} catch (error) {
console.error('운영정보 페이지네이션 에러:', error);
} finally {
setIsDetailLoading(false);
}
}
};
// 운영정보 Excel 다운로드 핸들러 추가
const operationExcelDownload = async () => {
if (tableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
toast.dismiss("global-confirm-toast");
confirmToast(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
onAnalysis ?? (() => { }),
"text-xl font-medium text-gray-800"
);
async function onAnalysis() {
setIsLoading(true);
try {
const RequestDTO: Record<string, string> = {
size: '10000',
};
if (var1) RequestDTO.bizInfoNm = var1;
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
// 수행기간 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
RequestDTO.fromflfmtEndYmd = startDate;
RequestDTO.toflfmtEndYmd = endDate;
// JSON.parse 사용으로 문자열 -> 배열 변환
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const params = new URLSearchParams(RequestDTO).toString();
const res = await getData(`v1/bizInfo?${params}`);
// 헤더 매핑 정의
const headerMapping: { [key: string]: string } = {
'bizInfoNm': '사업명',
'flfmtBgngYmd': '수행시작일자',
'flfmtEndYmd': '수행종료일자',
'flfmtYr': '수행년도',
'bizTypeCd': '사업유형',
'bizAmt': '사업금액(원)',
};
const bizTyCdMap = new Map(BIZ_TY_CD.map(code => [code.lwpoCd, code.lwpoCdNm]));
const mappedContent = (res.content || []).map((item: any) => {
const formattedItem = formatDisplayData(item);
return {
bizInfoNm: formattedItem.bizInfoNm ?? item.bizInfoNm ?? '',
flfmtBgngYmd: formattedItem.flfmtBgngYmd ?? item.flfmtBgngYmd ?? '',
flfmtEndYmd: formattedItem.flfmtEndYmd ?? item.flfmtEndYmd ?? '',
flfmtYr: formattedItem.flfmtYr ?? item.flfmtYr ?? '',
bizTypeCd: bizTyCdMap.get(item.bizTypeCd) || item.bizTypeCd || formattedItem.bizTypeCd || '',
bizAmt: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.bizAmt ?? item.bizAmt),
};
});
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedContent; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const label = WORK_TYPES.find((type) => type.val === workType)?.text
const fileName = `${label} 사업 현황_${todayYear}${todayMonth}${todayDate}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: "A1:F1",
headerMapping: headerMapping
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
// 운영정보 결과표 Excel 다운로드 핸들러 추가
const operationResultExcelDownload = async () => {
if (detailTableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
toast.dismiss("global-confirm-toast");
confirmToast(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
onAnalysis ?? (() => { }),
"text-xl font-medium text-gray-800"
);
async function onAnalysis() {
setIsLoading(true);
try {
const RequestDTO: Record<string, string> = {
size: '10000',
bizId: String(detailBizId),
};
const apiStartTime = performance.now();
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
const apiEndTime = performance.now();
const mappingStartTime = performance.now();
// 헤더 매핑 정의
const headerMapping: { [key: string]: string } = {
'fcltMngNo': '관리번호',
'swtrmt_zone_nm': '처리구역',
'usgCd': '용도',
'mqltCd': '재질',
'pdmtVl': '관경(mm)',
'prlgVl': '연장(m)',
'instlYmd': '설치일자',
'strc_stts_grd': '구조적 상태등급',
'oper_stts_grd': '운영적 상태등급',
};
const usgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val, code.text]));
const mqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val, code.text]));
// 🎯 단일 반복문으로 최적화 - 각 아이템마다 필요한 필드만 직접 매핑
const mappedData = (res.data || []).map((item: any) => {
const formattedItem = formatDisplayData(item);
// trim 한 번만 수행
const trimmedUsgCd = item?.usgCd?.trim();
const trimmedMqltCd = item?.mqltCd?.trim();
// zone 값 한 번만 계산
const zone = item.swtrmtZoneNm ?? formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '';
// 직접 매핑 - 반복문 없이 한 번에 객체 생성
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '',
swtrmt_zone_nm: (zone === null || zone === '') ? '-' : zone,
usgCd: usgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '',
mqltCd: mqltCdMap.get(trimmedMqltCd) || item.mqltCd || formattedItem.mqltCd || '',
pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '',
prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '',
strc_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.strc_stts_grd ?? item.strc_stts_grd),
oper_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.oper_stts_grd ?? item.oper_stts_grd),
};
});
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedData; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const label = WORK_TYPES.find((type) => type.val === workType)?.text
const fileName = `${label} 결과표_${todayYear}${todayMonth}${todayDate}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: "A1:I1",
headerMapping: headerMapping
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
return {
WORK_TYPES,
OPERATION_TABLE_HEADLIST,
MAINTENANACE_TABLE_HEADLIST,
CLEANING_TABLE_HEADLIST,
INSPECTION_TABLE_HEADLIST,
OPERATION_TABLE_SIZE,
fetchOperationFilteredData,
handleOperationCellClick,
handleOperationResultCellClick,
handleOperationPageNation,
operationExcelDownload,
operationResultExcelDownload
};
};
@@ -0,0 +1,173 @@
import { getDcData } from '@/components/lib/api';
import { fileDownload } from '@/utils';
import { useReportTabStore } from '@/store/useReportTabStore';
import useSearchModalStore from '@/app/store/useSearchModalStore';
export type Folder = {
clsfSystmSn: string;
upClsfSystmSn: string;
clsfSystmNm: string;
children: Folder[];
};
export type FlattenedFolder = {
[key: string]: string | number | undefined;
};
export type SelectDataType = {
text: string;
val: string;
sido?: string; // 시도 코드 (선택 사항)
}
export type ReportDataFile = {
no: number;
fileTitle: string;
regDt: string;
fileSn: string;
filePath: string;
}
export default function useReportTab() {
const REPORT_TYPES = [
{ val: '0', text: '전체' },
{ val: '1', text: '보고서' },
{ val: '2', text: '요약보고서' },
{ val: '3', text: '부록' },
{ val: '4', text: '기타' }
];
const { curPage, setIsLoading } = useSearchModalStore();
const { dcFiles: filesOrigin, files, setFiles } = useReportTabStore();
/**
* Flatten a hierarchical folder structure into a flat array.
* @param data - The hierarchical folder data to flatten.
* @returns An array of flattened folders with their paths and levels.
*/
const flattenFolders = (data: Folder[]): FlattenedFolder[] => {
const result: FlattenedFolder[] = [];
const exceptNodeNm = ["부분", "통합", "전체"];
const addNodeNm = ["보고서", "요약보고서", "부록", "기타"]; // level5
const columnNms: { [key: number]: string } = {
0: "root", 1: "sido", 2: "sigungu", 3: "approvalYear"
, 4: "plantZoneGubun", 5: "reportGubun", 7: "fileName", 8: "regDate"
};
const traverse = (
node: Folder,
path: { [key: string]: string | number },
level: number
) => {
const isYear = (value: string): boolean => {
return /^[0-9]{4}년$/.test(value);
};
if (level === 2 && isYear(node.clsfSystmNm)) {
level++;
}
if (level === 4 && addNodeNm.includes(node.clsfSystmNm)) {
level++; // 레벨을 증가시켜 하위값에 추가되도록 함.
}
const newPath = {
...path,
[`${columnNms[level]}`]: node.clsfSystmNm,
};
if (node.children.length === 0) {
newPath.no = result.length + 1;
newPath.clsfSystmSn = node.clsfSystmSn; // 파일 조회를 위한 고유번호
result.push(newPath);
} else {
if (level === 4) {
if (exceptNodeNm.includes(node.clsfSystmNm)) {
level--; // 제외할 노드 이름인 경우, 현재의 레벨에 하위값이 써지도록 함.
}
}
node.children.forEach((child) => {
traverse(child, newPath, level + 1);
});
}
}
if (data && data.length > 0 && data[0]?.clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리
data.forEach((root) => traverse(root, {}, 0));
} else {
data.forEach((root) => traverse(root, {}, 1));
}
return result;
}
const getDcFiles = async (clsfSystmSn: string): Promise<ReportDataFile[]> => {
if (!clsfSystmSn) {
console.error("clsfSystmSn is required to get files.");
return [];
}
const queryString = 'clsfSystmSn=' + clsfSystmSn;
const result = await getDcData(`/api/v1/dc/file${queryString ? `?${queryString}` : ''}`);
return result;
};
// 파일 이름 추가
const addFileName = async (data: FlattenedFolder[]) => {
const fileReportData = await Promise.all(
data.map(async (item) => {
const fileArr: ReportDataFile[] = await getDcFiles(String(item.clsfSystmSn));
let result: FlattenedFolder;
if (fileArr.length > 0) {
const f = fileArr[0];
const fCount = fileArr.length;
result = { ...item, fileName: f?.fileTitle + " 외 " + fCount + "건", regDate: f?.regDt, fileSn: f?.fileSn, filePath: f?.filePath };
} else {
result = { ...item, fileName: "", regDate: "", fileSn: "", filePath: "" };
}
return result;
})
);
return fileReportData;
};
const getDcFileDownload = async (fileSn: string, fileName: string, filePath: string) => {
if (filePath === "") {
alert(`filePath 정보가 없으므로 다운로드가 불가합니다.\nfilesn: ${fileSn}`);
return;
}
const queryString = 'filePath=' + encodeURIComponent(filePath);
const responseBlob = await getDcData(`/api/v1/dc/file/download${queryString ? `?${queryString}` : ''}`, "blob");
fileDownload(responseBlob, fileName);
}
// 파일 목록 조회
const getFiles = async () => {
setIsLoading(true); // 로딩 시작
try {
let data = filesOrigin;
data = data?.slice((curPage - 1) * 10, curPage * 10); // paging
data = data?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 }));
setFiles(data);
} catch (error) {
console.error("파일 목록 조회 에러:", error);
} finally {
setIsLoading(false); // 로딩 종료
}
};
// cellClick 핸들러
const handleCellClick = (index: number | undefined) => {
if (typeof index === "number" && files[index]) {
const fileName = files[index].fileTitle;
const filePath = files[index].filePath;
if (files[index].fileSn !== "") {
getDcFileDownload(files[index].fileSn, fileName, filePath);
}
}
};
return {
REPORT_TYPES,
flattenFolders,
addFileName,
getDcFiles,
getDcFileDownload,
getFiles,
handleCellClick
};
}
@@ -0,0 +1,171 @@
import { useEffect } from 'react';
import { getData } from "@/components/lib/api";
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import useStore from '../../../store/useStore';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import useSearchModalStore from '../../../store/useSearchModalStore';
import useBasicTab from './useBasicTab';
import useOperationTab from './useOperationTab';
export default function useSearchModal() {
const {
activeTab, basicType, workType,
showTable,
tableData, detailTableData,
curPage, detailCurPage,
detailBizId,
var1, var2,
yearFilter,
todayYear, todayMonth, todayDate,
setIsSearchModalOpen,
setShowTable,
setTableData, setDetailTableData,
setTableCount, setDetailTableCount,
setCurPage,
setIsLoading,
setIsResetPage, setIsResetDetailPage,
setIsResetSelection, setIsResetDetailSelection,
setDetailBizId,
resetFilters,
getDateString
} = useSearchModalStore();
const { fetchBasicFilteredData } = useBasicTab();
const { OPERATION_TABLE_SIZE, handleOperationPageNation } = useOperationTab();
const { BIZ_TY_CD } = useStore();
const TABS = [
{ val: "basic", text: "기본정보" },
{ val: "operation", text: "운영정보" },
{ val: "report", text: "하수도정비기본계획보고서" },
];
useEffect(() => {
resetFilters();
}, [activeTab, basicType, workType]);
// 페이지 변경 시마다 데이터 fetch
useEffect(() => {
setIsResetPage(false);
setIsResetDetailPage(true);
setDetailBizId(0);
console.log(`curPage: ${curPage}, showTable: ${showTable}`);
if (showTable && curPage > 1) fetchFilteredData();
}, [curPage]);
// 페이지 변경 시마다 데이터 fetch
useEffect(() => {
setIsResetDetailSelection(false);
if (showTable) handleOperationPageNation(Number(detailBizId));
}, [detailCurPage]);
// 검색 API 호출 함수
const fetchFilteredData = async () => {
setIsLoading(true); // 로딩 시작
setIsResetSelection(true); // 선택 초기화
setIsResetDetailSelection(true); // 선택 초기화
try {
if (activeTab === 'basic') {
fetchBasicFilteredData();
} else if (activeTab === 'operation') {
const RequestDTO: Record<string, string> = {
page: `${curPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
};
if (var1) RequestDTO.bizInfoNm = var1;
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
// 수행기간 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
RequestDTO.fromflfmtEndYmd = startDate;
RequestDTO.toflfmtEndYmd = endDate;
// JSON.parse 사용으로 문자열 -> 배열 변환
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const params = new URLSearchParams(RequestDTO).toString();
const res = await getData(`v1/bizInfo?${params}`);
const mappedContent = (res.content || []).map((item: any, index: number) => {
const formattedItem = formatDisplayData(item);
const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd);
return {
...formattedItem,
rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가
bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd,
};
});
setTableData(mappedContent || []);
setTableCount(res.totalElements || 0);
setDetailTableData([]);
setDetailTableCount(0);
}
setShowTable(true);
setIsResetSelection(false);
} catch (err) {
console.error('API ERROR:', err);
setTableData([]);
setIsResetSelection(false);
} finally {
setIsLoading(false); // 로딩 종료
setIsResetSelection(false);
// setIsResetDetailSelection(false);
}
};
// 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화
const handleSearch = () => {
setCurPage(1); // 페이지를 1로 초기화
fetchFilteredData();
setIsResetPage(true); // 페이지를 초기화
setIsResetDetailPage(true); // 디테일 페이지를 초기화
setIsResetSelection(false); // 선택 초기화
setIsResetDetailSelection(false); // 선택 초기화
setShowTable(true);
};
// 여러 시설 보기
const viewMultipleFeature = async () => {
if (activeTab === 'basic') {
if (!(tableData.length === 0)) {
setIsLoading(true);
const row = tableData[0];
const layerNm = row.lyrTypeNm;
const fids = tableData.map((item) => item.fid);
const core = GisApp.Module.layer as any;
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
setIsLoading(false);
setIsSearchModalOpen(false);
}
} else if (activeTab === 'operation') {
if (!(detailTableData.length === 0)) {
setIsLoading(true);
const row = detailTableData[0];
const layerNm = row.lyrTypeNm;
const fids = detailTableData.map((item) => item.fid);
const core = GisApp.Module.layer as any;
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
setIsLoading(false);
setIsSearchModalOpen(false);
}
}
}
return { TABS, getDateString, handleSearch, viewMultipleFeature };
};
@@ -1,841 +0,0 @@
import { useEffect, useState } from 'react';
import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui';
import axios from 'axios';
import {
MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES,
MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol
} from '../components/common/table';
import { getData } from "@/components/lib/api";
import { BASIC_TYPES } from './basicTab';
import { WORK_TYPES } from './operationTab';
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import useStore from '../../../store/useStore';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { confirmToast } from '@/components/ui/toast'
import { toast } from "react-hot-toast";
import useSearchModalStore from '../../../store/useSearchModalStore';
import { swagePplCqlFilters } from './swagePpl';
import { swagePrcsFcltCqlFilters } from './swagePrcsFclt';
import { swagePrcsZoneCqlFilters } from './swagePrcsZone';
import { swageMhlCqlFilters } from './swageMhl';
import { swagePmstnCqlFilters } from './swagePmstn';
import { stofcbrCqlFilters } from './stofcbr';
const API_URL = "/sewerage-service/api";
export const OPERATION_TABLE_SIZE = 5;
export interface SearchModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onFeatureSelect: (row: any) => void;
}
export function useSearchModal({ open, onOpenChange, onFeatureSelect }: SearchModalProps) {
const {
activeTab, basicType, workType,
showTable,
tableData, detailTableData,
curPage, detailCurPage,
detailBizId,
ctpvCd, ctpvName, sggCd, sggName,
start1, start2, end1, end2,
var1, var2,
yearFilter,
todayYear, todayMonth, todayDate,
setShowTable,
setTableData, setDetailTableData,
setTableCount, setDetailTableCount,
setCurPage, setDetailCurPage,
setIsLoading, setIsDetailLoading,
setIsResetPage, setIsResetDetailPage,
setIsResetSelection, setIsResetDetailSelection,
setDetailBizId,
resetFilters,
getDateString
} = useSearchModalStore();
const { ctpv, sgg, BIZ_TY_CD } = useStore();
const TABS = [
{ val: "basic", text: "기본정보" },
{ val: "operation", text: "운영정보" },
{ val: "report", text: "하수도정비기본계획보고서" },
];
useEffect(() => {
resetFilters();
}, [activeTab, basicType, workType]);
// 페이지 변경 시마다 데이터 fetch
useEffect(() => {
setIsResetPage(false);
setIsResetDetailPage(true);
setDetailBizId(0);
if (showTable) fetchFilteredData();
}, [curPage]);
// 페이지 변경 시마다 데이터 fetch
useEffect(() => {
setIsResetDetailSelection(false);
if (showTable) handleOperationPageNation(Number(detailBizId));
}, [detailCurPage]);
// 검색 API 호출 함수
const fetchFilteredData = async () => {
setIsLoading(true); // 로딩 시작
setIsResetSelection(true); // 선택 초기화
setIsResetDetailSelection(true); // 선택 초기화
try {
let cqlFilters: string[] = [];
if (activeTab === 'basic') {
const queryParams = new URLSearchParams({
lyrTypeNm: basicType,
page: `${curPage - 1}`,
size: '10',
sort: 'string',
});
if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = swagePrcsZoneCqlFilters({
ctpvCd, sggCd,
var1, var2
}); // 하수처리구역
else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = swagePrcsFcltCqlFilters({
ctpvName, sggName,
yearFilter,
start1, end1,
todayYear, todayMonth, todayDate,
getDateString
}); // 하수처리시설
else if (basicType === 'tb_ms_swage_ppl') cqlFilters = swagePplCqlFilters({
var1, var2,
ctpvCd, sggCd,
yearFilter,
start1, start2, end1, end2,
todayYear, todayMonth, todayDate,
getDateString
}); // 하수관로
else if (basicType === 'tb_ms_swage_mhl') cqlFilters = swageMhlCqlFilters({
ctpvCd, sggCd,
yearFilter,
start1, end1,
var1,
todayYear, todayMonth, todayDate,
getDateString
}); // 하수맨홀
else if (basicType === 'tb_ms_swage_pmstn') cqlFilters = swagePmstnCqlFilters({
ctpvCd, sggCd,
yearFilter,
start1, start2, end1, end2,
todayYear, todayMonth, todayDate,
getDateString
}); // 하수펌프장
else if (basicType === 'tb_ms_stofcbr') cqlFilters = stofcbrCqlFilters({
ctpvCd, sggCd,
yearFilter,
todayYear, todayMonth, todayDate,
getDateString
}); // 우수토실
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`);
// 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가
const dataWithRowNumber = (res.data.data || []).map((item: any, index: number) => {
// 🎯 먼저 각 아이템을 포맷팅
const formattedItem = formatDisplayData(item);
let ctpvCd = item?.ctpvNm ?? formattedItem?.ctpvNm ?? '';
let sggCd = item?.sggNm ?? formattedItem?.sggNm ?? '';
if (!ctpvCd || !sggCd) {
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
if (!sggCd && code) sggCd = code.slice(0, 5);
}
const matchedMhlCode = MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim());
const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd);
const ctpvNm = ctpv.find(code => code.val.trim() === ctpvCd)?.text || item?.ctpvNm;
const sggNm = sgg.find(code => code.val.trim() === sggCd)?.text || item?.sggNm;
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
// 추가 처리 및 일련번호 추가
return {
...formattedItem,
rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item?.scsYn, // scsYn이 null이면 'N'으로 변경
mqltCd: matchedMhlCode ? matchedMhlCode.text : item?.mqltCd,
usgCd: matchedMhlUsgCode ? matchedMhlUsgCode.text : (usgCdObj ? usgCdObj.text : item?.usgCd),
ctpvNm: ctpvNm ?? '-',
sggNm : sggNm ?? '-',
swtrmt_zone_nm: item.swtrmtZoneNm || item.swtrmt_zone_nm || '-',
};
});
setTableData(dataWithRowNumber);
setTableCount(res.data.count || []);
} else if (activeTab === 'operation') {
const RequestDTO: Record<string, string> = {
page: `${curPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
};
if (var1) RequestDTO.bizInfoNm = var1;
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
// 수행기간 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
RequestDTO.fromflfmtEndYmd = startDate;
RequestDTO.toflfmtEndYmd = endDate;
// JSON.parse 사용으로 문자열 -> 배열 변환
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const params = new URLSearchParams(RequestDTO).toString();
const res = await getData(`v1/bizInfo?${params}`);
const mappedContent = (res.content || []).map((item: any, index: number) => {
const formattedItem = formatDisplayData(item);
const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd);
return {
...formattedItem,
rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가
bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd,
};
});
setTableData(mappedContent || []);
setTableCount(res.totalElements || 0);
setDetailTableData([]);
setDetailTableCount(0);
}
setShowTable(true);
setIsResetSelection(false);
} catch (err) {
console.error('API ERROR:', err);
setTableData([]);
setIsResetSelection(false);
} finally {
setIsLoading(false); // 로딩 종료
setIsResetSelection(false);
// setIsResetDetailSelection(false);
}
};
// 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화
const handleSearch = () => {
setCurPage(1); // 페이지를 1로 초기화
fetchFilteredData();
setIsResetPage(true); // 페이지를 초기화
setIsResetDetailPage(true); // 디테일 페이지를 초기화
setIsResetSelection(false); // 선택 초기화
setIsResetDetailSelection(false); // 선택 초기화
setShowTable(true);
};
// 기본정보 테이블 cellClick 핸들러
const handleBasicCellClick = async (index?: number | undefined, headId?: String) => {
if (typeof index === "number") {
setIsLoading(true);
const row = tableData[index];
const layerNm = row.lyrTypeNm;
const fid = row.fid;
const core = GisApp.Module.layer as any;
await core.showSearchResultsFromGeoJSON(layerNm, fid);
setIsLoading(false);
onOpenChange(false); // SearchModal 닫기
}
};
// 운영정보 테이블 cellClick 핸들러
const handleOperationCellClick = async (index?: number | undefined, headId?: String) => {
setIsResetDetailPage(true); // 페이지를 1로 초기화
setIsResetDetailSelection(true); // 선택 초기화
setIsResetDetailPage(true); // 디테일 페이지를 초기화
if (typeof index === "number") {
setIsDetailLoading(true);
setDetailBizId(tableData[index].bizId);
setDetailCurPage(1); // 상세 페이지 초기화
try {
const RequestDTO: Record<string, string> = {
page: `${detailCurPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
bizId: tableData[index % OPERATION_TABLE_SIZE].bizId,
};
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
const formattedItem = formatDisplayData(item);
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim());
return {
...formattedItem,
rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1,
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
};
});
setDetailTableData(mappedData);
setDetailTableCount(res.count || 0);
} catch (error) {
console.error('운영정보 cellClick 에러:', error);
} finally {
setIsDetailLoading(false);
setIsResetDetailPage(false);
setIsResetDetailSelection(false);
}
}
};
// 운영정보 디테일 테이블 페이지네이션 핸들러
const handleOperationPageNation = async (index?: number | undefined) => {
if (typeof index === "number") {
setIsDetailLoading(true);
try {
const RequestDTO: Record<string, string> = {
page: `${detailCurPage - 1}`,
size: `${OPERATION_TABLE_SIZE}`,
bizId: String(detailBizId),
};
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
const formattedItem = formatDisplayData(item);
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim());
return {
...formattedItem,
rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1,
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
};
});
setDetailTableData(mappedData);
setDetailTableCount(res.count || 0);
} catch (error) {
console.error('운영정보 페이지네이션 에러:', error);
} finally {
setIsDetailLoading(false);
}
}
};
const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => {
if (typeof index === "number") {
setIsLoading(true);
const row = detailTableData[index];
const layerNm = row.lyrTypeNm;
const fid = row.fid;
const core = GisApp.Module.layer as any;
await core.showSearchResultsFromGeoJSON(layerNm, fid);
setIsLoading(false);
onOpenChange(false); // SearchModal 닫기
}
};
// 기본정보 Excel 다운로드 핸들러
const basicExcelDownload = async () => {
if (tableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
toast.dismiss("global-confirm-toast");
confirmToast(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
onAnalysis ?? (() => { }),
"text-xl font-medium text-gray-800"
);
async function onAnalysis() {
setIsLoading(true);
try {
const queryParams = new URLSearchParams({
lyrTypeNm: basicType,
size: '10000',
});
let cqlFilters: string[] = [];
let headerMapping: { [key: string]: string } = {};
let headerRange: string = "A1:F1";
switch (basicType) {
case 'tb_ms_swage_ppl': {
// 하수관로
cqlFilters = swagePplCqlFilters({
var1, var2,
ctpvCd, sggCd,
yearFilter,
start1, start2, end1, end2,
todayYear, todayMonth, todayDate,
getDateString
})
headerMapping = MsSwagePplExcelCol;
headerRange = "A1:M1";
break;
}
case 'tb_il_swage_prcs_fclt': {
// 하수처리시설
cqlFilters = swagePrcsFcltCqlFilters({
ctpvName, sggName,
yearFilter,
start1, end1,
todayYear, todayMonth, todayDate,
getDateString
});
headerMapping = SwtrmtFcltExcelCol;
headerRange = "A1:J1";
break;
}
case 'tb_ms_swage_prcs_zone': {
// 하수처리구역
cqlFilters = swagePrcsZoneCqlFilters({
ctpvCd, sggCd,
var1, var2
});
headerMapping = SwtrmtPrcsZoneExcelCol;
headerRange = "A1:D1";
break;
}
case 'tb_ms_swage_mhl': {
// 하수맨홀
cqlFilters = swageMhlCqlFilters({
ctpvCd, sggCd,
yearFilter,
start1, end1,
var1,
todayYear, todayMonth, todayDate,
getDateString
});
headerMapping = MsSwageMhlExcelCol;
headerRange = "A1:J1";
break;
}
case 'tb_ms_swage_pmstn': {
// 하수펌프장
cqlFilters = swagePmstnCqlFilters({
ctpvCd, sggCd,
yearFilter,
start1, start2, end1, end2,
todayYear, todayMonth, todayDate,
getDateString
});
headerMapping = ExclnSewagePumpExcelCol;
headerRange = "A1:J1";
break;
}
case 'tb_ms_stofcbr': {
// 우수토실
cqlFilters = stofcbrCqlFilters({
ctpvCd, sggCd,
yearFilter,
todayYear, todayMonth, todayDate,
getDateString
});
headerMapping = stofcbrExcelCol;
headerRange = "A1:H1";
break;
}
}
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`);
// const mhlCdMap = new Map(MS_SWAGE_MHL_CODES.map(code => [code.val?.trim(), code.text]));
const mhlUsgCdMap = new Map(MS_SWAGE_MHL_USE_CODES.map(code => [code.val?.trim(), code.text]));
const pplUsgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val?.trim(), code.text]));
const ctpvMap = new Map(ctpv.map(code => [code.val?.trim(), code.text]));
const sggMap = new Map(sgg.map(code => [code.val?.trim(), code.text]));
const mappedContent = (res.data.data || []).map((item: any) => {
const formattedItem = formatDisplayData(item);
let ctpvCd = item?.ctpvCd ?? formattedItem?.ctpvCd ?? '';
let sggCd = item?.sggCd ?? formattedItem?.sggCd ?? '';
if (!ctpvCd || !sggCd) {
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
if (!sggCd && code) sggCd = code.slice(0, 5);
}
// trim 한 번만 수행
const trimmedUsgCd = item?.usgCd?.trim();
// const trimmedMqltCd = item?.mqltCd?.trim();
switch (basicType) {
case 'tb_ms_swage_ppl': {
// 하수관로
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
instMngNo: formattedItem.instMngNo ?? item.instMngNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? item.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? item.sggNm ?? '-',
swtrmt_zone_nm: formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
usgCd: pplUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-',
pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '-',
prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '-',
tmptDpth: formattedItem.tmptDpth ?? item.tmptDpth ?? '-',
tmptMhlMngNo: formattedItem.tmptMhlMngNo ?? item.tmptMhlMngNo ?? '-',
scendDpth: formattedItem.scendDpth ?? item.scendDpth ?? '-',
scendMhlMngNo: formattedItem.scendMhlMngNo ?? item.scendMhlMngNo ?? '-',
};
}
case 'tb_il_swage_prcs_fclt': {
// 하수처리시설
return {
swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-',
fcltCpct: formattedItem.fcltCpct ?? item.fcltCpct ?? '-',
rgnExpln: formattedItem.rgnExpln ?? item.rgnExpln ?? '-',
oprtngStrtYmd: formattedItem.oprtngStrtYmd ?? item.oprtngStrtYmd ?? '-',
laeNm: formattedItem.laeNm ?? item.laeNm ?? '-',
sareaNm: formattedItem.sareaNm ?? item.sareaNm ?? '-',
rvrNm: formattedItem.rvrNm ?? item.rvrNm ?? '-',
dschgWtstNm: formattedItem.dschgWtstNm ?? item.dschgWtstNm ?? '-',
swtrmtPpltnCnt: formattedItem.swtrmtPpltnCnt ?? item.swtrmtPpltnCnt ?? '-',
rprsMsrcCn: formattedItem.rprsMsrcCn ?? item.rprsMsrcCn ?? '-'
};
}
case 'tb_ms_swage_prcs_zone': {
// 하수처리구역
return {
swtrmtZoneNm: formattedItem.swtrmtZoneNm ?? item.swtrmtZoneNm ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-'
};
}
case 'tb_ms_swage_mhl': {
// 하수맨홀
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
usgCd: mhlUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-',
swageMhlDiamVl: formattedItem.swageMhlDiamVl ?? item.swageMhlDiamVl ?? '-',
wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-',
vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-',
altdVl: formattedItem.altdVl ?? item.altdVl ?? '-',
dpthlVl: formattedItem.dpthlVl ?? item.dpthlVl ?? '-'
};
}
case 'tb_ms_swage_pmstn': {
// 하수펌프장
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
swagePmstnNm: formattedItem.swagePmstnNm ?? item.swagePmstnNm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
dailyPrcsCpct: formattedItem.dailyPrcsCpct ?? item.dailyPrcsCpct ?? '-',
maxImpndCpct: formattedItem.maxImpndCpct ?? item.maxImpndCpct ?? '-',
altdVl: formattedItem.altdVl ?? item.altdVl ?? '-',
wtlvVl: formattedItem.wtlvVl ?? item.wtlvVl ?? '-',
exclnWpmpAbltVl: formattedItem.exclnWpmpAbltVl ?? item.exclnWpmpAbltVl ?? '-'
};
}
case 'tb_ms_stofcbr': {
// 우수토실
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
mapshtNo: formattedItem.mapshtNo ?? item.mapshtNo ?? '-',
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
worflwWallHgt: formattedItem.worflwWallHgt ?? item.worflwWallHgt ?? '-',
swageMhlMngNo: formattedItem.swageMhlMngNo ?? item.swageMhlMngNo ?? '-',
drtnAngl: formattedItem.drtnAngl ?? item.drtnAngl ?? '-'
};
}
}
});
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedContent; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
const fileName = `${label} 조회 결과_${todayYear}${todayMonth}${todayDate}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: headerRange,
headerMapping: headerMapping
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
// 운영정보 Excel 다운로드 핸들러 추가
const operationExcelDownload = async () => {
if (tableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
toast.dismiss("global-confirm-toast");
confirmToast(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
onAnalysis ?? (() => { }),
"text-xl font-medium text-gray-800"
);
async function onAnalysis() {
setIsLoading(true);
try {
const RequestDTO: Record<string, string> = {
size: '10000',
};
if (var1) RequestDTO.bizInfoNm = var1;
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
// 수행기간 범위
const startDate = getDateString(
yearFilter.start,
'',
'',
"19000101",
);
const endDate = getDateString(
yearFilter.end,
'',
'',
`${todayYear}${todayMonth}${todayDate}`,
);
RequestDTO.fromflfmtEndYmd = startDate;
RequestDTO.toflfmtEndYmd = endDate;
// JSON.parse 사용으로 문자열 -> 배열 변환
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
const params = new URLSearchParams(RequestDTO).toString();
const res = await getData(`v1/bizInfo?${params}`);
// 헤더 매핑 정의
const headerMapping: { [key: string]: string } = {
'bizInfoNm': '사업명',
'flfmtBgngYmd': '수행시작일자',
'flfmtEndYmd': '수행종료일자',
'flfmtYr': '수행년도',
'bizTypeCd': '사업유형',
'bizAmt': '사업금액(원)',
};
const bizTyCdMap = new Map(BIZ_TY_CD.map(code => [code.lwpoCd, code.lwpoCdNm]));
const mappedContent = (res.content || []).map((item: any) => {
const formattedItem = formatDisplayData(item);
return {
bizInfoNm: formattedItem.bizInfoNm ?? item.bizInfoNm ?? '',
flfmtBgngYmd: formattedItem.flfmtBgngYmd ?? item.flfmtBgngYmd ?? '',
flfmtEndYmd: formattedItem.flfmtEndYmd ?? item.flfmtEndYmd ?? '',
flfmtYr: formattedItem.flfmtYr ?? item.flfmtYr ?? '',
bizTypeCd: bizTyCdMap.get(item.bizTypeCd) || item.bizTypeCd || formattedItem.bizTypeCd || '',
bizAmt: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.bizAmt ?? item.bizAmt),
};
});
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedContent; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const label = WORK_TYPES.find((type) => type.val === workType)?.text
const fileName = `${label} 사업 현황_${todayYear}${todayMonth}${todayDate}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: "A1:F1",
headerMapping: headerMapping
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
// 운영정보 Excel 다운로드 핸들러 추가
const operationResultExcelDownload = async () => {
if (detailTableData.length === 0) {
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
return;
}
toast.dismiss("global-confirm-toast");
confirmToast(
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
onAnalysis ?? (() => { }),
"text-xl font-medium text-gray-800"
);
async function onAnalysis() {
setIsLoading(true);
try {
const RequestDTO: Record<string, string> = {
size: '10000',
bizId: String(detailBizId),
};
const apiStartTime = performance.now();
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
const apiEndTime = performance.now();
const mappingStartTime = performance.now();
// 헤더 매핑 정의
const headerMapping: { [key: string]: string } = {
'fcltMngNo': '관리번호',
'swtrmt_zone_nm': '처리구역',
'usgCd': '용도',
'mqltCd': '재질',
'pdmtVl': '관경(mm)',
'prlgVl': '연장(m)',
'instlYmd': '설치일자',
'strc_stts_grd': '구조적 상태등급',
'oper_stts_grd': '운영적 상태등급',
};
const usgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val, code.text]));
const mqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val, code.text]));
// 🎯 단일 반복문으로 최적화 - 각 아이템마다 필요한 필드만 직접 매핑
const mappedData = (res.data || []).map((item: any) => {
const formattedItem = formatDisplayData(item);
// trim 한 번만 수행
const trimmedUsgCd = item?.usgCd?.trim();
const trimmedMqltCd = item?.mqltCd?.trim();
// zone 값 한 번만 계산
const zone = item.swtrmtZoneNm ?? formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '';
// 직접 매핑 - 반복문 없이 한 번에 객체 생성
return {
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '',
swtrmt_zone_nm: (zone === null || zone === '') ? '-' : zone,
usgCd: usgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '',
mqltCd: mqltCdMap.get(trimmedMqltCd) || item.mqltCd || formattedItem.mqltCd || '',
pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '',
prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '',
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '',
strc_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.strc_stts_grd ?? item.strc_stts_grd),
oper_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.oper_stts_grd ?? item.oper_stts_grd),
};
});
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = mappedData; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const label = WORK_TYPES.find((type) => type.val === workType)?.text
const fileName = `${label} 결과표_${todayYear}${todayMonth}${todayDate}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: [fileName],
headerRange: "A1:I1",
headerMapping: headerMapping
});
setIsLoading(false);
} catch (error) {
setIsLoading(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
}
};
// 여러 시설 보기
const viewMultipleFeature = async () => {
if (activeTab === 'basic') {
if (!(tableData.length === 0)) {
setIsLoading(true);
const row = tableData[0];
const layerNm = row.lyrTypeNm;
const fids = tableData.map((item) => item.fid);
const core = GisApp.Module.layer as any;
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
setIsLoading(false);
onOpenChange(false);
}
} else if (activeTab === 'operation') {
if (!(detailTableData.length === 0)) {
setIsLoading(true);
const row = detailTableData[0];
const layerNm = row.lyrTypeNm;
const fids = detailTableData.map((item) => item.fid);
const core = GisApp.Module.layer as any;
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
setIsLoading(false);
onOpenChange(false);
}
}
}
return {
open, onOpenChange, onFeatureSelect,
TABS,
getDateString,
handleSearch,
handleBasicCellClick,
handleOperationCellClick,
handleOperationPageNation,
handleOperationResultCellClick,
basicExcelDownload,
operationExcelDownload,
operationResultExcelDownload,
viewMultipleFeature,
};
};
@@ -1,7 +1,7 @@
"use client";
import React, { useRef, useEffect, useState } from "react";
import { Label } from "@common/components/ui/label";
import { useEffect } from "react";
export const WATER_DEPTH_LAYER_TYPES: string[] = [
'tb_ms_flood_dngr_map',
@@ -9,11 +9,11 @@ function FileUpload() {
const [file, setFile] = useState(null);
const [text, setText] = useState('');
const handleFileChange = (e) => {
const handleFileChange = (e: any) => {
setFile(e.target.files[0]);
};
const handleTextChange = (e) => {
const handleTextChange = (e: any) => {
setText(e.target.value);
};
@@ -24,7 +24,7 @@ function FileUpload() {
}
return null;
}
const handleFindUser = (e) => {
const handleFindUser = () => {
const auth = getCookie("Authorization");
if (auth) console.log("쿠키 auth : " + auth);
fetch(SEWERAGE_URL+"/v1/inftwtAnls/users/65a00f65-8460-49af-98ec-042977e56f4b", {
@@ -37,7 +37,7 @@ function FileUpload() {
console.log(data);
});
};
const handleFindUserOna = (e) => {
const handleFindUserOna = () => {
const auth = getCookie("Authorization");
if (auth) console.log("쿠키 auth : " + auth);
fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b").then((data) => {
@@ -45,7 +45,7 @@ function FileUpload() {
});
};
const handleSubmit = async (e) => {
const handleSubmit = async (e: any) => {
e.preventDefault();
if (!file) {
@@ -100,7 +100,7 @@ const Page = () => {
alert('파일을 선택해주세요')
return false
}
handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm)
handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm);
}
// 선택 파일 삭제
+5 -2
View File
@@ -80,12 +80,15 @@ export const deleteData = async (url: string, data?: object): Promise<any> => {
};
// DC API 호출
export const getDcData = async (url: string, data?: object): Promise<any> => {
export const getDcData = async (url: string, resType: string = "json"): Promise<any> => {
const res = await fetch('/dc-service' + url, {
method: "GET",
});
if (res.ok) {
const data = await res.json();
let data;
if (resType === "json") data = await res.json();
else if (resType === "blob") data = await res.blob();
return data;
}
throw new Error('Failed to fetch data');
@@ -1,27 +1,26 @@
export const handleDownload = async (api:string, fileNm:string) => {
try {
const response = await fetch(api, {
method: 'GET',
});
if (!response.ok) {
throw new Error('파일 다운로드 실패');
}
export const handleDownload = async (api: string, fileNm: string) => {
try {
const response = await fetch(api, {
method: 'GET',
});
if (!response.ok) throw new Error('파일 다운로드 실패');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const blob = await response.blob();
fileDownload(blob, fileNm);
} catch (error) {
console.error('다운로드 중 오류 발생:', error);
}
};
// 다운로드를 트리거할 가짜 <a> 태그 생성
const link = document.createElement('a');
link.href = url;
link.download = fileNm.normalize('NFC'); // 원하는 파일명 지정
document.body.appendChild(link);
link.click();
// 메모리 해제
link.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('다운로드 중 오류 발생:', error);
}
};
export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터 임시link로 연결
const url = window.URL.createObjectURL(data);
const link = document.createElement('a');
link.href = url;
link.download = fileName; // 원하는 파일명 지정
document.body.appendChild(link);
link.click();
// 메모리 해제
link.remove();
window.URL.revokeObjectURL(url);
}
+43
View File
@@ -0,0 +1,43 @@
import useReportTab, { type Folder, type ReportDataFile } from '../(map-layout)/map/searchModal/useReportTab';
import { create } from 'zustand';
import { getDcData } from '@/components/lib/api';
interface useReportTabStoreType {
isOpen: boolean;
files: ReportDataFile[];
dcTree: Folder[];
dcFiles: ReportDataFile[];
setIsOpen: (open: boolean) => void;
setFiles: (files: ReportDataFile[]) => void;
fetchDcTree: (clsfSystmNm: string) => void;
fetchDcFiles: (clsfSystmNm: string) => void;
}
export const useReportTabStore = create<useReportTabStoreType>((set, get) => ({
isOpen: false,
files: [],
dcTree: [],
dcFiles: [],
setIsOpen: (open: boolean) => set({ isOpen: open }),
setFiles: (files: ReportDataFile[]) => set({ files }),
fetchDcTree: async (clsfSystmNm: string) => {
const { dcTree } = get();
try {
const result = await getDcData("/api/v1/dc/clsf?clsfSystmNm=" + clsfSystmNm);
if (result && JSON.stringify(result) !== JSON.stringify(dcTree))
set({ dcTree: result });
} catch (error) {
console.error('Failed to fetch DC tree:', error);
}
},
fetchDcFiles: async (clsfSystmNm: string) => {
const { getDcFiles } = useReportTab();
const { dcFiles } = get();
try {
const result = await getDcFiles(clsfSystmNm);
if (result && JSON.stringify(result) !== JSON.stringify(dcFiles))
set({ dcFiles: result });
} catch (error) {
console.error('Failed to fetch DC files:', error);
}
}
}));
+10 -5
View File
@@ -23,6 +23,8 @@ const todayMonth = (today.getMonth() + 1).toString().padStart(2, "0");
const todayDate = today.getDate().toString().padStart(2, "0");
export interface SearchModalState {
// 모달 열림 상태
isSearchModalOpen: boolean;
// 탭 관련
activeTab: string;
basicType: string;
@@ -67,9 +69,9 @@ export interface SearchModalState {
todayYear: string;
todayMonth: string;
todayDate: string;
getDateString: (year: string, month: string, day: string, defaultDate: string) => string;
// setters
setIsSearchModalOpen: (open: boolean) => void;
setActiveTab: (tab: string) => void;
setBasicType: (type: string) => void;
setWorkType: (type: string) => void;
@@ -98,11 +100,13 @@ export interface SearchModalState {
setVar1: (val: string) => void;
setVar2: (val: string) => void;
setYearFilter: (filter: { start: string; end: string }) => void;
// 초기화 액션
resetFilters: () => void;
getDateString: (year: string, month: string, day: string, defaultDate: string) => string;
resetFilters: () => void; // 초기화 액션
}
const useSearchModalStore = create<SearchModalState>((set) => ({
isSearchModalOpen: false,
activeTab: "basic",
basicType: "tb_ms_swage_ppl",
workType: "['200', '201', '204']",
@@ -135,7 +139,7 @@ const useSearchModalStore = create<SearchModalState>((set) => ({
todayYear,
todayMonth,
todayDate,
getDateString,
setIsSearchModalOpen: (isSearchModalOpen) => set({ isSearchModalOpen }),
setActiveTab: (tab) => set({ activeTab: tab }),
setBasicType: (type) => set({ basicType: type }),
setWorkType: (type) => set({ workType: type }),
@@ -164,7 +168,8 @@ const useSearchModalStore = create<SearchModalState>((set) => ({
setVar1: (val) => set({ var1: val }),
setVar2: (val) => set({ var2: val }),
setYearFilter: (filter) => set({ yearFilter: filter }),
getDateString: getDateString,
// 초기화 액션
resetFilters: () =>
set({
+4 -2
View File
@@ -31,8 +31,10 @@
"@common/business_components/*": ["../../../packages/common/business_components/src/*"],
"@common/network": ["../../../packages/common/network/"],
"@common/config": ["../../../packages/common/config/dist/"],
"@utils": ["../app/components/lib/utils"],
"@/components/*": ["../app/components/*"]
"@/app/*": ["../app/*"],
"@/components/*": ["../app/components/*"],
"@/utils": ["../app/components/lib/utils"],
"@/store/*": ["../app/store/*"],
}
},
"include": [
+1 -1
View File
@@ -28,4 +28,4 @@ NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PUBLIC_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
+1 -1
View File
@@ -19,7 +19,7 @@ NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PUBLIC_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
+1 -1
View File
@@ -21,7 +21,7 @@ NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BE
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
NEXT_PUBLIC_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA=
NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA=
@@ -93,6 +93,7 @@ const Map = () => {
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as any;
if (!select) return;
@@ -107,6 +108,8 @@ const Map = () => {
if (popUpClick) {
const layerName = getWrLayerNm(data);
const featurePk = getWrFeaturePk(layerName ?? "");
// 재이용시설
if (featurePk) {
const lastPk =
@@ -114,6 +117,7 @@ const Map = () => {
? data[featurePk].replaceAll("-", "")
: data[featurePk];
featureId = `${layerName}.${lastPk}`;
} else {
console.warn("해당 피처는 선택된 레이어에서 찾을수 없습니다.");
}
@@ -160,7 +164,6 @@ const Map = () => {
setFeatureData(data);
setIsOpen(true);
setIsSearchModalOpen(false);
if (
popUpClick &&
foundFeature &&
@@ -181,6 +184,7 @@ const Map = () => {
useEffect(() => {
if (isFeatured) {
onFeatureSelect(isFeatured);
}
}, [isFeatured]);
@@ -189,6 +193,7 @@ const Map = () => {
const bc = new BroadcastChannel("mapDetailPopUp");
bc.onmessage = (event) => {
if (event.data.type === "SELECT_FEATURE") {
onFeatureSelect(event.data.feature, true);
closePopup();
}
@@ -263,8 +268,10 @@ const overlayFormatter = async (feature: any, props: any) => {
feature.getId?.() ?? p?.id ?? p?.fid ?? p?.ftrIdn ?? '';
if (!featureId) return null;
const tableId = String(featureId).split('.')[0]??'';
const fcltCd = getFeatureCd(props, tableId);
if (!fcltCd) return null;
let data: any = null;
+23 -21
View File
@@ -6,7 +6,6 @@ import {
ldrmntNodeListExport,
} from "../../matDmd/ldrmntCoSttn/headList";
export const ldrmntMapSize = 20;
export type ldrmntMapState = {
ldrmntMap: {
@@ -26,7 +25,7 @@ type ldrmntMapActions = {
con: string,
coNm: string
) => void;
ldrmntMapDetile:(featureCd:string) => Promise<customLdrmnt | undefined>;
ldrmntMapDetile: (featureCd: string) => Promise<customLdrmnt | undefined>;
};
export type ldrmntMapType = ldrmntMapState & ldrmntMapActions;
@@ -68,10 +67,10 @@ export const ldrmntMapActionsExport: (set: any) => ldrmntMapActions = (
}
},
ldrmntMapSearch: async (
page: number,
total: number,
con: string,
coNm: string
page: number,
total: number,
con: string,
coNm: string
) => {
let searchUrl = "";
@@ -98,21 +97,24 @@ export const ldrmntMapActionsExport: (set: any) => ldrmntMapActions = (
} catch (e) {
console.log(e);
}
},ldrmntMapDetile: async (featureCd: string) => {
try {
const ldrmtDetail = await getFetch(fetchType.WR,
`/reuseFc/request/temp/1/4/0/${featureCd}` )
if(ldrmtDetail&&ldrmtDetail.length>0){
return ldrmtDetail[0];
}else{
return null;
}
} catch (error) {
console.log(error);
throw error; // 호출부에서 catch 가능하도록
}
},
ldrmntMapDetile: async (featureCd: string) => {
try {
const ldrmtDetail = await getFetch(
fetchType.WR,
`/reuseFc/request/temp/1/4/0/${featureCd}`
);
if (ldrmtDetail && ldrmtDetail.length > 0) {
return ldrmtDetail.find(
(item: customLdrmnt) => item.dmdCoNm === featureCd
);
} else {
return null;
}
} catch (error) {
console.log(error);
throw error; // 호출부에서 catch 가능하도록
}
},
};
};
+16 -7
View File
@@ -96,7 +96,10 @@ export const getSelectedLayers = (isFeature: any, gisMaps: any) => {
};
export const getClickFeatureId = (data: any) => {
let featureId = "";
const layerName = data.layerNm;
if (layerName === "하수재이용시설") {
// 재이용시설
@@ -116,9 +119,15 @@ export const getClickFeatureId = (data: any) => {
} else if (layerName == "수요처") {
//수요처
featureId = `${layerName}.${data.dmdInfoId.replaceAll("-", "")}`;
} else {
featureId = `${layerName}.${data.fid || data.id || data.ftrIdn}`;
}else if (layerName == "잠재수요처") {
//잠재수요처
featureId = data.id_
} else {
featureId = data.id_
}
return featureId;
};
@@ -127,6 +136,8 @@ export function featurePopUpOpen(
featureCd: string,
popupDetailRef
) {
const url = `/mapFeaturePopup?layerNm=${encodeURIComponent(String(layerNm))}&featureCd=${encodeURIComponent(featureCd)} `;
const width =
String(layerNm) === "tb_wr_bdst_ruse_fclt"
@@ -193,11 +204,9 @@ export const getOverLayObj = (fclType: string, data: any,code?:any) => {
title = data?.bdstNm ?? '확인불가';
} else if (fclType === "온배수시설") {
newObj = {
미정: '확인불가',
// '설치완료일': data?.reuseBuilding[0]?.instlCmptnDt??'',
// '처리시설유무': data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctYn??'',
// '처리시설용량(m³/일)': data?.reuseBuilding[0]?.rainyObj.filtrTrtmfctCpct??'',
// '저류조용량(m³)': data?.reuseBuilding[0]?.rainyObj.pntfColArea??'',
'발전유형': data?.reuseBuilding[0]?.htdflObj.etgtType??'',
'설비용량': data?.reuseBuilding[0]?.htdflObj.fclCpct??'',
'담수화 여부': data?.reuseBuilding[0]?.htdflObj.plwtYn??'',
};
title = data?.bdstNm ?? '확인불가';
} else if (fclType === "수요처") {
@@ -1,50 +1,51 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import WrOverLayTable from "../WrOverLayTable/WrOverLayTable";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
Button,
} from "@common/components/ui";
import { getOverLayObj } from "@/components/lib/func/mapUtil";
import { ChevronLeft, ChevronRight } from "lucide-react";
type RuseTabData = {
data: Record<string, string>;
tabData:{
tabData: {
pbnstRuseFcltId: string;
ruseUtztnFcltNm: string;
}
}
};
};
export default function WrOverLayRuse({ overLayData }: { overLayData: any }) {
const [fcltTab, setFcltTab] = useState<string>("");
const [ruseData, setRuseData] = useState<RuseTabData[]>([]as RuseTabData[]);
// useEffect(() => {
// const detail = overLayData.find((list) => list.pbnstRuseFcltId === fcltTab);
// if (detail) setIsDetail(detail);
// }, [fcltTab, overLayData]);
const [ruseData, setRuseData] = useState<RuseTabData[]>([] as RuseTabData[]);
const [idx, setIdx] = useState(0);
const loop = true;
const go = (next: number) => {
if (loop) {
const n = (next + ruseData.length) % ruseData.length;
setIdx(n);
} else {
setIdx(Math.max(0, Math.min(ruseData.length - 1, next)));
}
};
const prev = () => go(idx - 1);
const next = () => go(idx + 1);
useEffect(() => {
if (overLayData && overLayData[0]) {
setFcltTab(overLayData[0].pbnstRuseFcltId);
const newRuseData= overLayData.map((ruse) =>{
const isData = getOverLayObj( '하수재이용시설',ruse);
return {
data: isData.newObj,
tabData:{
pbnstRuseFcltId: ruse.pbnstRuseFcltId,
ruseUtztnFcltNm: ruse.ruseUtztnFcltNm
}
}
})
const newRuseData = overLayData.map((ruse) => {
const isData = getOverLayObj("하수재이용시설", ruse);
return {
data: isData.newObj,
tabData: {
pbnstRuseFcltId: ruse.pbnstRuseFcltId,
ruseUtztnFcltNm: ruse.ruseUtztnFcltNm,
},
};
});
setRuseData(newRuseData);
setRuseData(newRuseData);
}
}, [overLayData]);
@@ -52,36 +53,42 @@ export default function WrOverLayRuse({ overLayData }: { overLayData: any }) {
return (
<>
<div>
<div className="bg-sky-600 text-white text-center font-semibold py-2 text-2xl">
{/* 커스텀 헤더: 한 칸만 보이는 슬라이더 형태 */}
<div className="mt-2 flex items-center gap-2">
<Button
variant="outline"
size="icon"
color="white"
onClick={prev}
disabled={!loop && idx === 0}
aria-label="이전"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex-1 select-none rounded-md border px-4 py-2 text-center">
{/* 가운데 이름만 노출 */}
<span className="text-xl font-medium">
{ruseData[idx]?.tabData.ruseUtztnFcltNm ?? ""}
</span>
</div>
<Tabs value={fcltTab} onValueChange={setFcltTab}>
<TabsList className="mt-2 mb-2 gap-6 flex flex-wrap">
{ruseData?.map((item, index) => {
return (
<TabsTrigger
key={item.tabData.pbnstRuseFcltId + index}
className="text-center !text-base size-3/12"
value={item.tabData.pbnstRuseFcltId}
>
{item.tabData.ruseUtztnFcltNm}
</TabsTrigger>
);
})}
</TabsList>
<Button
variant="outline"
size="icon"
onClick={next}
color="white"
disabled={!loop && idx === ruseData.length - 1}
aria-label="다음"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
{ruseData?.map((item) => (
<TabsContent
key={item.tabData.pbnstRuseFcltId}
className="flex-1 px-4 py-3"
value={item.tabData.pbnstRuseFcltId}
>
<div className="mb-4">
<WrOverLayTable overLayData={item.data} />
</div>
</TabsContent>
))}
</Tabs>
<div className="mb-4">
<WrOverLayTable overLayData={ruseData[idx]?.data??[{'확인불가':''}]} />
</div>
</div>
</>
);
@@ -71,7 +71,7 @@ export default function WrOverLay({
{/* 중앙 타이틀 */}
<div className="text-2xl text-center truncate">
{typeTitle + "(" + title + ")"}
{title === "하수재이용시설" ? title: typeTitle + "(" + title + ")"}
</div>
{/* 우측 실제 버튼 영역 */}
+1 -1
View File
@@ -11,7 +11,7 @@ const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const REUSE_API_URL = process.env.NEXT_PUBLIC_WR_SERVER_API_URL || "";
const OPERATION_API_URL = process.env.NEXT_PUBLIC_FO_SERVER_API_URL || "";
const BUSINESS_JUSO_API_URL = process.env.NEXT_PUBLIC_FINDADRESS_URL || "";
const AdressKey = process.env.NEXT_PUBLIC_WR_ADDRESS_KEY || "";
const AdressKey = process.env.NEXT_PRIVATE_WR_ADDRESS_KEY || "";
export function middleware(req: NextRequest) {
const pathname = req.nextUrl.pathname;
@@ -118,15 +118,21 @@ export default function LineBarchart({
if (!isFinite(maxValue) || maxValue <= 0) {
return { max: 0, ticks: [0] };
}
const exponent = Math.floor(Math.log10(maxValue));
const base = Math.pow(10, exponent);
const fraction = maxValue / base;
let niceFraction: number;
if (fraction <= 1) niceFraction = 1;
else if (fraction <= 2) niceFraction = 3;
else if (fraction <= 2) niceFraction = 2;
else if (fraction <= 3) niceFraction = 3;
else if (fraction <= 4) niceFraction = 4;
else if (fraction <= 5) niceFraction = 5;
else if (fraction <= 6) niceFraction = 6;
else if (fraction <= 7) niceFraction = 7;
else if (fraction <= 8) niceFraction = 8;
else if (fraction <= 9) niceFraction = 9;
else niceFraction = 10;
const niceMax = niceFraction * base; // round up to 1/2/5 * 10^k
@@ -164,7 +170,7 @@ export default function LineBarchart({
return (
<text
x={x - padding} // 맨 좌측 바깥
y={y + height / 2} // 수직 중앙
y={y + height / 2 - 12} // 수직 중앙
textAnchor="middle"
transform={`rotate(-90, ${x - padding}, ${y + height / 2})`}
className="text-[12px] font-bold"
@@ -195,12 +201,12 @@ export default function LineBarchart({
</div>
) : null}
<div className="h-[calc(100%-42px)] w-[calc(100%-40px)] ">
<ChartContainer config={chartConfig ?? {}} className="h-full w-full">
<ChartContainer config={chartConfig ?? {}} className="h-full w-[100%] ml-5">
<ComposedChart
data={chartData}
height={graphSize * 7.5}
width={graphSize * 12.5}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
margin={{ top: 5, right: 20, left: 30, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
{setXAxisYearTick?
@@ -210,7 +216,7 @@ export default function LineBarchart({
{/* 왼쪽 Y축: lineVal */}
{yFieldText && <YAxis
yAxisId="left"
className="text-[12px] font-bold"
className="text-[12px] font-bold"
tickFormatter={(value) =>
typeof value === "number" ? value.toLocaleString() : value
}
@@ -218,7 +224,6 @@ export default function LineBarchart({
ticks={leftScale.ticks}
orientation="left"
label={(props) => <CustomAxisText {...props} fieldText={yFieldText} position="left" />}
/>}
{/* 오른쪽 Y축: barVal */}
{yFieldText2 && <YAxis
@@ -316,11 +316,7 @@ function Gis({
)}
{/* SearchView가 존재할 때만 렌더링 */}
{SearchView && (
<SearchView
open={isSearchModalOpen}
onOpenChange={setIsSearchModalOpen}
onFeatureSelect={handleFeatureSelect}
/>
<SearchView />
)}
{/* 시도/시군구 조회 영역 */}
{SearchBar && (
@@ -10,8 +10,8 @@ export default function createOverlayManager(map, vectorLayer, overlayFormatter,
const getCoordinateKey = buildCoordinateKeyFactory(coordPrecision);
let currentFormatter = overlayFormatter;
const overlayMap = new Map(); // coordKey -> Overlay
const overlayElementMap = new Map(); // coordKey -> HTMLElement
const overlayMap = new Map(); // feature -> Overlay
const overlayElementMap = new Map(); // feature -> HTMLElement
const vectorSource = vectorLayer?.getSource?.();
const SYNC_MIN_INTERVAL = 120;
@@ -25,22 +25,22 @@ export default function createOverlayManager(map, vectorLayer, overlayFormatter,
const clearAllOverlaysAsync = (batch = 150) => {
const keys = Array.from(overlayMap.keys());
if (!keys.length) return;
const features = Array.from(overlayMap.keys());
if (!features.length) return;
// 재귀로 사용하는 이유 한번에 다지운는ㄴ것이아닌 150개 이상 더있을경우를 대비해서 다시 호출하기 위해(지도가 계속 멈춤)
const step = () => {
let iNum = 0;
const end = Math.min(iNum + batch, keys.length);
const end = Math.min(iNum + batch, features.length);
for (; iNum < end; iNum++) {
const k = keys[iNum];
const ov = overlayMap.get(k);
const feat = features[iNum];
const ov = overlayMap.get(feat);
try { if (ov) map.removeOverlay(ov); } catch {}
overlayMap.delete(k);
const el = overlayElementMap.get(k);
if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(k); }
overlayMap.delete(feat);
const el = overlayElementMap.get(feat);
if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(feat); }
}
if (iNum < keys.length) {
if (iNum < features.length) {
(typeof requestAnimationFrame === 'function' ? requestAnimationFrame : setTimeout)(step, 0);
}
};
@@ -75,9 +75,8 @@ export default function createOverlayManager(map, vectorLayer, overlayFormatter,
const ensureOverlay = (feature, properties) => {
const coord = getFeatureLabelCoordinate(feature);
if (!Array.isArray(coord)) return null;
const key = getCoordinateKey(coord);
let el = overlayElementMap.get(key);
let el = overlayElementMap.get(feature);
if (!el) {
if (!currentFormatter || typeof currentFormatter !== 'function') return null;
@@ -86,19 +85,19 @@ export default function createOverlayManager(map, vectorLayer, overlayFormatter,
// 1) 동기 HTMLElement
if (maybeEl && typeof maybeEl === 'object' && (maybeEl.nodeType === 1 || maybeEl.tagName)) {
el = maybeEl;
overlayElementMap.set(key, el);
overlayElementMap.set(feature, el);
}
// 2) 비동기 Promise<HTMLDivElement|null>
else if (maybeEl && typeof maybeEl.then === 'function') {
maybeEl.then((resolvedEl) => {
if (!resolvedEl || !(resolvedEl.nodeType === 1 || resolvedEl.tagName)) return;
// 이미 다른 사이클에서 만들어졌으면 덮어쓰지 않음
if (!overlayElementMap.has(key)) overlayElementMap.set(key, resolvedEl);
if (!overlayElementMap.has(feature)) overlayElementMap.set(feature, resolvedEl);
let ov = overlayMap.get(key);
let ov = overlayMap.get(feature);
if (!ov) {
ov = new Overlay({ element: resolvedEl, positioning: 'bottom-center', offset: [0, -18], stopEvent: true });
overlayMap.set(key, ov);
overlayMap.set(feature, ov);
try { map.addOverlay(ov); } catch {}
} else {
try { ov.setElement(resolvedEl); } catch {}
@@ -115,10 +114,10 @@ export default function createOverlayManager(map, vectorLayer, overlayFormatter,
}
// 여기까지 왔으면 el은 HTMLElement
let ov = overlayMap.get(key);
let ov = overlayMap.get(feature);
if (!ov) {
ov = new Overlay({ element: el, positioning: 'bottom-center', offset: [0, -18], stopEvent: true });
overlayMap.set(key, ov);
overlayMap.set(feature, ov);
map.addOverlay(ov);
}
ov.setPosition(coord);
@@ -132,52 +131,35 @@ export default function createOverlayManager(map, vectorLayer, overlayFormatter,
const size = map.getSize?.();
const viewExtent = view && size ? view.calculateExtent(size) : null;
// gather features in viewport only and group by coordinate
const groups = new Map(); // coordKey -> feature[]
const addFeature = (f) => {
const c = getFeatureLabelCoordinate(f);
if (!Array.isArray(c)) return;
const k = getCoordinateKey(c);
const arr = groups.get(k);
if (arr) arr.push(f); else groups.set(k, [f]);
};
const features = [];
const pushFeature = (f) => features.push(f);
if (vectorSource?.forEachFeatureInExtent && viewExtent) {
vectorSource.forEachFeatureInExtent(viewExtent, addFeature);
vectorSource.forEachFeatureInExtent(viewExtent, pushFeature);
} else {
vectorSource?.forEachFeature(addFeature);
vectorSource?.forEachFeature(pushFeature);
}
// cap overlays per viewport with simple uniform sampling
const keys = Array.from(groups.keys());
let selectedKeys;
if (!isFinite(maxOverlaysPerViewport) || keys.length <= maxOverlaysPerViewport) {
selectedKeys = keys;
} else {
const step = Math.ceil(keys.length / maxOverlaysPerViewport);
selectedKeys = [];
for (let i = 0; i < keys.length && selectedKeys.length < maxOverlaysPerViewport; i += step) {
selectedKeys.push(keys[i]);
let selected = features;
if (isFinite(maxOverlaysPerViewport) && features.length > maxOverlaysPerViewport) {
const step = Math.ceil(features.length / maxOverlaysPerViewport);
selected = [];
for (let i = 0; i < features.length && selected.length < maxOverlaysPerViewport; i += step) {
selected.push(features[i]);
}
}
// ensure selected overlays
for (const k of selectedKeys) {
const featuresAtKey = groups.get(k);
const rep = featuresAtKey[0];
const props = rep.getProperties?.() ?? {};
if (featuresAtKey.length > 1) props.__groupCount = featuresAtKey.length;
ensureOverlay(rep, props);
for (const f of selected) {
const props = f.getProperties?.() ?? {};
ensureOverlay(f, props);
}
// remove non-selected overlays
const keep = new Set(selectedKeys);
for (const [k, ov] of overlayMap.entries()) {
if (!keep.has(k)) {
const keep = new Set(selected);
for (const [feat, ov] of overlayMap.entries()) {
if (!keep.has(feat)) {
try { map.removeOverlay(ov); } catch {}
overlayMap.delete(k);
const el = overlayElementMap.get(k);
if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(k); }
overlayMap.delete(feat);
const el = overlayElementMap.get(feat);
if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(feat); }
}
}
};