diff --git a/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx index 826ab3222..263d9e42e 100644 --- a/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx +++ b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx @@ -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(null); const [hoveredRegion, setHoveredRegion] = useState(null); const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null); + const [chartOption, setChartOption] = useState({} as WrStatusChartProps) const svgContainerRef = useRef(null); - const [chartData] = useState([ - { - 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) 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 = { - 서울: '서울특별시', - 부산: '부산광역시', - 대구: '대구광역시', - 인천: '인천광역시', - 광주: '광주광역시', - 대전: '대전광역시', - 울산: '울산광역시', - 세종: '세종특별자치시', - 경기: '경기도', - 강원: '강원특별자치도', - 충북: '충청북도', - 충남: '충청남도', - 전북: '전라북도', - 전남: '전라남도', - 경북: '경상북도', - 경남: '경상남도', - 제주: '제주특별자치도' - }; - 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) 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 (
@@ -401,7 +237,6 @@ const WrStatusPage: React.FC = () => { {/* 상단 제목 */}

물재이용 현황

-
관리자 ▼
{/* 콘텐츠 본문 */} @@ -439,11 +274,11 @@ const WrStatusPage: React.FC = () => {
{hoveredRegion}
{colorLegend.map(({ label, color, key }) => ( -
+
{label}:{' '} - {tableData.find(d => d.region === hoveredRegion)?.[key as keyof TableData] ?? '-'} + {statusData?.find(d => d.region === hoveredRegion)?.[key as keyof DmStatusDataType] || '-'}
))} @@ -453,22 +288,12 @@ const WrStatusPage: React.FC = () => { {/* 범례 */}
-
- - 하수재이용시설 (총 717개소) -
-
- - 빗물이용시설 (총 3,730개소) -
-
- - 중수도시설 (총 1,074개소) -
-
- - 온배수시설 (총 201개소) -
+ {facilityTotals?.map((item, index) => ( +
+ + {item} +
+ )) }
@@ -479,10 +304,10 @@ const WrStatusPage: React.FC = () => { {/* 재이용시설현황 표 */}
{/* 타이틀 + 버튼 영역 */} -
+

재이용시설현황(2023)

-

행정구역별 재이용시설현황(개소)

+

행정구역별 재이용시설현황[개소(천m³/년)]

@@ -493,14 +318,18 @@ const WrStatusPage: React.FC = () => { 번호 시도 - 하수(이용량,천𝓂³/년) + 하수재이용시설 + 빗물이용시설 + 중수도시설 + 온배수시설 + {/* 하수(이용량,천𝓂³/년) 빗물(이용량,천𝓂³/년) 중수도(이용량,천𝓂³/년) - 온배수(이용량,천𝓂³/년) + 온배수(이용량,천𝓂³/년) */} - {tableData.map((row, index) => ( + {statusData?.map((row, index) => ( {index+1} @@ -526,16 +355,18 @@ const WrStatusPage: React.FC = () => {
- + {chartOption? + + :<>}
@@ -545,5 +376,3 @@ const WrStatusPage: React.FC = () => {
); }; - -export default WrStatusPage; \ No newline at end of file diff --git a/apps/dm/app/common/comFunction.ts b/apps/dm/app/common/comFunction.ts new file mode 100644 index 000000000..6d7274b95 --- /dev/null +++ b/apps/dm/app/common/comFunction.ts @@ -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 = { + 서울: '서울특별시', + 부산: '부산광역시', + 대구: '대구광역시', + 인천: '인천광역시', + 광주: '광주광역시', + 대전: '대전광역시', + 울산: '울산광역시', + 세종: '세종특별자치시', + 경기: '경기도', + 강원: '강원특별자치도', + 충북: '충청북도', + 충남: '충청남도', + 전북: '전라북도', + 전남: '전라남도', + 경북: '경상북도', + 경남: '경상남도', + 제주: '제주특별자치도' + }; + + +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] ??'', + })); +} \ No newline at end of file diff --git a/apps/dm/app/store/store.tsx b/apps/dm/app/store/store.tsx index be6a26475..f885a3bd0 100644 --- a/apps/dm/app/store/store.tsx +++ b/apps/dm/app/store/store.tsx @@ -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()((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) - } - }) -} \ No newline at end of file + ...DmMapReducer(set), + ...DmStatusReducer(set), + }; + }); +}; diff --git a/apps/dm/app/store/wrStatus/index.tsx b/apps/dm/app/store/wrStatus/index.tsx new file mode 100644 index 000000000..417bfab6c --- /dev/null +++ b/apps/dm/app/store/wrStatus/index.tsx @@ -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, + }, + })); + }, + }; +}; diff --git a/apps/dm/app/store/wrStatus/type.ts b/apps/dm/app/store/wrStatus/type.ts new file mode 100644 index 000000000..3e674227b --- /dev/null +++ b/apps/dm/app/store/wrStatus/type.ts @@ -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[]; + } \ No newline at end of file diff --git a/apps/dm/public/data/wrStatusData.json b/apps/dm/public/data/wrStatusData.json new file mode 100644 index 000000000..334b521c1 --- /dev/null +++ b/apps/dm/public/data/wrStatusData.json @@ -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" } + ] \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx index fa1d770e7..04490f741 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx @@ -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 = [ diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index e1bbc45ad..7ca34053b 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -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(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={} waterDepth={showWaterDepth ? WaterDepth : undefined} diff --git a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx index 5443bb65d..62fed6a28 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx @@ -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 = (props) => { +const BasicTab: React.FC = () => { const { basicType, setBasicType } = useSearchModalStore(); return ( @@ -45,12 +43,12 @@ const BasicTab: React.FC = (props) => {
{/* 조회 조건 */} - {basicType === "tb_ms_swage_ppl" && ()} {/* 하수관로 */} - {basicType === "tb_il_swage_prcs_fclt" && ()} {/* 하수처리시설 */} - {basicType === "tb_ms_swage_prcs_zone" && ()} {/* 하수처리구역 */} - {basicType === "tb_ms_swage_mhl" && ()} {/* 하수맨홀 */} - {basicType === "tb_ms_swage_pmstn" && ()} {/* 하수펌프장 */} - {basicType === "tb_ms_stofcbr" && ()} {/* 우수토실 */} + {basicType === "tb_ms_swage_ppl" && ()} {/* 하수관로 */} + {basicType === "tb_il_swage_prcs_fclt" && ()} {/* 하수처리시설 */} + {basicType === "tb_ms_swage_prcs_zone" && ()} {/* 하수처리구역 */} + {basicType === "tb_ms_swage_mhl" && ()} {/* 하수맨홀 */} + {basicType === "tb_ms_swage_pmstn" && ()} {/* 하수펌프장 */} + {basicType === "tb_ms_stofcbr" && ()} {/* 우수토실 */} ); }; diff --git a/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx index 6f9e532a1..c77cc58cc 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx @@ -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 = (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, diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportFiles.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportFiles.tsx deleted file mode 100644 index 0746d6489..000000000 --- a/apps/sn/app/(map-layout)/map/searchModal/reportFiles.tsx +++ /dev/null @@ -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 = ({ clsfSystmSn }: ModalOpenProp) => { - const [isLoading, setIsLoading] = useState(false); - const [isResetPage, setIsResetPage] = useState(false); - const [isResetSelection, setIsResetSelection] = useState(false); - const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useReportFileModalStore(); // 모달 상태 관리 스토어 - const [files, setFiles] = useState([]); - 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 = () => ( - <> -
- - { - return (<>조회 결과 ({filesOrigin.length.toLocaleString('ko-KR')})건); - })() - } - tableData={files} - headlist={headlist} - pageSize={10} - total={filesOrigin.length} - cellClick={handleCellClick} - onPageChange={setCurPage} - resetPageNo={isResetPage} - resetSelection={isResetSelection} - /> -
- - ); - return ( - - ); -}; - -export default React.memo(ReportFiles); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportFilesModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportFilesModal.tsx new file mode 100644 index 000000000..62c62b811 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/reportFilesModal.tsx @@ -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 = ({ 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 = () => ( + <> +
+ + { + 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} + /> +
+ + ); + + + return ( + + ); +}; + +export default React.memo(ReportFilesModal); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx index d476fedd7..1885cd8e9 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ open, isSearch = true, ctpvNm, sg setReportGubun(String(val))} selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2' /> @@ -233,7 +238,7 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg {/* 모달 컴포넌트 */} - {isModalOpen && } + {isReportFilesModalOpen && } ); }; diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx index 078f3d473..da1f05169 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -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 = (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 = (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 = (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 = (props) => { }))} onValueChange={(value: string) => setActiveTab(value)} /> - {/* 기본정보 탭 */} - {activeTab === 'basic' && ( - - )} - {/* 운영정보 탭 */} - {activeTab === 'operation' && ( - - )} - {/* 하수도정비기본계획보고서 탭 */} - {activeTab === 'report' && ( - - )} + {activeTab === 'basic' && ()} {/* 기본정보 탭 */} + {activeTab === 'operation' && ()} {/* 운영정보 탭 */} + {activeTab === 'report' && ()} {/* 하수도정비기본계획보고서 탭 */} {/* 📋 결과 리스트 */} @@ -192,8 +183,8 @@ const SearchModal: React.FC = (props) => { return ( <> { - 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 => { - 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([]); - - 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((set) => ({ - isOpen: false, - setIsOpen: (open: boolean) => set({ isOpen: open }), -})); diff --git a/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx b/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx index 53d8866f7..11c4c8bfa 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx @@ -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 = (props) => { +const Stofcbr: React.FC = () => { const { ctpvCd, setCtpvCd, sggCd, setSggCd, yearFilter, setYearFilter, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return ( diff --git a/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx b/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx index 302105c63..04ffeb1fa 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx @@ -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 = (props) => { +const SwageMhl: React.FC = () => { const { var1, setVar1, start1, setStart1, end1, setEnd1, @@ -68,7 +57,7 @@ const SwageMhl: React.FC = (props) => { sggCd, setSggCd, yearFilter, setYearFilter, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return (
diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx index 9286ccbdb..f3eef5d66 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx @@ -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 = (props) => { +const SwagePmstn: React.FC = () => { const { start1, setStart1, end1, setEnd1, @@ -71,7 +59,7 @@ const SwagePmstn: React.FC = (props) => { sggCd, setSggCd, yearFilter, setYearFilter, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return (
diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx index 71a0e9935..0a79d8f81 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx @@ -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 = (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 ( diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx index dcc5546ad..97486e4a3 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx @@ -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 = (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 (
diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx index 84e30c5ee..dcda670f2 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx @@ -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 = (props) => { +const swagePrcsZone: React.FC = () => { const { var1, setVar1, var2, setVar2, ctpvCd, setCtpvCd, sggCd, setSggCd, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return (
diff --git a/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts new file mode 100644 index 000000000..1d49529da --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts @@ -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 }; +}; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/useOperationTab.ts b/apps/sn/app/(map-layout)/map/searchModal/useOperationTab.ts new file mode 100644 index 000000000..dd7b8d3cc --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/useOperationTab.ts @@ -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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 + }; +}; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/useReportTab.ts b/apps/sn/app/(map-layout)/map/searchModal/useReportTab.ts new file mode 100644 index 000000000..52fabc9f0 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/useReportTab.ts @@ -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 => { + 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 + }; +} diff --git a/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.ts b/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.ts new file mode 100644 index 000000000..ba376290d --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.ts @@ -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 = { + 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 }; +}; diff --git a/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.tsx deleted file mode 100644 index 74695a53f..000000000 --- a/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.tsx +++ /dev/null @@ -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 = { - 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 = { - 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 = { - 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 = { - 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 = { - 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, - }; -}; diff --git a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx index a1434d895..be78c069c 100644 --- a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx +++ b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx @@ -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', diff --git a/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx b/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx index 29d7f464e..75ddd89a0 100644 --- a/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx @@ -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) { diff --git a/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx b/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx index a23f3330f..08ac017be 100644 --- a/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx +++ b/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx @@ -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); } // 선택 파일 삭제 diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index 0da6984a0..4ffab546b 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -80,12 +80,15 @@ export const deleteData = async (url: string, data?: object): Promise => { }; // DC API 호출 -export const getDcData = async (url: string, data?: object): Promise => { +export const getDcData = async (url: string, resType: string = "json"): Promise => { 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'); diff --git a/apps/sn/app/components/lib/utils/fileDownload.ts b/apps/sn/app/components/lib/utils/fileDownload.ts index 79d337e3d..047847256 100644 --- a/apps/sn/app/components/lib/utils/fileDownload.ts +++ b/apps/sn/app/components/lib/utils/fileDownload.ts @@ -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); + } +}; - // 다운로드를 트리거할 가짜 태그 생성 - 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); - } -}; \ No newline at end of file +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); +} \ No newline at end of file diff --git a/apps/sn/app/store/useReportTabStore.ts b/apps/sn/app/store/useReportTabStore.ts new file mode 100644 index 000000000..05f43ebc5 --- /dev/null +++ b/apps/sn/app/store/useReportTabStore.ts @@ -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((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); + } + } +})); \ No newline at end of file diff --git a/apps/sn/app/store/useSearchModalStore.ts b/apps/sn/app/store/useSearchModalStore.ts index 8ad1c356c..acb93969d 100644 --- a/apps/sn/app/store/useSearchModalStore.ts +++ b/apps/sn/app/store/useSearchModalStore.ts @@ -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((set) => ({ + isSearchModalOpen: false, activeTab: "basic", basicType: "tb_ms_swage_ppl", workType: "['200', '201', '204']", @@ -135,7 +139,7 @@ const useSearchModalStore = create((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((set) => ({ setVar1: (val) => set({ var1: val }), setVar2: (val) => set({ var2: val }), setYearFilter: (filter) => set({ yearFilter: filter }), - + + getDateString: getDateString, // 초기화 액션 resetFilters: () => set({ diff --git a/apps/sn/tsconfig.json b/apps/sn/tsconfig.json index 078e6ae2b..1634b3f08 100644 --- a/apps/sn/tsconfig.json +++ b/apps/sn/tsconfig.json @@ -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": [ diff --git a/apps/wr/.env.development b/apps/wr/.env.development index d3ec10727..769d229e2 100644 --- a/apps/wr/.env.development +++ b/apps/wr/.env.development @@ -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= diff --git a/apps/wr/.env.local b/apps/wr/.env.local index 547195433..df38635ea 100644 --- a/apps/wr/.env.local +++ b/apps/wr/.env.local @@ -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 diff --git a/apps/wr/.env.production b/apps/wr/.env.production index aef204e50..e5900440f 100644 --- a/apps/wr/.env.production +++ b/apps/wr/.env.production @@ -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= diff --git a/apps/wr/app/(map-layout)/map/mainMap/page.tsx b/apps/wr/app/(map-layout)/map/mainMap/page.tsx index 52955d457..362d45cb8 100644 --- a/apps/wr/app/(map-layout)/map/mainMap/page.tsx +++ b/apps/wr/app/(map-layout)/map/mainMap/page.tsx @@ -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; diff --git a/apps/wr/app/store/map/ldrmntCoMap/index.tsx b/apps/wr/app/store/map/ldrmntCoMap/index.tsx index 651fe21f2..baac27509 100644 --- a/apps/wr/app/store/map/ldrmntCoMap/index.tsx +++ b/apps/wr/app/store/map/ldrmntCoMap/index.tsx @@ -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; + ldrmntMapDetile: (featureCd: string) => Promise; }; 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 가능하도록 } + }, }; }; diff --git a/apps/wr/components/lib/func/mapUtil.ts b/apps/wr/components/lib/func/mapUtil.ts index 925961b88..13ce5b82a 100644 --- a/apps/wr/components/lib/func/mapUtil.ts +++ b/apps/wr/components/lib/func/mapUtil.ts @@ -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 === "수요처") { diff --git a/apps/wr/components/ui/map/wrOverLay/WrOverLayRuse/WrOverLayRuse.tsx b/apps/wr/components/ui/map/wrOverLay/WrOverLayRuse/WrOverLayRuse.tsx index 62369c348..0a4b98b21 100644 --- a/apps/wr/components/ui/map/wrOverLay/WrOverLayRuse/WrOverLayRuse.tsx +++ b/apps/wr/components/ui/map/wrOverLay/WrOverLayRuse/WrOverLayRuse.tsx @@ -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; - tabData:{ + tabData: { pbnstRuseFcltId: string; ruseUtztnFcltNm: string; - } -} - + }; +}; export default function WrOverLayRuse({ overLayData }: { overLayData: any }) { - const [fcltTab, setFcltTab] = useState(""); - const [ruseData, setRuseData] = useState([]as RuseTabData[]); - // useEffect(() => { - // const detail = overLayData.find((list) => list.pbnstRuseFcltId === fcltTab); - // if (detail) setIsDetail(detail); - // }, [fcltTab, overLayData]); + const [ruseData, setRuseData] = useState([] 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 ( <>
-
+ + {/* 커스텀 헤더: 한 칸만 보이는 슬라이더 형태 */} +
+ + +
+ {/* 가운데 이름만 노출 */} + + {ruseData[idx]?.tabData.ruseUtztnFcltNm ?? ""} +
- - - {ruseData?.map((item, index) => { - return ( - - {item.tabData.ruseUtztnFcltNm} - - ); - })} - + +
- {ruseData?.map((item) => ( - -
- -
-
- ))} - +
+ +
); diff --git a/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx index 225b2ff4c..a6d04973d 100644 --- a/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx +++ b/apps/wr/components/ui/map/wrOverLay/wrOverLay.tsx @@ -71,7 +71,7 @@ export default function WrOverLay({ {/* 중앙 타이틀 */}
- {typeTitle + "(" + title + ")"} + {title === "하수재이용시설" ? title: typeTitle + "(" + title + ")"}
{/* 우측 실제 버튼 영역 */} diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index 716a851b8..5dc77eab4 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -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; diff --git a/packages/common/business_components/src/ui/chart/LineBarChart.tsx b/packages/common/business_components/src/ui/chart/LineBarChart.tsx index eee09fb65..f26cb976c 100644 --- a/packages/common/business_components/src/ui/chart/LineBarChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineBarChart.tsx @@ -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 ( ) : null}
- + {setXAxisYearTick? @@ -210,7 +216,7 @@ export default function LineBarchart({ {/* 왼쪽 Y축: lineVal */} {yFieldText && typeof value === "number" ? value.toLocaleString() : value } @@ -218,7 +224,6 @@ export default function LineBarchart({ ticks={leftScale.ticks} orientation="left" label={(props) => } - />} {/* 오른쪽 Y축: barVal */} {yFieldText2 && + )} {/* 시도/시군구 조회 영역 */} {SearchBar && ( diff --git a/packages/common/business_components/src/ui/map/GisOverlay.js b/packages/common/business_components/src/ui/map/GisOverlay.js index ca22cf9c9..387f74c83 100644 --- a/packages/common/business_components/src/ui/map/GisOverlay.js +++ b/packages/common/business_components/src/ui/map/GisOverlay.js @@ -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 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); } } } };