From c7b22b3e959a26f7621b38f7e9a4877a321ced94 Mon Sep 17 00:00:00 2001 From: ljpark Date: Wed, 25 Mar 2026 13:03:39 +0900 Subject: [PATCH] =?UTF-8?q?sn:=20=ED=95=98=EC=88=98=EA=B4=80=EB=A1=9C=20ht?= =?UTF-8?q?ml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/html/(map-layout)/layout.tsx | 58 ++ .../map/components/InputSection.tsx | 58 ++ .../map/components/common/codeMapper.ts | 162 ++++ .../map/components/common/index.ts | 68 ++ .../map/components/common/table.ts | 848 ++++++++++++++++++ .../components/popupComponent/MsSwagePpl.tsx | 163 ++++ .../map/components/popupComponent/Ssc.tsx | 358 ++++++++ .../components/popupComponent/SwtrmtFclt.tsx | 73 ++ .../popupComponent/chart/aplcnYms.ts | 271 ++++++ .../popupComponent/chart/infilChart.tsx | 297 ++++++ .../popupComponent/chart/rdiiChart.tsx | 357 ++++++++ .../popupComponent/chartModalStore.ts | 16 + .../popupComponent/featureClickEventPopup.tsx | 639 +++++++++++++ .../components/popupComponent/sewagePump.tsx | 69 ++ .../map/components/popupComponent/smh.tsx | 146 +++ .../popupComponent/stormWaterPump.tsx | 37 + apps/sn/app/html/(map-layout)/map/page.tsx | 184 ++++ .../(map-layout)/map/searchModal/basicTab.tsx | 66 ++ .../map/searchModal/operationTab.tsx | 98 ++ .../map/searchModal/reportFilesModal.tsx | 108 +++ .../map/searchModal/reportTab.tsx | 272 ++++++ .../map/searchModal/searchModal.tsx | 238 +++++ .../(map-layout)/map/searchModal/stofcbr.tsx | 92 ++ .../(map-layout)/map/searchModal/swageMhl.tsx | 156 ++++ .../map/searchModal/swagePmstn.tsx | 175 ++++ .../(map-layout)/map/searchModal/swagePpl.tsx | 209 +++++ .../map/searchModal/swagePrcsFclt.tsx | 150 ++++ .../map/searchModal/swagePrcsZone.tsx | 93 ++ .../map/searchModal/useBasicTab.ts | 348 +++++++ .../map/searchModal/useOperationTab.ts | 452 ++++++++++ .../map/searchModal/useReportTab.ts | 188 ++++ .../map/searchModal/useSearchModal.ts | 161 ++++ .../map/waterDepth/waterDepth.tsx | 98 ++ 33 files changed, 6708 insertions(+) create mode 100644 apps/sn/app/html/(map-layout)/layout.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/InputSection.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/common/codeMapper.ts create mode 100644 apps/sn/app/html/(map-layout)/map/components/common/index.ts create mode 100644 apps/sn/app/html/(map-layout)/map/components/common/table.ts create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/Ssc.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/infilChart.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/chartModalStore.ts create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/sewagePump.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/smh.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/components/popupComponent/stormWaterPump.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/page.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/basicTab.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/operationTab.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/reportFilesModal.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/reportTab.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/searchModal.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/stofcbr.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/swageMhl.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/swagePmstn.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/swagePpl.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/swagePrcsFclt.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/swagePrcsZone.tsx create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/useBasicTab.ts create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/useOperationTab.ts create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/useReportTab.ts create mode 100644 apps/sn/app/html/(map-layout)/map/searchModal/useSearchModal.ts create mode 100644 apps/sn/app/html/(map-layout)/map/waterDepth/waterDepth.tsx diff --git a/apps/sn/app/html/(map-layout)/layout.tsx b/apps/sn/app/html/(map-layout)/layout.tsx new file mode 100644 index 000000000..25500a909 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/layout.tsx @@ -0,0 +1,58 @@ +/** + * ➡️ 경로 : 하수관망도 모든 페이지 + * + * 📄 Map Layout + * Next.js App Router (app/) 기반 맵 레이아웃 컴포넌트 + * + * - 메뉴 클릭 이벤트 처리 및 라우팅 + * - 현재 메뉴 정보 Zustand를 통한 상태 관리 + * - 시도/시군구/사업 구분/사업 유형 데이터 초기 로드 + * - NoFooterLayout을 통한 공통 레이아웃 적용 + * - QueryClient를 통한 데이터 캐싱 및 관리 + * - 경로 변경 시 메뉴 정보 동기화 + */ +"use client"; + +import React, { useEffect } from 'react'; +import { usePathname, useRouter } from "next/navigation"; +import { useQueryClient } from "@tanstack/react-query"; +import { NoFooterLayout } from "@common/components"; +import type { MenuItem } from "@/entities/menu"; +import { useMenuStore } from "@/shared/lib"; +import useStore from "@/store/useStore"; + +export default function MapLayout({ + children, +}: { + children: React.ReactNode; +}) { + const router = useRouter(); + const pathname = usePathname(); + const queryClient = useQueryClient(); + + const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo); + + const { fetchCtpv, fetchSgg, fetchBIZ_SE_CD, fetchBIZ_TY_CD } = useStore(); + + useEffect(() => { + fetchCtpv(); + fetchSgg(); + fetchBIZ_SE_CD(); + fetchBIZ_TY_CD(); + }, []); + + const handleMenuClick = (item: MenuItem) => { + setCurrentMenuInfo(item); + router.push(item.urlPath); + }; + + return ( + + {children} + + ); +} diff --git a/apps/sn/app/html/(map-layout)/map/components/InputSection.tsx b/apps/sn/app/html/(map-layout)/map/components/InputSection.tsx new file mode 100644 index 000000000..1d45103a1 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/InputSection.tsx @@ -0,0 +1,58 @@ +import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@common/components/ui"; +import React from "react"; + +interface InputSectionProps { + title: string; + data: { year: string; type: string, bizInfoNm: string, bizType: string }[]; + sectionKey: string; + onInputChange: (section: string, index: number, field: string, value: string) => void; +} + +const InputSection: React.FC = ({ title, data, sectionKey, onInputChange }) => { + return ( +
+ {/* 섹션 제목 */} +
+

{title}

+
+ {/* 테이블 */} +
+ + + + 구분 + 진단연도 + 사업명 + 유형 + + + + {data.map((row, index) => ( + + + {row.type} + + + onInputChange(sectionKey, index, "year", e.target.value)} + className="w-full text-xl border-radius-0 px-3 py-2 text-base text-center leading-normal" + /> + + + {row.bizInfoNm} + + + {row.bizType} + + + ))} + +
+
+
+ ); +}; + +export default InputSection; \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/components/common/codeMapper.ts b/apps/sn/app/html/(map-layout)/map/components/common/codeMapper.ts new file mode 100644 index 000000000..7b591b09c --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/common/codeMapper.ts @@ -0,0 +1,162 @@ +import { MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from './table'; + +interface CodeMapperProps { + layerName: string; + data: any; +} + +/** + * 레이어별 코드 변환 함수 + * @param layerName - 레이어 이름 + * @param data - 변환할 데이터 객체 + * @returns 변환된 데이터 객체 + */ +export const codeMapper = ({ layerName, data }: CodeMapperProps): any => { + if (!data) return data; + + const processedData = { ...data }; + + switch (layerName) { + case '하수관로': + // usg_cd 값 변환 (용도코드) + if (processedData.usg_cd) { + const usageCodeItem = MS_SWAGE_PPL_USE_CODES.find( + code => code.val === processedData.usg_cd.trim() + ); + if (usageCodeItem) { + processedData.usg_cd = usageCodeItem.text; + } + } + + // mqlt_cd 값 변환 (재질코드) + if (processedData.mqlt_cd) { + const materialCodeItem = MS_SWAGE_PPL_CODES.find( + code => code.val === processedData.mqlt_cd.trim() + ); + if (materialCodeItem) { + processedData.mqlt_cd = materialCodeItem.text; + } + } + break; + + case '하수맨홀': + // usg_cd 값 변환 (용도코드) + if (processedData.usg_cd) { + const usageCodeItem = MS_SWAGE_MHL_USE_CODES.find( + code => code.val.trim() === processedData.usg_cd.trim() + ); + if (usageCodeItem) { + processedData.usg_cd = usageCodeItem.text; + } + } + + // mqlt_cd 값 변환 (재질코드) + if (processedData.mqlt_cd) { + const materialCodeItem = MS_SWAGE_MHL_CODES.find( + code => code.val === processedData.mqlt_cd.trim() + ); + if (materialCodeItem) { + processedData.mqlt_cd = materialCodeItem.text; + } + } + break; + + case '하수펌프장': + // usg_cd 값 변환 (용도코드) + if (processedData.usg_cd) { + const usageCodeItem = DRTW_SEWAGE_PUMP_USE_CODES.find( + code => code.val === processedData.usg_cd.trim() + ); + if (usageCodeItem) { + processedData.usg_cd = usageCodeItem.text; + } + } + break; + + case '우수토실': + // prrtymngYn 값 변환 (중점여부) + if (processedData.prrtymngYn) { + if (processedData.prrtymngYn === 'Y') { + processedData.prrtymngYn = '중점우수토실'; + } else if (processedData.prrtymngYn === 'N') { + processedData.prrtymngYn = '우수토실'; + } + } + break; + case '중점우수토실': + if (processedData.manage_no) { + processedData.mqlt_cd = processedData.manage_no; + } + if (processedData.mesure_spot_se) { + const mesureSpotSe = [{ id: '0', text: '합류식' }, { id: '1', text: '분류식' }]; + const mesureSpotSeItem = mesureSpotSe.find( + code => code.id === processedData.mesure_spot_se.trim() + ); + if (mesureSpotSeItem) processedData.mesure_spot_se = mesureSpotSeItem.text; + } + break; + + default: + // 다른 레이어는 변환하지 않음 + break; + } + + return processedData; +}; + +/** + * 저장 시 코드 역변환 함수 (한글 → 코드) + * @param layerName - 레이어 이름 + * @param data - 역변환할 데이터 객체 + * @returns 역변환된 데이터 객체 + */ +export const reverseCodeMapper = ({ layerName, data }: CodeMapperProps): any => { + if (!data) return data; + + const processedData = { ...data }; + + switch (layerName) { + case '하수맨홀': + // mqlt_cd 값 역변환 (텍스트 → 코드) + if (processedData.mqlt_cd && typeof processedData.mqlt_cd === 'string') { + const materialCodeItem = MS_SWAGE_MHL_CODES.find( + code => code.text === processedData.mqlt_cd.trim() + ); + if (materialCodeItem) { + processedData.mqlt_cd = materialCodeItem.val; + } + } + break; + + case '우수토실': + // prrtymngYn 값 역변환 (텍스트 → Y/N) + if (processedData.prrtymngYn) { + if (processedData.prrtymngYn === '중점우수토실') { + processedData.prrtymngYn = 'Y'; + } else if (processedData.prrtymngYn === '우수토실') { + processedData.prrtymngYn = 'N'; + } + } + break; + + case '하수펌프장': + // usg_cd 값 역변환 (텍스트 → 코드) + if (processedData.usg_cd && typeof processedData.usg_cd === 'string') { + const usageCodeItem = DRTW_SEWAGE_PUMP_USE_CODES.find( + code => code.text === processedData.usg_cd.trim() + ); + if (usageCodeItem) { + processedData.usg_cd = usageCodeItem.val; + } + } + break; + + default: + break; + } + + return processedData; +}; + +// 기본 내보내기 +export default { codeMapper, reverseCodeMapper }; \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/components/common/index.ts b/apps/sn/app/html/(map-layout)/map/components/common/index.ts new file mode 100644 index 000000000..d2aed5956 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/common/index.ts @@ -0,0 +1,68 @@ +import MsSwagePpl from "../popupComponent/MsSwagePpl"; // 하수관로 +import sewagePump from "../popupComponent/sewagePump"; // 하수펌프 +import smh from "../popupComponent/smh"; +import Ssc from "../popupComponent/Ssc"; +import StormWaterPump from "../popupComponent/stormWaterPump"; +import SwtrmtFclt from "../popupComponent/SwtrmtFclt"; + +type LayerComponentProps = { + featureData: any; + isMainSsc?: boolean; + isDrtwSewagePump?: boolean; + rainMeasureData?: any; + onSaveClick?: () => void; + onFormDataChange?: (data: any) => void; // 추가 + chmbrXcnts: string; + chmbrYdnts: string; + gisObject?: Record; // GIS 객체 추가 +}; +type LayerContentMap = { + [key: string]: { + component?: React.FC; + props?: any; + }; +}; +// 레이어 이름과 컴포넌트를 매핑 +// 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업 +const layerContentMap: LayerContentMap = { + "하수처리시설": { component: SwtrmtFclt, }, // 디지털 자료관 연계정보가 없으니 일단 주석 처리 + "하수처리구역": {}, + "우수토실": { component: Ssc, }, + "중점우수토실": { component: Ssc, }, // 중점우수토실 여부 설정 + "하수관로": { component: MsSwagePpl, }, + "하수맨홀": { component: smh, }, + "하수펌프장": { component: sewagePump }, + // "빗물펌프": { component: StormWaterPump }, + // "오수펌프": { component: sewagePump }, +}; + +// 동적 컴포넌트 반환 함수 +export const getLayerContentComponent = (layerNm: string): React.FC | null => { + const component = layerContentMap[layerNm]; + if (!component || !component.component) { + // component가 없으면 null 반환 + return () => null; + } + return component.component +}; +// 사용자 입력 필드 (보수 및 갱신, 청소 및 준설, 점검 및 진단) +export const handleInputChange = (section: string, + index: number, + field: string, + value: string, + dataMap: { + [key: string]: { + data: { year: string; type: string, bizInfoNm: string, bizType: string }[]; + setData: React.Dispatch>; + }; + } +) => { + const target = dataMap[section]; + if (!target) return; + + const updated = [...target.data]; + if (updated[index]) { + updated[index][field as keyof typeof updated[number]] = value; + } + target.setData(updated); +}; \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/components/common/table.ts b/apps/sn/app/html/(map-layout)/map/components/common/table.ts new file mode 100644 index 000000000..4e501fb20 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/common/table.ts @@ -0,0 +1,848 @@ +// 하수관로 +export const MsSwagePplCol = [ + { id: 'fclt_mng_no', title: '시설관리번호' }, + { id: 'inst_mng_no', title: '기관관리번호' }, + { id: 'instl_ymd', title: '설치일자' }, + { id: 'mapsht_no', title: '도엽번호' }, + { id: 'pdmt_vl', title: '관경(mm)' }, + { id: 'prlg_vl', title: '연장(m)' }, + { id: 'usg_cd', title: '용도' }, + { id: 'mqlt_cd', title: '재질' }, + { id: 'avg_grdn', title: '평균구배(m/m)' }, + { id: 'drtw_mltv_area', title: '오수배수면적(m²)' }, + { id: 'excln_mltv_area', title: '우수배수면적(m²)' }, + { id: 'tmpt_dpth', title: '시점깊이(m)' }, + { id: 'tmpt_mhl_mng_no', title: '시점맨홀관리번호' }, + { id: 'scend_dpth', title: '종점깊이(m)' }, + { id: 'scend_mhl_mng_no', title: '종점맨홀관리번호' }, + { id: 'trfln_cnt', title: '차선수' }, + { id: 'vrtc_len', title: '세로길이(m)' }, + { id: 'wdth_len', title: '가로길이(m)' }, + // { id: 'rnyssn_fvlc', title: '우기 시 유속(m/s)' }, + // { id: 'clar_fvlc', title: '건기 시 유속(m/s)' }, + // { id: 'mng_inst_cd', title: '관리기관코드' }, + // { id: 'pbadms_emd_cd', title: '행정읍면동코드' }, + // { id: 'fclty_shp_cd', title: '시설물형태코드' }, + // { id: 'fid', title: 'FID' }, + // { id: 'ftre_smbol_cd', title: '지형지물부호코드' }, + // { id: 'lyrTypeNm', title: '레이어타입명' }, +] +// 하수관로 searchModal 기본정보tab +export const MsSwagePplCol2 = [ + { id: 'rowNumber', title: '일련\n번호' }, + { id: 'fcltMngNo', title: '시설\n관리번호' }, + { id: 'instMngNo', title: '기관\n관리번호' }, + { id: 'ctpvNm', title: '시도' }, + { id: 'sggNm', title: '시군구' }, + { id: "swtrmt_zone_nm", title: "처리구역" }, + { id: 'instlYmd', title: '설치일자' }, + // { id: 'mapshtNo', title: '도엽번호' }, + { id: 'usgCd', title: '용도' }, + { id: 'pdmtVl', title: '관경\n(mm)' }, + { id: 'prlgVl', title: '연장\n(m)' }, + // { id: 'avgGrdn', title: '평균구배\n(m/m)' }, + { id: 'tmptDpth', title: '시점깊이\n(m)' }, + { id: 'tmptMhlMngNo', title: '시점맨홀\n관리번호' }, + { id: 'scendDpth', title: '종점깊이\n(m)' }, + { id: 'scendMhlMngNo', title: '종점맨홀\n관리번호' }, + { id: 'vrtcLen', title: '세로길이\n(m)' }, + { id: 'wdthLen', title: '가로길이\n(m)' }, + { id: 'fcltyShpCd', title: '시설물형태코드' }, + // { id: 'fid', title: 'FID' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'mqltCd', title: '재질코드' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + // { id: 'usgCd', title: '용도코드' }, + // { id: 'lyrTypeNm', title: '레이어타입명' }, +] +export const MsSwagePplExcelCol = { + 'fcltMngNo': '시설 관리번호', + 'instMngNo': '기관 관리번호', + 'ctpvNm': '시도', + 'sggNm': '시군구', + 'swtrmt_zone_nm': '처리구역', + 'instlYmd': '설치일자', + 'usgCd': '용도', + 'pdmtVl': '관경(mm)', + 'prlgVl': '연장(m)', + 'tmptDpth': '시점 깊이(m)', + 'tmptMhlMngNo': '시점맨홀 관리번호', + 'scendDpth': '종점 깊이(m)', + 'scendMhlMngNo': '종점맨홀 관리번호', + 'vrtcLen': '세로길이(m)', + 'wdthLen': '가로길이(m)', + 'fcltyShpCd': '시설물형태코드', + 'mqltCd': '재질코드', +}; +// 하수관로 용도 코드표 +export const MS_SWAGE_PPL_USE_CODES = [ + { val: "SBA000", text: "미분류" }, + { val: "SBA001", text: "합류관" }, + { val: "SBA002", text: "차집관" }, + { val: "SBA003", text: "우수관" }, + { val: "SBA004", text: "오수관" }, + { val: "SBA999", text: "기타" }, +]; +// 하수관로 재질 코드표 +export const MS_SWAGE_PPL_CODES = [ + { val: "MOP000", text: "미분류" }, + { val: "MOP001", text: "닥타일주철관(DCIP)" }, + { val: "MOP002", text: "강관(SP)" }, + { val: "MOP003", text: "아연도강관(GS)" }, + { val: "MOP004", text: "염화비닐관(PVC)" }, + { val: "MOP005", text: "폴리에틸렌관(PE)" }, + { val: "MOP006", text: "스테인레스관(SSP)" }, + { val: "MOP007", text: "동관(COP)" }, + { val: "MOP008", text: "P.S.콘크리트관(PC)" }, + { val: "MOP009", text: "석면시멘트관(AS)" }, + { val: "MOP010", text: "회주철관(CIP)" }, + { val: "MOP011", text: "연관(LP)" }, + { val: "MOP012", text: "흉관(HP)" }, + { val: "MOP013", text: "철근콘크리트관(RC)" }, + { val: "MOP014", text: "강관(ST)" }, + { val: "MOP015", text: "주철관(CI)" }, + { val: "MOP016", text: "화학제품류관(PV)" }, + { val: "MOP017", text: "도관(VC)" }, + { val: "MOP018", text: "석축(SE)" }, + { val: "MOP019", text: "도복장 강관(SP)" }, + { val: "MOP020", text: "흉관(EP)" }, + { val: "MOP021", text: "아연도강관(GP)" }, + { val: "MOP022", text: "청동관(BR)" }, + { val: "MOP023", text: "고무몰딩(RM)" }, + { val: "MOP024", text: "주강(CS)" }, + { val: "MOP025", text: "VR관" }, + { val: "MOP026", text: "HDPE관" }, + { val: "MOP027", text: "시멘트라이닝주철관(C)" }, + { val: "MOP028", text: "수도용 PE관(PE)" }, + { val: "MOP029", text: "석면시멘트관(AC)" }, + { val: "MOP030", text: "폴리에틸렌분체라이닝강관(PEP)" }, + { val: "MOP031", text: "에폭시코팅강관(EPS)" }, + { val: "MOP032", text: "HI-3P" }, + { val: "MOP033", text: "철근콘크리트박스(RCB)" }, + { val: "MOP034", text: "콘크리트중선함(CD)" }, + { val: "MOP035", text: "후랙시불전선함(FP)" }, + { val: "MOP036", text: "전선함(FD)" }, + { val: "MOP038", text: "이중보온관" }, + { val: "MOP039", text: "폴리에틸렌피복강관(PLP)" }, + { val: "MOP041", text: "FRP" }, + { val: "MOP042", text: "강재" }, + { val: "MOP999", text: "기타" }, + { val: "MOP800", text: "2중벽PE관" }, + { val: "MOP801", text: "3중벽PE관" }, + { val: "MOP802", text: "가스배관용 강관" }, + { val: "MOP803", text: "강관(피복 Double, 용접접합)" }, + { val: "MOP804", text: "강관(피복 Single, 용접접합)" }, + { val: "MOP805", text: "강관(CP)" }, + { val: "MOP806", text: "강철" }, + { val: "MOP807", text: "강화플라스틱복합관" }, + { val: "MOP808", text: "경질염화비닐관(HI-VP)" }, + { val: "MOP809", text: "고강성PVC이중벽관" }, + { val: "MOP810", text: "고광도PVC이중벽관" }, + { val: "MOP811", text: "고밀도폴리에틸렌관(THP)" }, + { val: "MOP812", text: "고밀도폴리에틸렌파상형이중벽관" }, + { val: "MOP813", text: "공장제 박스관거" }, + { val: "MOP814", text: "관로없음(알케이블)" }, + { val: "MOP815", text: "나선형강관" }, + { val: "MOP816", text: "내충격수도관" }, + { val: "MOP817", text: "내충격용하수도용경질염화비닐관(HI-VG)" }, + { val: "MOP818", text: "내충격HI-VG2관" }, + { val: "MOP819", text: "내충격PVC하수관" }, + { val: "MOP820", text: "녹석(돌산호과 일종)" }, + { val: "MOP821", text: "동관(구리관, 용접)" }, + { val: "MOP822", text: "둥근잔돌" }, + { val: "MOP823", text: "무근콘크리트" }, + { val: "MOP824", text: "백관" }, + { val: "MOP825", text: "복합철판파형관" }, + { val: "MOP826", text: "분체라이닝관" }, + { val: "MOP827", text: "석축박스" }, + { val: "MOP828", text: "석축철콘" }, + { val: "MOP829", text: "수도용경질염화비관" }, + { val: "MOP830", text: "수도용경질염화비닐이음관" }, + { val: "MOP831", text: "수도용엑포시수지분체내외면코팅강관" }, + { val: "MOP832", text: "수지파형강관(PCS)" }, + { val: "MOP833", text: "수지CSP" }, + { val: "MOP834", text: "스테인레스 주름관" }, + { val: "MOP835", text: "스테인레스관(STS)" }, + { val: "MOP836", text: "스테인레스스틸파이프관" }, + { val: "MOP837", text: "아스콘" }, + { val: "MOP838", text: "아스팔트" }, + { val: "MOP839", text: "아스팔트도복장강관(ASP)" }, + { val: "MOP840", text: "아연" }, + { val: "MOP841", text: "아연도금(백관)" }, + { val: "MOP842", text: "알루미늄" }, + { val: "MOP843", text: "알루미늄합금" }, + { val: "MOP844", text: "압력배관용 탄소강관" }, + { val: "MOP845", text: "양철관" }, + { val: "MOP846", text: "양철주름관(CSP)" }, + { val: "MOP847", text: "연합인슈강판" }, + { val: "MOP848", text: "와사관(GSP)" }, + { val: "MOP849", text: "원심력철근콘크리트" }, + { val: "MOP850", text: "유리섬유관(GRP)" }, + { val: "MOP851", text: "유리섬유복합관(GRP)" }, + { val: "MOP852", text: "이음매없는동및동합금관" }, + { val: "MOP853", text: "일반배관용스테인리스강관" }, + { val: "MOP854", text: "전압 콘크리트관" }, + { val: "MOP855", text: "주름관" }, + { val: "MOP856", text: "주름주철파형강관" }, + { val: "MOP857", text: "지중 저압케이블 입상관" }, + { val: "MOP858", text: "진동및전압철근콘크리트관" }, + { val: "MOP859", text: "철근콘크리트" }, + { val: "MOP860", text: "철근콘크리트 블록" }, + { val: "MOP861", text: "철근콘크리트조립흙막이" }, + { val: "MOP862", text: "철판매입관" }, + { val: "MOP863", text: "청동" }, + { val: "MOP864", text: "코아식프리스트레스트콘크리트관" }, + { val: "MOP865", text: "콘크리트" }, + { val: "MOP866", text: "콘크리트 폴륨관" }, + { val: "MOP867", text: "콘크리트블록" }, + { val: "MOP868", text: "탄소강" }, + { val: "MOP869", text: "탄소강(카본스틸)" }, + { val: "MOP870", text: "탄소주강" }, + { val: "MOP871", text: "트러후" }, + { val: "MOP872", text: "파선형강관(RCF)" }, + { val: "MOP873", text: "파형강관(CSP)" }, + { val: "MOP874", text: "파형관" }, + { val: "MOP875", text: "파형플라스틱관" }, + { val: "MOP876", text: "폴리머관" }, + { val: "MOP877", text: "폴리에스테르수지콘크리트관" }, + { val: "MOP878", text: "폴리에틸렌 전선관" }, + { val: "MOP879", text: "폴리에틸렌 피복강관" }, + { val: "MOP880", text: "폴리에틸렌관 1호관" }, + { val: "MOP881", text: "폴리에틸렌관 2호관" }, + { val: "MOP882", text: "폴리에틸렌관(2중벽)" }, + { val: "MOP883", text: "폴리에틸렌관(3중벽)" }, + { val: "MOP884", text: "폴리에틸렌분체라이닝" }, + { val: "MOP885", text: "폴리에틸렌이중벽하수관" }, + { val: "MOP886", text: "폴리탄산에스테르" }, + { val: "MOP887", text: "프리스트레스트콘크리트실린더관" }, + { val: "MOP888", text: "플라스틱" }, + { val: "MOP889", text: "플라스틱주름관" }, + { val: "MOP890", text: "하수도용고강성경질염화비닐이중벽주름관" }, + { val: "MOP891", text: "현장타설관" }, + { val: "MOP892", text: "현장타설철근콘크리트관" }, + { val: "MOP893", text: "흑관" }, + { val: "MOP894", text: "흙" }, + { val: "MOP895", text: "AP" }, + { val: "MOP896", text: "API관" }, + { val: "MOP897", text: "ASP" }, + { val: "MOP898", text: "CMCIP" }, + { val: "MOP899", text: "CSP" }, + { val: "MOP900", text: "DRP" }, + { val: "MOP901", text: "DSP" }, + { val: "MOP902", text: "DTC관" }, + { val: "MOP903", text: "ELP" }, + { val: "MOP904", text: "EPP" }, + { val: "MOP905", text: "FC관" }, + { val: "MOP906", text: "H" }, + { val: "MOP907", text: "H6" }, + { val: "MOP908", text: "HCP" }, + { val: "MOP909", text: "HI_2P관" }, + { val: "MOP910", text: "HI-KP" }, + { val: "MOP911", text: "HL" }, + { val: "MOP912", text: "IP" }, + { val: "MOP913", text: "KCGP" }, + { val: "MOP914", text: "KP관" }, + { val: "MOP915", text: "P2S관" }, + { val: "MOP916", text: "PC박스(PCB)" }, + { val: "MOP917", text: "PCS" }, + { val: "MOP918", text: "PDP" }, + { val: "MOP919", text: "PE이중벽관" }, + { val: "MOP920", text: "PE적측벽1종관" }, + { val: "MOP921", text: "PFP관" }, + { val: "MOP922", text: "PG관" }, + { val: "MOP923", text: "PIP관(이중보온관)" }, + { val: "MOP924", text: "PLCSP" }, + { val: "MOP925", text: "PS" }, + { val: "MOP926", text: "PUB" }, + { val: "MOP927", text: "PVB" }, + { val: "MOP928", text: "PVC이중벽관(PVC2)" }, + { val: "MOP929", text: "PVC-DC" }, + { val: "MOP930", text: "PVC-DV" }, + { val: "MOP931", text: "SBK" }, + { val: "MOP932", text: "SCP" }, + { val: "MOP933", text: "SEB" }, + { val: "MOP934", text: "SIS관(이중보온관)" }, + { val: "MOP935", text: "SMC" }, + { val: "MOP936", text: "STW" }, + { val: "MOP937", text: "SUS" }, + { val: "MOP938", text: "THP나선관" }, + { val: "MOP939", text: "U관" }, + { val: "MOP940", text: "VG2" }, + { val: "MOP941", text: "WSP(와사피)" }, + { val: "MOP942", text: "Steel Grating" }, + { val: "MOP943", text: "모래" }, + { val: "MOP944", text: "인조석" }, + { val: "MOP945", text: "SPOL" }, + { val: "MOP946", text: "PCF관(강판보강 PE 나선형관)" }, + { val: "MOP947", text: "PVC3(PVC3중벽관)" }, + { val: "MOP948", text: "PE다중벽관" }, + { val: "MOP977", text: "전주용입상관" }, + { val: "MOP978", text: "MD관" }, +]; +// 하수관로 'fcltyShpCd' '시설물형태코드' +export const MS_SWAGE_PPL_SHAPE = [ + { val: "FOR000 ", text: "미분류" }, + { val: "FOR001 ", text: "원형" }, + { val: "FOR002 ", text: "원추형" }, + { val: "FOR003 ", text: "원통형" }, + { val: "FOR004 ", text: "구형" }, + { val: "FOR005 ", text: "타원형" }, + { val: "FOR006 ", text: "난원" }, + { val: "FOR007 ", text: "반원형" }, + + { val: "FOR010 ", text: "피라미드형" }, + { val: "FOR011 ", text: "사면체형" }, + { val: "FOR012 ", text: "기둥,축" }, + { val: "FOR013 ", text: "돛대,활대처럼 둥글고 긴 원재" }, + { val: "FOR014 ", text: "아치형" }, + { val: "FOR015 ", text: "다중아치형" }, + { val: "FOR016 ", text: "돔형" }, + { val: "FOR017 ", text: "고형체형" }, + + { val: "FOR020 ", text: "사각형" }, + { val: "FOR021 ", text: "평방형" }, + { val: "FOR022 ", text: "다이아몬드형" }, + { val: "FOR023 ", text: "사다리꼴" }, + { val: "FOR024 ", text: "각형" }, + { val: "FOR025 ", text: "마제형관" }, + { val: "FOR026 ", text: "태형" }, + + { val: "FOR030 ", text: "A형" }, + { val: "FOR031 ", text: "H형" }, + { val: "FOR032 ", text: "I형" }, + { val: "FOR033 ", text: "Y형" }, + { val: "FOR034 ", text: "T형" }, + + { val: "FOR050 ", text: "불규칙한 형태" }, + + { val: "FOR900 ", text: "계란형" }, + { val: "FOR901 ", text: "구(사각)형" }, + { val: "FOR902 ", text: "높이조절철개형" }, + { val: "FOR903 ", text: "박스형(RCB)" }, + { val: "FOR904 ", text: "반월개거" }, + { val: "FOR905 ", text: "반월개거(측구)" }, + { val: "FOR906 ", text: "벽돌조" }, + { val: "FOR907 ", text: "사각철판" }, + { val: "FOR908 ", text: "석축개거" }, + { val: "FOR909 ", text: "석축개거(측구)" }, + { val: "FOR910 ", text: "승수로" }, + { val: "FOR911 ", text: "옹벽개거" }, + { val: "FOR912 ", text: "옹벽개거(측구)" }, + { val: "FOR913 ", text: "이단" }, + { val: "FOR914 ", text: "이중침전형" }, + { val: "FOR915 ", text: "자연개거" }, + { val: "FOR916 ", text: "자연개거(측구)" }, + { val: "FOR917 ", text: "조립식" }, + { val: "FOR918 ", text: "지상관" }, + { val: "FOR919 ", text: "지상단구" }, + { val: "FOR920 ", text: "지상쌍구" }, + { val: "FOR921 ", text: "지하단구" }, + { val: "FOR922 ", text: "지하쌍구" }, + { val: "FOR923 ", text: "직사각형" }, + { val: "FOR924 ", text: "타설식" }, + { val: "FOR925 ", text: "토류형" }, + { val: "FOR926 ", text: "흄관원형" }, + { val: "FOR927 ", text: "L형측구" }, + { val: "FOR928 ", text: "LO형측구" }, + { val: "FOR929 ", text: "LU형측구" }, + { val: "FOR930 ", text: "PVC이중벽관" }, + { val: "FOR931 ", text: "RCB 맨홀" }, + { val: "FOR932 ", text: "U형" }, + { val: "FOR933 ", text: "U형측구(미복개)" }, + { val: "FOR934 ", text: "U형측구(복개)" }, + + { val: "FOR999 ", text: "기타" } +]; +// 하수처리시설 +export const SwtrmtFcltCol = [ + { id: 'swtrmt_fclt_nm', title: '하수처리시설명' }, + { id: 'fclt_cpct', title: '시설용량(㎥/d)' }, + { id: 'rgn_expln', title: '지역설명' }, + { id: 'cmcn_ymd', title: '준공일자' }, + { id: 'oprtng_strt_ymd', title: '가동개시일자' }, + { id: 'clsg_ymd', title: '폐쇄일자' }, + // { id: 'prcs_msrc_cd', title: '처리공법코드' }, + // { id: 'lae_cd', title: '지자체코드' }, + { id: 'lae_nm', title: '지자체명' }, + { id: 'ctpv_nm', title: '시도명' }, + { id: 'sgg_nm', title: '시군구명' }, + // { id: 'sarea_cd', title: '권역코드' }, + { id: 'sarea_nm', title: '권역명' }, + { id: 'lrge_sarea_nm', title: '대권역명' }, + { id: 'rvr_nm', title: '하천명' }, + { id: 'addr', title: '주소' }, + { id: 'dtl_addr', title: '상세주소' }, + { id: 'rdnm_addr', title: '도로명주소' }, + { id: 'dtl_rdnm_addr', title: '상세도로명주소' }, + { id: 'dschg_wtst_nm', title: '방류수계명' }, + { id: 'plln_area', title: '부지면적(m²)' }, + { id: 'mng_rprtv_bzenty_nm', title: '관리대행업제명' }, + { id: 'swtrmt_ppltn_cnt', title: '하수처리인구수(인)' }, + { id: 'prcs_zone_area_sum', title: '처리구역면적합계(m²)' }, + { id: 'xcrd', title: 'X좌표' }, + { id: 'ycrd', title: 'Y좌표' }, + // { id: 'oprtngSttsCd', title: '가동상태코드' }, + { id: 'rprs_msrc_cn', title: '대표공법내용' }, + // { id: 'stty_crtr_bod', title: '법정기준생물화학적산소요구량(mg/L)' }, + // { id: 'stty_crtr_toc', title: '법정기준TOC((mg/L))' }, + // { id: 'stty_crtr_ss', title: '법정기준SS(mg/L)' }, + // { id: 'stty_crtr_tn', title: '법정기준총질소(mg/L)' }, + // { id: 'stty_crtr_tph', title: '법정기준총인(mg/L)' }, + // { id: 'stty_crtr_clbcl_cnt', title: '법정기준대장균수(군수/100mL)' }, + { id: 'bsn_nm', title: '유역명' }, + // { id: 'obsrvnSptCd', title: '관측지점코드' }, +]; +export const SwtrmtFcltCol2 = [ + { id: 'rowNumber', title: '일련\n번호' }, + { id: 'swtrmtFcltNm', title: '하수처리\n시설명' }, + { id: 'fcltCpct', title: '시설\n용량\n(㎥/d)' }, + { id: 'rgnExpln', title: '지역설명' }, + // { id: 'cmcnYmd', title: '준공일자' }, + { id: 'oprtngStrtYmd', title: '가동개시일자' }, + // { id: 'clsgYmd', title: '폐쇄일자' }, + // { id: 'prcsMsrcCd', title: '처리공법코드' }, + // { id: 'laeCd', title: '지자체코드' }, + { id: 'laeNm', title: '지자체명' }, + // { id: 'ctpvNm', title: '시도명' }, + // { id: 'sggNm', title: '시군구명' }, + // { id: 'sareaCd', title: '권역코드' }, + { id: 'sareaNm', title: '권역명' }, + // { id: 'lrgeSareaNm', title: '대권역명' }, + { id: 'rvrNm', title: '하천명' }, + // { id: 'addr', title: '주소' }, + // { id: 'dtlAddr', title: '상세주소' }, + // { id: 'rdnmAddr', title: '도로명주소' }, + // { id: 'dtlRdnmAddr', title: '상세도로명주소' }, + { id: 'dschgWtstNm', title: '방류수계명' }, + // { id: 'pllnArea', title: '부지면적(m²)' }, + // { id: 'mngRprtvBzentyNm', title: '관리대행업체명' }, + { id: 'swtrmtPpltnCnt', title: '하수\n처리\n인구수\n(인)' }, + // { id: 'prcsZoneAreaSum', title: '처리구역면적합계(m²)' }, + // { id: 'xcrd', title: 'X좌표' }, + // { id: 'ycrd', title: 'Y좌표' }, + // { id: 'oprtngSttsCd', title: '가동상태코드' }, + { id: 'rprsMsrcCn', title: '대표\n공법\n내용' }, + // { id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량(mg/L)' }, + // { id: 'sttyCrtrToc', title: '법정기준TOC(mg/L)' }, + // { id: 'sttyCrtrSs', title: '법정기준SS(mg/L)' }, + // { id: 'sttyCrtrTn', title: '법정기준총질소(mg/L)' }, + // { id: 'sttyCrtrTph', title: '법정기준총인(mg/L)' }, + // { id: 'sttyCrtrClbclCnt', title: '법정기준대장균수(군수/100mL)' }, + // { id: 'bsnNm', title: '유역명' }, + // { id: 'obsrvnSptCd', title: '관측지점코드' }, +]; +export const SwtrmtFcltExcelCol = { + 'swtrmtFcltNm': '하수처리시설명', + 'fcltCpct': '시설 용량(㎥/d)', + 'rgnExpln': '지역설명', + 'oprtngStrtYmd': '가동개시 일자', + 'laeNm': '지자체명', + 'sareaNm': '권역명', + 'rvrNm': '하천명', + 'dschgWtstNm': '방류수계명', + 'swtrmtPpltnCnt': '하수 처리 인구수(인)', + 'rprsMsrcCn': '대표 공법 내용' +}; + +// 하수처리구역 +export const SwtrmtPrcsZoneCol = [ + // { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' }, + { id: 'swtrmt_zone_nm', title: '하수처리구역명' }, + // { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' }, + // { id: 'ctpv_cd', title: '시도코드' }, + { id: 'ctpv_nm', title: '시도명' }, + // { id: 'sgg_cd', title: '시군구코드' }, + { id: 'sgg_nm', title: '시군구명' }, + { id: 'swtrmt_fclt_nm', title: '하수처리시설명' }, +]; +export const SwtrmtPrcsZoneCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' }, + { id: 'swtrmtZoneNm', title: '하수처리구역명' }, + // { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' }, + // { id: 'ctpv_cd', title: '시도코드' }, + { id: 'ctpvNm', title: '시도' }, + // { id: 'sgg_cd', title: '시군구코드' }, + { id: 'sggNm', title: '시군구' }, + { id: 'swtrmtFcltNm', title: '하수처리시설명' }, +]; +export const SwtrmtPrcsZoneExcelCol = { + 'swtrmtZoneNm': '하수처리구역명', + 'ctpvNm': '시도', + 'sggNm': '시군구', + 'swtrmtFcltNm': '하수처리시설명' +}; + +// 하수맨홀 +export const MsSwageMhlCol = [ + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftre_smbol_cd', title: '지형지물부호코드' }, + { id: 'fclt_mng_no', title: '시설관리번호' }, + // { id: 'pbadms_emd_cd', title: '행정읍면동코드' }, + { id: 'mapsht_no', title: '도엽번호' }, + // { id: 'mng_inst_cd', title: '관리기관코드' }, + { id: 'instl_ymd', title: '설치일자' }, + { id: 'last_drdgn_ymd', title: '최종준설일자' }, + // { id: 'usg_cd', title: '용도코드' }, + // { id: 'fclty_shp_cd', title: '시설물형태코드' }, + // { id: 'mhl_knd_cd', title: '맨홀종류코드' }, + { id: 'swage_mhL_diam_vl', title: '하수맨홀구경(mm)' }, + { id: 'mqlt_cd', title: '재질' }, + { id: 'wdth_len', title: '가로길이(m)' }, + { id: 'vrtc_len', title: '세로길이(m)' }, + // { id: 'ang_dir', title: '표고' }, + { id: 'altd_vl', title: '지표고(EL.m)' }, + { id: 'dpthl_vl', title: '맨홀저고(EL.m)' }, + // { id: 'inst_mng_no', title: '기관관리번호' } +]; +export const MsSwageMhlCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fcltMngNo', title: '시설관리번호' }, + { id: 'ctpvNm', title: '시도' }, + { id: 'sggNm', title: '시군구' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + // { id: 'mapshtNo', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instlYmd', title: '설치일자' }, + // { id: 'lastDrdgnYmd', title: '최종준설일자' }, + { id: 'usgCd', title: '용도' }, + // { id: 'fcltyShpCd', title: '시설물형태코드' }, + // { id: 'mhlKndCd', title: '맨홀종류코드' }, + { id: 'swageMhlDiamVl', title: '하수맨홀구경(mm)' }, + // { id: 'mqltCd', title: '재질코드' }, + { id: 'wdthLen', title: '가로길이(m)' }, + { id: 'vrtcLen', title: '세로길이(m)' }, + // { id: 'angDir', title: '표고' }, + { id: 'altdVl', title: '지표고(EL.m)' }, + { id: 'dpthlVl', title: '맨홀저고(EL.m)' }, + // { id: 'instMngNo', title: '기관관리번호' } +]; +export const MsSwageMhlExcelCol = { + 'fcltMngNo': '시설관리번호', + 'ctpvNm': '시도', + 'sggNm': '시군구', + 'instlYmd': '설치일자', + 'usgCd': '용도', + 'swageMhlDiamVl': '하수맨홀구경(mm)', + 'wdthLen': '가로길이(m)', + 'vrtcLen': '세로길이(m)', + 'altdVl': '지표고(EL.m)', + 'dpthlVl': '맨홀저고(EL.m)' +}; +// 하수맨홀 뚜껑재질 코드표 +export const MS_SWAGE_MHL_CODES = [ + { val: "SBC000", text: "미분류" }, + { val: "SBC001", text: "주철" }, + { val: "SBC002", text: "철판" }, + { val: "SBC003", text: "콘크리트" }, + { val: "SBC004", text: "그레이팅" }, + { val: "SBC091", text: "칼라" }, + { val: "SBC092", text: "S.G" }, + { val: "SBC999", text: "기타" }, + { val: "SBC900", text: "각주철" }, + { val: "SBC901", text: "강재" }, + { val: "SBC902", text: "나무" }, + { val: "SBC903", text: "나무,널판지,합판" }, + { val: "SBC904", text: "대리석" }, + { val: "SBC905", text: "뚜껑없음" }, + { val: "SBC906", text: "보도용콘크리트" }, + { val: "SBC907", text: "스틸(ST)" }, + { val: "SBC908", text: "신형 CON" }, + { val: "SBC909", text: "알루미늄" }, + { val: "SBC910", text: "염화비닐(PE)" }, + { val: "SBC911", text: "인조석" }, + { val: "SBC912", text: "인조석(주철)" }, + { val: "SBC913", text: "인조CONC" }, + { val: "SBC914", text: "철, 철근" }, + { val: "SBC915", text: "칼라콘크리트" }, + { val: "SBC916", text: "플라스틱" }, + { val: "SBC917", text: "PVC" }, + { val: "SBC918", text: "SP" }, + { val: "SBC919", text: "SS" }, + { val: "SBC920", text: "닥타일주철" }, + { val: "SBC921", text: "철근G" }, + { val: "SBC922", text: "폴리에틸렌(PE)" }, + { val: "SBC923", text: "회주철" } +]; +// 하수맨홀 용도 코드표 +export const MS_SWAGE_MHL_USE_CODES = [ + { val: "SMU000 ", text: "미분류" }, + { val: "SMU001 ", text: "우수" }, + { val: "SMU002 ", text: "오수" }, + { val: "SMU003 ", text: "집수" }, + { val: "SMU004 ", text: "합류" }, + { val: "SMU005 ", text: "차집" }, + { val: "SMU999 ", text: "기타" }, + { val: "SMU900 ", text: "1호" }, + { val: "SMU901 ", text: "2호" }, + { val: "SMU902 ", text: "3호" }, + { val: "SMU903 ", text: "4호" }, + { val: "SMU904 ", text: "5호" }, + { val: "SMU905 ", text: "6호" }, + { val: "SMU906 ", text: "7호" }, + { val: "SMU907 ", text: "8호" }, + { val: "SMU908 ", text: "구거용맨홀" }, + { val: "SMU909 ", text: "부관부착맨홀" }, + { val: "SMU910 ", text: "조립식맨홀" }, + { val: "SMU911 ", text: "특1호" }, + { val: "SMU912 ", text: "특2호" }, + { val: "SMU913 ", text: "특3호" }, + { val: "SMU914 ", text: "특4호" }, + { val: "SMU915 ", text: "특5호" }, + { val: "SMU916 ", text: "하수맨홀용도" }, + { val: "SMU917 ", text: "현장타설관거용" } +]; + +// 우수펌프장 +export const ExclnSewagePumpCol = [ + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fclt_mng_no', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + { id: 'mapsht_no', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instl_ymd', title: '설치일자' }, + { id: 'swage_pmstn_nm', title: '하수펌프장명' }, + { id: 'plln_area', title: '부지면적(m²)' }, + // { id: 'opngSttsCd', title: '개통상태코드' }, + // { id: 'usgCd', title: '용도코드' }, + { id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' }, + { id: 'max_impnd_cpct', title: '최대저수용량(㎥)' }, + { id: 'altd_vl', title: '표고(EL.m)' }, + { id: 'wtlv_vl', title: '수위(m)' }, + { id: 'excln_wpmp_ablt_vl', title: '우수양수능력(m²/d)' }, + // { id: 'instMngNo', title: '기관관리번호' }, +] +export const ExclnSewagePumpCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fcltMngNo', title: '시설관리번호' }, + { id: 'ctpvNm', title: '시도' }, + { id: 'sggNm', title: '시군구' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + // { id: 'mapshtNo', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'swagePmstnNm', title: '하수펌프장명' }, + { id: 'instlYmd', title: '설치일자' }, + // { id: 'pllnArea', title: '부지면적(m²)' }, + // { id: 'opngSttsCd', title: '개통상태코드' }, + // { id: 'usgCd', title: '용도코드' }, + { id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' }, + { id: 'maxImpndCpct', title: '최대저수용량(㎥)' }, + { id: 'altdVl', title: '표고(EL.m)' }, + { id: 'wtlvVl', title: '수위(m)' }, + { id: 'exclnWpmpAbltVl', title: '우수양수능력(m²/d)' }, + // { id: 'instMngNo', title: '기관관리번호' }, +] +export const ExclnSewagePumpExcelCol = { + 'fcltMngNo': '시설관리번호', + 'ctpvNm': '시도', + 'sggNm': '시군구', + 'swagePmstnNm': '하수펌프장명', + 'instlYmd': '설치일자', + 'dailyPrcsCpct': '일일처리용량(㎥)', + 'maxImpndCpct': '최대저수용량(㎥)', + 'altdVl': '표고(EL.m)', + 'wtlvVl': '수위(m)', + 'exclnWpmpAbltVl': '우수양수능력(m²/d)' +}; +// 오수펌프장 +export const DrtwSewagePumpCol = [ + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fclt_mng_no', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + { id: 'mapsht_no', title: '도엽번호' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instl_ymd', title: '설치일자' }, + { id: 'swage_pmstn_nm', title: '하수펌프장명' }, + { id: 'plln_area', title: '부지면적(m²)' }, + // { id: 'opngSttsCd', title: '개통상태코드' }, + // { id: 'usgCd', title: '용도코드' }, + { id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' }, + { id: 'max_impnd_cpct', title: '최대저수용량(㎥)' }, + { id: 'altd_vl', title: '표고(EL.m)' }, + { id: 'wtlv_vl', title: '수위(m)' }, + { id: 'excln_wpmp_ablt_vl', title: '오수양수능력(m²/d)' }, + // { id: 'instMngNo', title: '기관관리번호' }, +] +export const DrtwSewagePumpCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'fid', title: 'FID' }, + // { id: 'geom', title: 'geometry' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fcltMngNo', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + // { id: 'mapshtNo', title: '도엽번호' }, + { id: 'ctpvNm', title: '시도' }, + { id: 'sggNm', title: '시군구' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'swagePmstnNm', title: '하수펌프장명' }, + { id: 'instlYmd', title: '설치일자' }, + // { id: 'pllnArea', title: '부지면적(m²)' }, + // { id: 'opngSttsCd', title: '개통상태코드' }, + // { id: 'usgCd', title: '용도코드' }, + { id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' }, + { id: 'maxImpndCpct', title: '최대저수용량(㎥)' }, + { id: 'altdVl', title: '표고(EL.m)' }, + { id: 'wtlvVl', title: '수위(m)' }, + { id: 'exclnWpmpAbltVl', title: '오수양수능력(m²/d)' }, + // { id: 'instMngNo', title: '기관관리번호' }, +] +// 펌프장 용도 코드표 +export const DRTW_SEWAGE_PUMP_USE_CODES = [ + { val: "SBE000", text: "미분류" }, + { val: "SBE001", text: "빗물펌프장" }, + { val: "SBE002", text: "오수펌프장" }, + { val: "SBE003", text: "처리장내펌프장" }, + { val: "SBE900", text: "오수배제" }, + { val: "SBE901", text: "우수배제" }, + { val: "SBE999", text: "기타" }, +]; + +// 우수토실 +export const stofcbrCol = [ + // { id: 'ftre_smbol_cd', title: '지형지물부호코드' }, + { id: 'fclt_mng_no', title: '시설관리번호' }, + // { id: 'pbadms_emd_cd', title: '행정읍면동코드' }, + { id: 'mapsht_no', title: '도엽번호' }, + // { id: 'mng_inst_cd', title: '관리기관코드' }, + { id: 'instl_ymd', title: '설치일자' }, + // { id: 'fclty_shp_cd', title: '시설물형태코드' }, + { id: 'worflw_wall_hgt', title: '월류벽높이(m)' }, + // { id: 'mhl_exst_yn_cd', title: '맨홀존재여부코드' }, + // { id: 'swage_mhl_ftre_smbol_cd', title: '하수맨홀지형지물부호코드' }, + { id: 'swage_mhl_mng_no', title: '하수맨홀관리번호' }, + { id: 'drtn_angl', title: '방향각도(°)' }, +] +export const stofcbrCol2 = [ + { id: 'rowNumber', title: '일련번호' }, + // { id: 'ftreSmbolCd', title: '지형지물부호코드' }, + { id: 'fcltMngNo', title: '시설관리번호' }, + // { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + { id: 'mapshtNo', title: '도엽번호' }, + { id: 'ctpvNm', title: '시도' }, + { id: 'sggNm', title: '시군구' }, + // { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instlYmd', title: '설치일자' }, + // { id: 'fcltyShpCd', title: '시설물형태코드' }, + { id: 'worflwWallHgt', title: '월류벽높이(m)' }, + // { id: 'mhlExstYnCd', title: '맨홀존재여부코드' }, + // { id: 'swageMhlFtreSmbolCd', title: '하수맨홀지형지물부호코드' }, + { id: 'swageMhlMngNo', title: '하수맨홀관리번호' }, + { id: 'drtnAngl', title: '방향각도(°)' }, +] +export const stofcbrExcelCol = { + 'fcltMngNo': '시설관리번호', + 'mapshtNo': '도엽번호', + 'ctpvNm': '시도', + 'sggNm': '시군구', + 'instlYmd': '설치일자', + 'worflwWallHgt': '월류벽높이(m)', + 'swageMhlMngNo': '하수맨홀관리번호', + 'drtnAngl': '방향각도(°)' +}; + +// 중점우수토실 측정지점 +export const untrtSwgRainCol1 = [ + { id: 'manage_no', title: '관리번호' }, + { id: 'swr_ppl_eqp_nm', title: '처리구역' }, + { id: 'swr_ppl_trt_nm', title: '처리분구' }, + { id: 'addr', title: '인근주소' }, + { id: 'chmbr_xcnts', title: '토실 위도' }, + { id: 'chmbr_ydnts', title: '토실 경도' } +]; + +// 중점우수토실 측정지점 시설현황 +export const untrtSwgRainCol2 = [ + { id: 'mesure_spot_se', title: '구분' }, // 유량계측정방식 기타 + { id: 'discharge_pipe_spec', title: '방류관로 규격(m)' }, + { id: 'swr_ppl_eqp_ar', title: '배수구역 면적(m²)' }, + { id: 'swr_trt_popl', title: '하수처리 인구(인)' }, + { id: 'screen_instl_at', title: '스크린설치 여부' }, + { id: 'weir_hg', title: '위어 높이(m)' }, + { id: 'weir_lt', title: '위어 길이(m)' } +]; + +// 중점우수토실 강우정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol3 = [ + { id: 'rainBeginDt', title: '강우시작일시' }, + { id: 'rainEndDt', title: '강우종료일시' }, + { id: 'precdngDry', title: '선행건기(h)' }, + { id: 'rainInfo', title: '강우정보' }, + { id: 'rainFall', title: '누적강우량(mm)' }, + { id: 'cntncTime', title: '지속시간(h)' }, + { id: 'rainIn', title: '강우강도(mm/h)' } +]; + +// 중점우수토실 측정정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol4 = [ + { id: 'mesureBeginDt', title: '측정시작일시' }, + { id: 'mesureEndDt', title: '측정종료일시' }, + { id: 'mesureIntrvl', title: '측정간격(분)' }, + { id: 'mesureMhrlsInstlLc', title: '측정기기 설치위치' }, + { id: 'flwmtrCombined', title: '유량계 종류 및 모델명' }, // 합쳐진 컬럼 + { id: 'qltwtrBeginDt', title: '채수시작일시' }, + { id: 'qltwtrEndDt', title: '채수종료일시' }, + { id: 'qltwtrCo', title: '채수횟수' } +]; + +// 중점우수토실 수량/수질측정 정보 +export const untrtSwgRainCol5 = [ + { id: 'odr', title: '회차' }, + { id: 'mesureDt', title: '측정일자' }, + { id: 'mesureQy', title: '수량(m³)' }, + { id: 'mesureBod', title: 'BOD(mg/L)' }, + { id: 'mesureToc', title: 'TOC(mg/L)' }, + { id: 'mesureLoad', title: '오염부하량(kg)' } +]; + +// 🎯 날짜 필드들 (yyyymmdd -> yyyy년 mm월 dd일) +export const dateFields = [ + 'instl_ymd', 'last_drdgn_ymd', 'cmcn_ymd', 'oprtng_strt_ymd', 'clsg_ymd', + 'mesure_begin_dt', 'mesure_end_dt', 'qltwtr_begin_dt', 'qltwtr_end_dt', + 'rain_begin_dt', 'rain_end_dt', 'mesureDt', + // 🎯 camelCase 버전 추가 + 'instlYmd', 'lastDrdgnYmd', 'cmcnYmd', 'oprtngStrtYmd', 'clsgYmd', + 'mesureBeginDt', 'mesureEndDt', 'qltwtrBeginDt', 'qltwtrEndDt', + 'rainBeginDt', 'rainEndDt', + 'flfmtBgngYmd', 'flfmtEndYmd', 'flfmtYr' +]; + +// 🎯 연월 필드들 (yyyymm -> yyyy년 m월) +export const yearMonthFields = ['aplcnYm',]; + +// 🎯 날짜시간 필드들 (yyyy-mm-dd hh:mm:ss -> yyyy년 m월 d일 hh:mm) +export const dateTimeFields = ['regDt',]; + +// 🎯 숫자 필드들 (천 단위 구분자) +export const numberFields = [ + 'pdmt', 'prlg', 'avg_grdn', 'drtw_mltv_area', 'excln_mltv_area', 'tmpt_dpth', 'scend_dpth', 'trfln_cnt', 'vrtc_len', 'wdth_len', + 'fclt_cpct', 'plln_area', 'prcs_zone_area_sum', 'swtrmt_ppltn_cnt', 'stty_crtr_bod', 'stty_crtr_toc', 'stty_crtr_ss', 'stty_crtr_tn', 'stty_crtr_tph', 'stty_crtr_clbcl_cnt', + 'swage_mh_diam', 'wdth_len', 'vrtc_len', 'altd_vl', 'dpthlVl', + 'plln_area', 'daily_prcs_cpct', 'max_impnd_cpct', 'altd_vl', 'wtlv_vl', 'excln_wpmp_ablt', + 'worflw_wall_hgt', + 'swr_ppl_eqp_ar', 'swr_trt_popl', 'weir_hg', 'weir_lt', + 'precdng_dry', 'rain_fall', 'cntnc_time', 'rain_in', + 'mesure_intrvl', 'qltwtr_co', 'mesureQy', 'mesureBod', 'mesureToc', 'mesureLoad', + // 🎯 camelCase 버전 추가 + 'avgGrdn', 'drtwMltvArea', 'exclnMltvArea', 'tmptDpth', 'scendDpth', 'trflnCnt', 'vrtcLen', 'wdthLen', + 'fcltCpct', 'pllnArea', 'prcsZoneAreaSum', 'swtrmtPpltnCnt', 'sttyCltrBod', 'sttyCltrToc', 'sttyCltrSs', 'sttyCltrTn', 'sttyCltrTph', 'sttyCltrClbclCnt', + 'swageMhDiam', 'altdVl', 'dpthlVl', + 'dailyPrcsCpct', 'maxImpndCpct', 'wtlvVl', 'exclnWpmpAblt', + 'worflwWallHgt', + 'swrPplEqpAr', 'swrTrtPopl', 'weirHg', 'weirLt', + 'precdngDry', 'rainFall', 'cntncTime', 'rainIn', + 'mesureIntrvl', 'qltwtrCo', 'rnysnFvlc', 'clarFvlc', + 'bizAmt', 'count', + 'chmbr_xcnts', 'chmbr_ydnts' +]; diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx new file mode 100644 index 000000000..0e2ec3780 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx @@ -0,0 +1,163 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (하수관로) + * + * 📄 MsSwagePpl + * 하수관로 정보 컴포넌트 + * + * - 하수관로의 자체입력정보 표시 및 편집 기능 + * - 보수 및 갱신, 청소 및 준설, 점검 및 진단 데이터 조회 및 표시 + * - VerticalTable을 통한 입력 폼 렌더링 + * - InputSection을 통한 운영 데이터 관리 + * - 저장 버튼을 통한 데이터 저장 기능 + * + * 🔗 사용 API + * - 시설별 운영 정보 조회: GET /api/v1/bizInfo/bizByFclt (sewerage-service) + */ +'use client' +import { VerticalTable } from '@/components/ui/verticalTable'; +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'; + +// 하수관로 +const inputColumn = [ + { id: "instlMbdyCd", title: "설치주제", type: "select", options: ["BTL", "재정사업"] }, + { id: "swtrmtZoneNm", title: "처리구역", type: "input" }, + { id: "mltvZoneNm", title: "배수구역", type: "input" }, + { id: "mltvDtdvNm", title: "배수분구", type: "input" }, + { id: "lineCd", title: "지선/간선", type: "select", options: ["지선", "간선"] }, + { id: "floCd", title: "흐름방식", type: "select", options: ["자연유하", "압송"] }, + { id: "layCd", title: "매설위치", type: "select", options: ["도로", "하상"] }, +]; + +type operationDataType = { year: string; type: string, bizInfoNm: string, bizType: string }; + +const MsSwagePpl = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () => void }) => { + const [formData, setFormData] = useState({}); + const [repairData, setRepairData] = useState([]); + const [cleaningData, setCleaningData] = useState([]); + const [inspectionData, setInspectionData] = useState([]); + const dataMap = { + repair: { data: repairData, setData: setRepairData }, + cleaning: { data: cleaningData, setData: setCleaningData }, + inspection: { data: inspectionData, setData: setInspectionData }, + }; + + const { BIZ_SE_CD, BIZ_TY_CD } = useStore(); + + // 마운트 및 업데이트 시 API 호출 + + useEffect(() => { + if (featureData) { + setFormData(featureData); + } + + const fetchAll = async () => { + const operationData = await getData(`v1/bizInfo/bizByFclt?mngInstCd=${featureData?.mng_inst_cd}&fcltMngNo=${featureData?.fclt_mng_no}&page=0&size=1`); + + let inspectionArr: operationDataType[] = []; + let cleaningArr: operationDataType[] = []; + let repairArr: operationDataType[] = []; + + if (Array.isArray(operationData)) { + operationData.forEach(item => { + if (!item.flfmt_yr) return; + // biz_se_cd에 해당하는 type명 찾기 + const seCodeObj = BIZ_SE_CD.find(code => code.lwpoCd === item.biz_se_cd); + const tyCodeObj = BIZ_TY_CD.find(code => code.lwpoCd === item.biz_ty_cd); + const seTypeName = seCodeObj ? seCodeObj.lwpoCdNm : ""; + const tyTypeName = tyCodeObj ? tyCodeObj.lwpoCdNm : ""; + switch (item.biz_se_cd) { + case "100": + inspectionArr.push({ + year: String(item.flfmt_yr), + type: seTypeName, + bizInfoNm: String(item.biz_info_nm), + bizType: tyTypeName, + }); + break; + case "203": + cleaningArr.push({ + year: String(item.flfmt_yr), + type: seTypeName, + bizInfoNm: String(item.biz_info_nm), + bizType: tyTypeName, + }); + break; + case "200": + case "201": + case "204": + repairArr.push({ + year: String(item.flfmt_yr), + type: seTypeName, + bizInfoNm: String(item.biz_info_nm), + bizType: tyTypeName, + }); + break; + } + }); + } + + setInspectionData(inspectionArr); + setCleaningData(cleaningArr); + setRepairData(repairArr); + }; + fetchAll(); + }, [featureData]); + + // 폼 데이터가 변경될 때 호출될 핸들러 + const handleFormDataChange = (newFormData: any) => { + setFormData(newFormData); + }; + + // 저장 버튼 클릭 시 최신 formData를 부모에게 전달 + const handleSaveClick = () => { + if (onSaveClick) { + // 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달 + onSaveClick(); + } + }; + + + return ( + <> + + handleInputChange(section, index, field, value, dataMap)} + /> + handleInputChange(section, index, field, value, dataMap)} + /> + handleInputChange(section, index, field, value, dataMap)} + /> + + ) +} + +export default MsSwagePpl \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/Ssc.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/Ssc.tsx new file mode 100644 index 000000000..f908fe98b --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/Ssc.tsx @@ -0,0 +1,358 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (우수토실) + * + * 📄 Ssc + * 우수토실 정보 컴포넌트 + * + * - 우수토실의 자체입력정보 표시 및 편집 기능 + * - 중점우수토실 운영정보 모달 표시 (강우/측정 정보, 수량/수질측정 정보) + * - VerticalTable을 통한 입력 폼 렌더링 + * - 저장 버튼을 통한 데이터 저장 기능 + * + * 🔗 사용 API + * - 위경도로 강우시 미처리 하수 목록 조회: GET /api/v1/onsemiro/getSwgRainsByGeomety (sewerage-service) + * - 강우시 월류 하수 관리 모니터링 조회: GET /api/v1/onsemiro/swgRain/{adstDstrCd}/{manageNo}/mntrngs (sewerage-service) + */ +import { UiModal, UiTable } from '@common/business_components/ui'; +import { Button, TableHead, TableRow } from '@common/components/ui'; +import React, { useState, useEffect } from 'react'; +import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table'; +import { VerticalTable } from '@/components/ui/verticalTable'; +import { getData } from "@/components/lib/api"; +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; +import { Label } from '@radix-ui/react-label'; + +const inputColumn = [ + { id: "prrtymngYn", title: "중점여부", type: "select", options: ["우수토실", "중점우수토실"] }, + { id: "swtrmtZoneNm", title: "처리구역", type: "input" }, +]; + +const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: { + featureData: any, + isMainSsc?: boolean, + chmbrXcnts?: string, + chmbrYdnts?: string, + onSaveClick?: () => void +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [isDataLoaded, setIsDataLoaded] = useState(false); + const [formData, setFormData] = useState({}); + const [rainMeasureData, setRainMeasureData] = useState({}); // 강우, 측정 정보 + const [mntrngsData, setMntrngsData] = useState([]); // 수량/수질측정 정보 + const [totalCount, setTotalCount] = useState(0); + const [totalDetailCount, setTotalDetailCount] = useState(0); + const [selectedRowIndex, setSelectedRowIndex] = useState(-1); + const [curPage, setCurPage] = useState(1); + const [curDetailPage, setCurDetailPage] = useState(1); + const [isResetPage, setIsResetPage] = useState(true); + const [isResetDetailPage, setIsResetDetailPage] = useState(true); + const [isResetSelection, setIsResetSelection] = useState(false); + + // 강우, 측정 정보 API 호출 함수 + const fetchRainMeasureData = async (chmbrXcnts: string, chmbrYdnts: string) => { + setIsResetDetailPage(true); + setIsResetSelection(true); + if (!chmbrXcnts || !chmbrYdnts) { + console.warn('추가 데이터 호출 실패: 필수 매개변수 누락', { chmbrXcnts, chmbrYdnts }); + return []; + } + + try { + const queryParams = new URLSearchParams({ + chmbrXcnts: chmbrXcnts, + chmbrYdnts: chmbrYdnts, + page: `${curPage - 1}`, + size: '3', + }); + + let result; + const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, { + method: "GET", + }); + if (res.ok) { + // 응답이 비어있는지 확인 + const contentLength = res.headers.get('content-length'); + if (contentLength === '0') result = null; + + // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 + const text = await res.text(); + if (!text || text.trim() === '') result = null; + + // JSON 파싱 시도 + result = JSON.parse(text); + } + + // helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM" + const formatNoYearNoSec = (val: any) => { + if (!val && val !== 0) return "-"; + const s = String(val); + // 정규식으로 안전하게 매칭 후 변환 + const m = s.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})(?::\d{2})?$/); + if (m) { + const [, , mm, dd, hh, min] = m; + return `${mm}-${dd} ${hh}:${min}`; + } + // 포맷이 예상과 다르면, 길이에 따라 잘라보기 + if (s.length >= 16) return s.substring(5, 16); // "YYYY-" 제거하고 초 전까지 + return s; + }; + + const processedDataArray: any[] = result.content.map((data: any) => { + // 🎯 formatDisplayData로 기본 포맷팅 적용 + const formattedItem = formatDisplayData(data); + const flwmtrInstTp = data.flwmtrInstTp === 'R' ? '이동식' : data.flwmtrInstTp === 'S' ? '고정식' : '-'; + const flwmtrMesureTp = data.flwmtrMesureTp === 'R' ? '레이더' : data.flwmtrMesureTp === 'P' ? '플륨' + : data.flwmtrMesureTp === 'U' ? '초음파' : data.flwmtrMesureTp === 'E' ? '기타' : '-'; + const flwmtrCombined = `${flwmtrInstTp}, ${flwmtrMesureTp}`; + // 🎯 추가 데이터 조합 + return { + ...formattedItem, + rainInfo: `${data.rainInfo === '0' ? '강우량계' : data.observatory || '-'}`, // 강우정보 0: 강우량계,1: 관측기관 이면 OBSERVATORY 이값사용 + mesureIntrvl: `${data.mesureIntrvlGbn === '1' ? '1분' : (data.mesureIntrvlGbn === '0' ? data.mesureIntrvl + '분' : '-')}`, // 측정간격 + flwmtrCombined: `${flwmtrCombined} / ${data.flwmtrKnd || '-'}`, // 유량계종류 및 모델명 + mesureBeginDt: data.mesureBeginDt ? formatNoYearNoSec(data.mesureBeginDt) : '-', // 수량 측정시작일 + mesureEndDt: data.mesureEndDt ? formatNoYearNoSec(data.mesureEndDt) : '-', // 수량 측정종료일 + qltwtrBeginDt: data.qltwtrBeginDt ? formatNoYearNoSec(data.qltwtrBeginDt) : '-', // 수질 측정시작일(채수시작일) + qltwtrEndDt: data.qltwtrEndDt ? formatNoYearNoSec(data.qltwtrEndDt) : '-', // 수질 측정종료일(채수종료일) + }; + }); + setRainMeasureData(processedDataArray); + setTotalCount(result?.totalElements || 0); + } catch (error) { + console.error('추가 데이터 로딩 실패:', error); + return []; + } + }; + + // 수량/수질측정 정보 API 호출 함수 + const fetchMntrngsData = async (adstDstrCd: string, manageNo: string) => { + setIsResetDetailPage(true); + if (!adstDstrCd || !manageNo) { + console.warn('추가 데이터 호출 실패: 필수 매개변수 누락', { adstDstrCd, manageNo }); + return []; + } + + try { + const queryParams = new URLSearchParams({ + page: `${curDetailPage - 1}`, + size: '3', + }); + + let result; + const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, { + method: "GET", + }); + if (res.ok) { + // 응답이 비어있는지 확인 + const contentLength = res.headers.get('content-length'); + if (contentLength === '0') result = null; + + // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 + const text = await res.text(); + if (!text || text.trim() === '') result = null; + + // JSON 파싱 시도 + result = JSON.parse(text); + } + + // 각 아이템 전처리 + const processedDataArray = result.content.map((data: any) => { + // 기본 포맷팅 적용 + const formattedItem = formatDisplayData(data); + return { + ...formattedItem, + }; + }); + + setMntrngsData(processedDataArray); + setTotalDetailCount(result?.totalElements || 0); + } catch (error) { + console.error('추가 데이터 로딩 실패:', error); + return []; + } + }; + + + // 🎯 추가 데이터는 별도 useEffect로 비동기 로딩 + useEffect(() => { + // 강우/측정 정보 데이터 호출 + if (isMainSsc && chmbrXcnts && chmbrYdnts) { + fetchRainMeasureData(chmbrXcnts, chmbrYdnts) + } + }, [chmbrXcnts, chmbrYdnts]); + + const openModal = () => { + setIsModalOpen(true); + }; + + const closeModal = () => { + setIsModalOpen(false); + if (!isDataLoaded) { + setTimeout(() => setIsDataLoaded(true), 100); + } + }; + + // featureData가 변경될 때 formData 초기화 + useEffect(() => { + if (featureData) { + setFormData(featureData); + setMntrngsData([]); + } + }, [featureData]); + + // featureData가 변경될 때 formData 초기화 + useEffect(() => { + // showTable이 true일 때만 fetch (최초 렌더 방지) + setIsResetDetailPage(false); + setSelectedRowIndex(-1); + if (chmbrXcnts && chmbrYdnts) fetchRainMeasureData(chmbrXcnts, chmbrYdnts); + }, [curPage]); + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + if (chmbrXcnts && chmbrYdnts && selectedRowIndex !== -1) fetchMntrngsData(rainMeasureData[selectedRowIndex].adstDstrCd, rainMeasureData[selectedRowIndex].manageNo); + }, [curDetailPage]); + + // 폼 데이터가 변경될 때 호출될 핸들러 + const handleFormDataChange = (newFormData: any) => { + setFormData(newFormData); + }; + + // 저장 버튼 클릭 시 최신 formData를 부모에게 전달 + const handleSaveClick = () => { + if (onSaveClick) { + // 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달 + onSaveClick(); + } + }; + + // 테이블 셀 클릭 핸들러 추가 + const handleCellClick = async (index: number | undefined, headId: string) => { + if (index !== undefined && rainMeasureData) { + await fetchMntrngsData(rainMeasureData[index].adstDstrCd, rainMeasureData[index].manageNo); + setSelectedRowIndex(index); + } + }; + + const renderModalBody = () => ( +
+
+ +
+ {/* 강우정보 테이블 */} + + + {/* 측정정보 테이블 */} + + {/* 커스텀 헤더 */} + + 수량 + 수질 + + + 측정시작일시 + 측정종료일시 + 측정간격(분) + 측정기기설치위치 + 유량계 종류 및 모델명 + 채수시작일시 + 채수종료일시 + 채수횟수 + + +
+ +
+ {/* 수량/수질측정 정보 테이블 */} + +
+ +
+
+ ) + + return ( +
+ {!isMainSsc && ( + + )} + + {isMainSsc && ( +
+
+

중점우수토실 운영정보

+
+ + + +
+ )} +
+ ) +} + +export default Ssc \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx new file mode 100644 index 000000000..8b029673f --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx @@ -0,0 +1,73 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (하수처리시설) + * + * 📄 SwtrmtFclt + * 하수처리시설 정보 컴포넌트 + * + * - 하수처리시설의 디지털 자료관 연계 정보 표시 + * - VerticalTable을 통한 테이블 렌더링 + * - 조회 버튼을 통한 모달 표시 및 ReportTab 연동 + */ +'use client' +import React, { useState } from 'react' +import { VerticalTable } from '@/components/ui/verticalTable'; +import { UiModal } from '@common/business_components/ui'; +import { Button } from '@common/components/ui'; +import ReportTab from '../../searchModal/reportTab'; + +// 하수처리시설 +const headList = [ + { id: "title", title: "하수도정비 기본계획보고서" }, +]; + +const SwtrmtFclt = ({ gisObject }: { gisObject: any }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const ctpvNm = gisObject?.ctpv_nm; + const sggNm = gisObject?.sgg_nm; + + return ( +
+ setIsModalOpen(true)}>조회 + }} + headName='디지털 자료관 연계정보' + tableData={[]} + headlist={headList} + pageSize={20} + total={0} + cellClick={undefined} + isEditable={true} + onPageChange={function (page: number): void { + throw new Error("Function not implemented."); + }} + /> + + } + useDirectRender={true} + /> +
+ ) +} + +export default SwtrmtFclt \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts b/apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts new file mode 100644 index 000000000..aba8d2eed --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/aplcnYms.ts @@ -0,0 +1,271 @@ +import {useState, useEffect} from 'react'; +import { getData } from '@/components/lib/api'; +import moment from "moment"; + +export type TInftwtAnls = { + inftwtAnlsSn:number; + aplcnYm: string; + ctpvCd: string; + sggCd: string; + swtrmtFcltNm: string; + atflCd: string; + atflId: string; + atflInpNm: string; + atflHrPttnNm: string; + atflMpngNm: string; + atflOilqNm: string; + atflNm: string; + drtwSwtcRt:number; + anlsInfo: string; + scsYn: string; +}; +export type TInftwtAnlsRslt = { + inftwtAnlsSn: number; + pplMngNo: string; + anlsTm: string; + predcOilq:number; +} +export type TRdiiAnls = { + rainInfwtAnlsSn:number; + aplcnYm: string; + ctpvCd: string; + sggCd: string; + swtrmtFcltNm: string; + atflCd: string; + atflId: string; + atflInpNm: string; + atflHrPttnNm: string; + atflMpngNm: string; + atflRainNm: string; + atflNm: string; + drtwSwtcRt:number; + anlsInfo: string; + scsYn: string; + srcNm: string; // 출처이름 +}; +export type TRdiiAnlsRslt = { + rainInfwtAnlsSn: number; + pplMngNo: string; + anlsTm: string; + predcOilq:number; + minq:number; +} +export type TOilqData = { + inftwtAnlsSn: number; // 침입수분석 일련번호 + pplMngNo: string; // 관로관리번호 + mtrngDt: string; // 계측일시 + oilq: number; // 유량 +} +export type TRainData = { + rainInfwtAnlsSn: number; + mtrngDt: string; + amrf: number; // 강우량 +} + +function roundTo(n: number, digits: number): number { + const factor = Math.pow(10, digits); + return Math.round(n * factor) / factor; +} + +const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string, hidden?: boolean }[] => { + const yyyymms1 : { text: string; val: string; hidden?: boolean }[] = yyyymms.map(dateStr => { + const year = dateStr.slice(0, 4); + const month = dateStr.slice(4, 6); + return { + text: `${year}-${month}`, + val: dateStr + }; + }); + yyyymms1.unshift({ val: '0', text: '선택', hidden: true }); + return yyyymms1; +}; +const selectAplcnYms = async(gubun:string) => { // 상수사용월 + try { + const queryString = ''; + const result = await getData(`v1/${gubun}/aplcnYm${queryString ? `?${queryString}` : ''}`); + if (result && result.length > 0) { + return getOptionsAplcnYm(result); + } + } catch (error) { + console.error(error); + } +}; + +export const useAplcnYms = (gubun:string) => { + const [data, setData] = useState<{ text: string; val: string }[]>([]); + + useEffect(() => { + const fetch = async (gubun:string) => { + const result = await selectAplcnYms(gubun); + if (result) setData(result); + }; + fetch(gubun); + }, [gubun]); + + return data; +}; +export const getOptionsInftwtAnlsInfo = (anlsArr: TInftwtAnls[]): { text: string; val: number, hidden?: boolean }[] => { + const options : { text: string; val: number; hidden?: boolean }[] = anlsArr.map(anls => ({ + text: anls.anlsInfo, + val: anls.inftwtAnlsSn, + })); + options.unshift({ val: 0, text: '선택', hidden: true }); + return options; +}; + +export const selectInftwtAnls = async(aplcnYm:string) => { // 침입수분석 + try { + const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : ''; + const result = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); + if (result && result.numberOfElements > 0) { + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const getChartDataInftwtAnlsRslt = (arr: TInftwtAnlsRslt[]): { label: string; val: number }[] => { + const options = arr.map(anlsRslt => ({ + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("HH시"), + val: roundTo(anlsRslt.predcOilq, 2), + })); + return options; +}; +export const selectInftwtAnlsRslt = async(inftwtAnlsSn:number, pplMngNo:string) => { // 침입수분석 결과 + try { + if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) { + console.warn("inftwtAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/inftwtAnlsRslt/ppl/${inftwtAnlsSn}/${pplMngNo}`); + if (result && result.numberOfElements > 0) { + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const getChartDataOilq = (arr: TOilqData[]): { label: string; val: number }[] => { + const options = arr.map(anlsRslt => ({ + label: anlsRslt.mtrngDt, + val: roundTo(anlsRslt.oilq, 2), + })); + return options; +}; +export const selectOilq = async(inftwtAnlsSn:number, pplMngNo:string) => { // 유량 + try { + if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) { + console.warn("inftwtAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/inftwtAnlsRslt/oilqPpl/${inftwtAnlsSn}/${pplMngNo}`); + if (result && result.numberOfElements > 0) { + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const selectPplInftwtQty = async(inftwtAnlsSn:number, pplMngNo:string) => { // 침입수량 + try { + if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) { + console.warn("inftwtAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/inftwtAnlsRslt/pplInftwtQty/${inftwtAnlsSn}/${pplMngNo}`); + if (result.length > 0) { + return result[0].avg_day_flow; + } else { + return 0; // 데이터가 없을 경우 0 반환 + } + } catch (error) { + console.error(error); + } +}; +//---------- +export const selectPplRdiiQty = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // rdii값 + try { + if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) { + console.warn("rainInfwtAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/rdiiAnlsRslt/pplRdiiQty/${rainInfwtAnlsSn}/${pplMngNo}`); + if (result?.sustaintime > 0) { + return result; + } + } catch (error) { + console.error(error); + } +}; + +export const getOptionsRdiiAnlsInfo = (anlsArr: TRdiiAnls[]): { text: string; val: number, hidden?: boolean }[] => { + const options : { text: string; val: number; hidden?: boolean }[] = anlsArr.map(anls => ({ + text: anls.anlsInfo, + val: anls.rainInfwtAnlsSn, + })); + options.unshift({ val: 0, text: '선택', hidden: true }); + return options; +}; + +export const selectRdiiAnls = async(aplcnYm:string) => { // 침입수분석 + try { + const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : ''; + const result = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); + if (result && result.numberOfElements > 0) { + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; lineVal0: number, barVal0:number }[] => { + const options = arr.map(anlsRslt => ({ + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"), + lineVal0: roundTo(anlsRslt.predcOilq, 2), + barVal0: 0 + })); + return options; +}; +export const getChartDataRdiiAnlsRsltMinq = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => { + const options = arr.map(anlsRslt => ({ + label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"), + val: roundTo(anlsRslt.minq, 2), + })); + return options; +}; +export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // RDII분석 결과 + try { + if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) { + console.warn("RdiiAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/rdiiAnlsRslt/ppl/${rainInfwtAnlsSn}/${pplMngNo}`); + if (result && result.numberOfElements > 0) { + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const selectRainData = async(rainInfwtAnlsSn:number) => { // 강우데이터 + try { + if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0)) { + console.warn("RdiiAnlsSn or pplMngNo is not provided"); + return []; + } + const result = await getData(`v1/rdiiAnlsRslt/rainData/${rainInfwtAnlsSn}`); + if (result && result.numberOfElements > 0) { + return result.content; + } + } catch (error) { + console.error(error); + } +}; +export const getChartDataRainData = (arr: TRainData[]): { label: string; lineVal0: number, barVal0: number }[] => { + const options = arr.map(data => ({ + label: moment(data.mtrngDt, "YYYY-MM-DD HH:mm:ss").format("MM-DD HH시"), + lineVal0: 0, + barVal0: roundTo(data.amrf,2), + })); + return options; +}; \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/infilChart.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/infilChart.tsx new file mode 100644 index 000000000..3836b87f6 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/infilChart.tsx @@ -0,0 +1,297 @@ +'use client' + +import React, { useEffect, useMemo, useState } from 'react'; +import moment from 'moment'; + +import { + CHART_FORM, + type ChartConfig, + MainChart, + UiModal, +} from '@common/business_components'; +import { SelectBox } from '@common/business_components/ui'; +import { Button, Input, Label } from '@common/components'; + +import type { TInftwtAnls } from './aplcnYms'; +import * as fn from './aplcnYms'; +import { useInfilChartModalStore } from '../chartModalStore'; + +type ModalOpenProp = { + pplMngNo?: string; // 관로관리번호 +}; + +type DataType = { + label: string; + val: number; +}; + +const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }]; + +const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => { + const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useInfilChartModalStore(); + + const [selectedAplcnYm, setSelectedAplcnYm] = useState(''); + const [inftwtAnlss, setInftwtAnlss] = useState([]); + const [inftwtAnlsInfos, setInftwtAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>( + INITIAL_ANALYSIS_OPTIONS, + ); + const [inftwtAnls, setInftwtAnls] = useState(null); + const [selectedInftwtAnls, setSelectedInftwtAnls] = useState(0); + const [chartData, setChartData] = useState<{ chartData1: DataType[]; chartData2: DataType[] }>({ + chartData1: [], + chartData2: [], + }); + const [inftwtQty, setInftwtQty] = useState('0'); + + const aplcnYms = fn.useAplcnYms('inftwtAnlsRslt'); + const normalizedPplMngNo = pplMngNo ? `L${pplMngNo}` : ''; + + const chartForm = CHART_FORM.LINE; + const chartConfig = { + chartData1: { label: '순오수량', color: 'red' }, + chartData2: { label: '실측하수량', color: 'orange' }, + } as ChartConfig; + + const hasChartData = useMemo(() => { + return chartData.chartData1.length > 0 || chartData.chartData2.length > 0; + }, [chartData]); + + const resetState = () => { + setSelectedAplcnYm(''); + setInftwtAnlss([]); + setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS); + setInftwtAnls(null); + setSelectedInftwtAnls(0); + setChartData({ chartData1: [], chartData2: [] }); + setInftwtQty('0'); + }; + + useEffect(() => { + if (!isModalOpen) { + resetState(); // 모달이 닫힐 때 리셋 + } + }, [isModalOpen]); + + const handleAplcnYmChange = async (val: string | number) => { + const selectedValue = String(val); + + setSelectedAplcnYm(selectedValue); + setInftwtAnlss([]); + setInftwtAnls(null); + setSelectedInftwtAnls(0); + setInftwtQty('0'); + setChartData({ chartData1: [], chartData2: [] }); + + if (!selectedValue || selectedValue === '0') { + setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS); + return; + } + + try { + const data = await fn.selectInftwtAnls(selectedValue); + const list = data ?? []; + setInftwtAnlss(list); + setInftwtAnlsInfos(fn.getOptionsInftwtAnlsInfo(list)); + } catch (error) { + console.error('selectInftwtAnls 에러:', error); + setInftwtAnlss([]); + setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS); + } + }; + + const handleInftwtAnlsSnChange = (val: string | number) => { + const selectedValue = Number(val); + + setSelectedInftwtAnls(selectedValue); + setChartData({ chartData1: [], chartData2: [] }); + setInftwtQty('0'); + + if (selectedValue === 0) { + setInftwtAnls(null); + return; + } + + const selectedAnls = inftwtAnlss.find((anls) => anls.inftwtAnlsSn === selectedValue) ?? null; + setInftwtAnls(selectedAnls); + }; + + const infilRsltChart = async () => { + if (!selectedAplcnYm || selectedInftwtAnls === 0 || !normalizedPplMngNo) { + setChartData({ chartData1: [], chartData2: [] }); + setInftwtQty('0'); + return; + } + + try { + const [inftwtAnlsRslt, oilq, qty] = await Promise.all([ + fn.selectInftwtAnlsRslt(selectedInftwtAnls, normalizedPplMngNo), + fn.selectOilq(selectedInftwtAnls, normalizedPplMngNo).catch((err) => { + console.error('selectOilq 에러:', err); + return []; + }), + fn.selectPplInftwtQty(selectedInftwtAnls, normalizedPplMngNo).catch((err) => { + console.error('selectPplInftwtQty 에러:', err); + return 0; + }), + ]); + + setChartData({ + chartData1: fn.getChartDataInftwtAnlsRslt(inftwtAnlsRslt ?? []), + chartData2: fn.getChartDataOilq(oilq ?? []), + }); + + const normalizedQty = Number(qty ?? 0); + setInftwtQty(normalizedQty.toLocaleString('ko-KR')); + } catch (error) { + console.error('infilRsltChart 에러:', error); + setChartData({ chartData1: [], chartData2: [] }); + setInftwtQty('0'); + } + }; + + const renderInfoInput = (label: string, value?: string | number | null) => ( +
+ + +
+ ); + + const handleReset = () => { + resetState(); + } + + const renderBody = () => ( +
+
+
+
+ +
+
+

+ 침입수 분석 선택 조건 +

+ +
+
+
+ + +
+ +
+ + +
+ + {renderInfoInput('오수전환율:', inftwtAnls?.drtwSwtcRt ?? '')} + {renderInfoInput('시간패턴 파일명:', inftwtAnls?.atflHrPttnNm ?? '')} + {renderInfoInput('수용가-맨홀 매칭:', inftwtAnls?.atflMpngNm ?? '')} + {renderInfoInput('관망도 파일명:', inftwtAnls?.atflInpNm ?? '')} + {renderInfoInput('유량 파일명:', inftwtAnls?.atflOilqNm ?? '')} +
+
+ +
+    + +
+
+ +
+

침입수 분석 결과

+ +
+
+
+ {/* ● */} + 상수사용량 적용연월 +
+
+ {selectedAplcnYm ? moment(selectedAplcnYm, 'YYYYMM').format('YYYY-MM') : '-'} +
+
+ +
+
+ {/* ● */} + 침입수량 산정결과 +
+
+ 침입수량 +
+ {inftwtQty} + m³/d +
+
+
+
+ +
+ {hasChartData ? ( + + ) : ( +
+ 데이터가 없습니다. +
+ )} +
+
+
+
+ ); + + return ( + + ); +}; + +export default React.memo(InfilChart); diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx new file mode 100644 index 000000000..aa622be1b --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/chart/rdiiChart.tsx @@ -0,0 +1,357 @@ +'use client' + +import React, { useEffect, useMemo, useState } from 'react'; +import moment from 'moment'; + +import { + CHART_FORM, + type ChartConfig, + MainChart, + UiModal, +} from '@common/business_components'; +import { SelectBox } from '@common/business_components/ui'; +import { Button, Input, Label } from '@common/components'; + +import type { TRdiiAnls } from './aplcnYms'; +import * as fn from './aplcnYms'; +import { useRdiiChartModalStore } from '../chartModalStore'; + +type ModalOpenProp = { + pplMngNo?: string; // 관로관리번호 +}; + +type LineBarChartData = { + label: string; + lineVal0: number; + barVal0: number; +}; + +const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }]; + +const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => { + const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore(); + + const [aplcnYm, setAplcnYm] = useState(''); + const [rdiiAnlss, setRdiiAnlss] = useState([]); + const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>( + INITIAL_ANALYSIS_OPTIONS, + ); + const [rdiiAnls, setRdiiAnls] = useState(null); // 강우유입수분석정보 + const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState(0);// 강우유입수분석 정보 선택시 사용 + const [chartData, setChartData] = useState([]); + const [rdiiQty, setRdiiQty] = useState('0'); // 강우유입수량 + const [rainTotal, setRainTotal] = useState('0'); // 적용강우량 + const [totalVolume, setTotalVolume] = useState('0');// 총체적 m³/mm + + const aplcnYms = fn.useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기 + const normalizedPplMngNo = pplMngNo ? `L${pplMngNo}` : ''; + + const chartForm = CHART_FORM.LINEBAR2; + const chartConfig = { + barVal0: { + label: '강수량', + color: '#33AAFF', + }, + lineVal0: { + label: 'RDII', + color: '#FF0000', + }, + } as ChartConfig; + + const hasChartData = useMemo(() => chartData.length > 0, [chartData]); + + const resetState = () => { + setAplcnYm(''); // 상수사용월 초기화 + setRdiiAnlss([]); // 강우유입수분석 초기화 + setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS); // 강우유입수분석정보 초기화 + setRdiiAnls(null); // 강우유입수분석정보 초기화 + setRainInfwtAnlsSn(0); // 강우유입수분석 일련번호 초기화 + setChartData([]); + setRdiiQty('0'); // 강우유입수량 초기화 + setRainTotal('0');// 적용강우량 초기화 + setTotalVolume('0');// 총체적 m³/mm 초기화 + }; + + useEffect(() => { + if (!isModalOpen) { + resetState(); + } + }, [isModalOpen]); + + const handleAplcnYmChange = async (val: string | number) => { + const selectedValue = String(val); + + setAplcnYm(selectedValue); + setRdiiAnlss([]); + setRdiiAnls(null); + setRainInfwtAnlsSn(0); + setChartData([]); + setRdiiQty('0'); + setRainTotal('0'); + setTotalVolume('0'); + + if (!selectedValue || selectedValue === '0') { + setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS); + return; + } + + try { + const data = await fn.selectRdiiAnls(selectedValue); // 상수사용월 변경시 분석 정보 리스트 가져오기 + const list = data ?? []; + setRdiiAnlss(list); + setRdiiAnlsInfos(fn.getOptionsRdiiAnlsInfo(list)); + } catch (error) { + console.error('selectRdiiAnls 에러:', error); + setRdiiAnlss([]); + setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS); + } + }; + + const handleRdiiAnlsSnChange = (val: string | number) => { + const selectedValue = Number(val); + + setRainInfwtAnlsSn(selectedValue); + setChartData([]); + setRdiiQty('0'); + setRainTotal('0'); + setTotalVolume('0'); + + if (selectedValue === 0) { + setRdiiAnls(null); + return; + } + + const selectedAnls = rdiiAnlss.find((anls) => anls.rainInfwtAnlsSn === selectedValue) ?? null; + setRdiiAnls(selectedAnls); + }; + + const fetchFilteredData = async () => { + if (!rainInfwtAnlsSn || !normalizedPplMngNo) { + setChartData([]); + setRdiiQty('0'); + setRainTotal('0'); + setTotalVolume('0'); + return; + } + + try { + const [rsltData, rainData, qtyData] = await Promise.all([ + fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, normalizedPplMngNo), + fn.selectRainData(rainInfwtAnlsSn), + fn.selectPplRdiiQty(rainInfwtAnlsSn, normalizedPplMngNo), + ]); + + const rsltDataFormatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []); + const rainDataFormatted = fn.getChartDataRainData(rainData ?? []); + const rainMap = new Map(rainDataFormatted.map((item) => [item.label, item.barVal0])); + + rsltDataFormatted.forEach((item) => { + const matchedVal = rainMap.get(item.label); + if (matchedVal !== undefined) { + item.barVal0 = matchedVal; + } + }); + + setChartData(rsltDataFormatted);// 차트 데이터 설정 + + const avgRdii = Number(qtyData?.rdiih ?? 0); + const appliedRainfall = Number(qtyData?.raintotal ?? 0); + const rdiiPerRainfall = + qtyData?.raintotal && qtyData?.raintotal !== 0 + ? Math.round(avgRdii * Number(qtyData?.sustaintime ?? 0) / qtyData.raintotal) + : 0; + + setRdiiQty(Math.round(avgRdii * 24).toLocaleString('ko-KR')); + setRainTotal(appliedRainfall.toLocaleString('ko-KR')); + setTotalVolume(rdiiPerRainfall.toLocaleString('ko-KR')); + } catch (error) { + console.error('rdiiRsltChart 에러:', error); + setChartData([]); + setRdiiQty('0'); + setRainTotal('0'); + setTotalVolume('0'); + } + }; + + const renderInfoInput = (label: string, value?: string | number | null) => ( +
+ + +
+ ); + + const renderBody = () => ( +
+
+
+
+ +
+
+

+ 강우유입수분석 선택 조건 +

+ +
+
+
+ + +
+ +
+ + +
+ + {renderInfoInput('오수전환율:', rdiiAnls?.drtwSwtcRt ?? '')} + {renderInfoInput('수용가-맨홀 매칭:', rdiiAnls?.atflMpngNm ?? '')} + {renderInfoInput('관망도 파일명:', rdiiAnls?.atflInpNm ?? '')} + {renderInfoInput('강우 파일명:', rdiiAnls?.atflRainNm ?? '')} + {renderInfoInput('RTK계수 출처:', rdiiAnls?.srcNm ?? '')} +
+
+ +
+ +    + +
+
+ +
+

+ 강우유입수분석 결과 +

+ +
+ {/* 좌측 카드 */} +
+
+ 상수사용량 적용연월 +
+
+ {aplcnYm ? moment(aplcnYm, 'YYYYMM').format('YYYY-MM') : '-'} +
+
+ + {/* 우측 카드 */} +
+ {/* 좌측: 제목 (세로 가운데 정렬) */} +
+
+ 강우유입수량 분석결과 +
+
+ + {/* 우측: 지표 영역 */} +
+
+
+ 평균RDII +
+
+ {rdiiQty} +
+
+ m³/d +
+
+ +
+
+ 적용 강우량 +
+
+ {rainTotal} +
+
+ mm +
+
+ +
+
+ 강우량 대비 RDII +
+
+ {totalVolume} +
+
+ m³/mm +
+
+
+
+
+ +
+ {hasChartData ? ( + + ) : ( +
+ 데이터가 없습니다. +
+ )} +
+
+
+
+ ); + + return ( + + ); +}; + +export default React.memo(RdiiChart); diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/chartModalStore.ts b/apps/sn/app/html/(map-layout)/map/components/popupComponent/chartModalStore.ts new file mode 100644 index 000000000..c84fa48a7 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/chartModalStore.ts @@ -0,0 +1,16 @@ +import { create } from 'zustand'; + +interface ModalStore { + isOpen: boolean; + setIsOpen: (open: boolean) => void; +} + +export const useInfilChartModalStore = create((set) => ({ + isOpen: false, + setIsOpen: (open: boolean) => set({ isOpen: open }), +})); + +export const useRdiiChartModalStore = create((set) => ({ + isOpen: false, + setIsOpen: (open: boolean) => set({ isOpen: open }), +})); \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx new file mode 100644 index 000000000..11e61b006 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx @@ -0,0 +1,639 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 + * + * 📄 FeatureClickEventPopup + * 시설물(피처) 클릭시 오른쪽 팝업창 컴포넌트 + * + * - 지도 시설물(피처) 클릭 시 팝업 표시 및 데이터 로딩 + * - 레이어 유형에 따른 동적 테이블 및 헤드리스트 설정 + * - 자체입력정보 조회 및 저장 기능 (하수관로, 우수토실, 하수펌프장, 하수맨홀) + * - 코드 매핑 및 데이터 포맷팅 처리 + * - 상류관로 검색, 침입수/RDII 분석 차트 표시 + * - 모달을 통한 저장 확인 및 API 연동 + * + * 🔗 사용 API + * - 하수관로 자체입력정보 조회: GET /api/v1/fcltppl/{fcltMngNo}/{mngInstCd} (sewerage-service) + * - 우수토실 자체입력정보 조회: GET /api/v1/fcltstofcbr/{fcltMngNo}/{mngInstCd} (sewerage-service) + * - 하수펌프장 자체입력정보 조회: GET /api/v1/fcltpump/{fcltMngNo}/{mngInstCd} (sewerage-service) + * - 하수맨홀 자체입력정보 조회: GET /api/v1/fcltmhl/{fcltMngNo}/{mngInstCd} (sewerage-service) + * - 하수관로 자체입력정보 저장: POST /api/v1/fcltppl (sewerage-service) + * - 우수토실 자체입력정보 저장: POST /api/v1/fcltstofcbr (sewerage-service) + * - 하수펌프장 자체입력정보 저장: POST /api/v1/fcltpump (sewerage-service) + * - 하수맨홀 자체입력정보 저장: POST /api/v1/fcltmhl (sewerage-service) + */ +'use client' +import { getData, postData } from "@/components/lib/api"; +import Modal from "@/components/ui/modal"; +import { VerticalTable } from "@/components/ui/verticalTable"; +import { useGisContext } from "@common/business_components/ui/map/Gis"; +import { Button } from "@common/components/ui"; +import { useEffect, useRef, useState, useMemo } from "react"; +import Style from "ol/style/Style.js"; +import Fill from "ol/style/Fill.js"; +import Stroke from "ol/style/Stroke.js"; +import CircleStyle from "ol/style/Circle.js"; +import { getLayerContentComponent } from "../common"; +import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table"; +import InfilChart from './chart/infilChart'; +import RdiiChart from './chart/rdiiChart'; +import { useInfilChartModalStore, useRdiiChartModalStore } from "./chartModalStore"; +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; +import { codeMapper } from "../common/codeMapper"; +import LoadingOverlay from '@/components/ui/LoadingBar'; +import { searchUpperPpls } from "./smh"; + +const linkedFacilityHighlightStyle = new Style({ + image: new CircleStyle({ + radius: 18, + fill: new Fill({ + color: 'rgba(255, 235, 59, 0.35)', + }), + stroke: new Stroke({ + color: 'rgba(255, 193, 7, 0.45)', + width: 2, + }), + }), + fill: new Fill({ + color: 'rgba(255, 235, 59, 0.2)', + }), + stroke: new Stroke({ + color: 'rgba(255, 193, 7, 0.35)', + width: 2.5, + }), + zIndex: 9999, +}); + + +const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => { + const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; }; + const [layerNm, setLayerNm] = useState(""); // 레이어 이름 상태 + const [layerSpecificNm, setLayerSpecificNm] = useState(""); // 레이어 이름 상태 + const [tableData, setTableData] = useState(null); // 피처 데이터 + const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 + const [headlist2, setHeadlist2] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 + const [buttonChk, setButtonChk] = useState(true) + const [fetchData, setFetchData] = useState(); // api 호출여부 + const contentRef = useRef(null); + const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태 + const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부 + const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부 + // 🎯 InfilChart 및 RdiiChart 관련 상태 추가 + const { setIsOpen: setIsModalInfil } = useInfilChartModalStore(); + const { setIsOpen: setIsModalRdii } = useRdiiChartModalStore(); + const [isClicked, setIsClicked] = useState(true); // 클릭 여부 상태 + const [isLoading, setIsLoading] = useState(false); // 로딩 상태 + const [chmbrXcnts, setChmbrXcnts] = useState(''); // 로딩 상태 + const [chmbrYdnts, setChmbrYdnts] = useState(''); // 로딩 상태 + const linkedFacilityFeatureRef = useRef(null); + const linkedFacilityOriginalStyleRef = useRef(null); + + // 🎯 관로관리번호 추출 + const pplMngNo = featureData?.values_?.fclt_mng_no || ""; + + const featureId = typeof featureData.getId === 'function' + ? featureData.getId() + : featureData.fid; + + // 🎯 데이터 전처리 메모이제이션 + const processedData = useMemo(() => { + if (!featureData?.values_) return null; + + return { + ...featureData.values_, + discharge_pipe_spec: `직경: ${featureData.values_.pipe_diameter || '-'} / 높이: ${featureData.values_.pipe_hg || '-'} / 길이: ${featureData.values_.pipe_lt || '-'}` + }; + }, [featureData?.values_]); + + const clearLinkedFacilityHighlight = () => { + if (linkedFacilityFeatureRef.current) { + linkedFacilityFeatureRef.current.setStyle(linkedFacilityOriginalStyleRef.current ?? undefined); + linkedFacilityFeatureRef.current.changed?.(); + } + + linkedFacilityFeatureRef.current = null; + linkedFacilityOriginalStyleRef.current = null; + }; + + // 하수처리구역에 연결된 하수처리시설 피처를 찾아 기존 아이콘 위에 노랑 하이라이트를 덧씌운다. + const highlightLinkedFacility = (facilityCode: string | null | undefined) => { + clearLinkedFacilityHighlight(); + + if (!gisMap || !facilityCode) return; + + const normalizedFacilityCode = String(facilityCode).trim(); + const layers = gisMap.getLayers().getArray(); + + for (const layer of layers) { + const source = layer?.getSource?.(); + if (!source?.getFeatures) continue; + + const features = source.getFeatures(); + const targetFeature = features.find((feature: any) => { + const featureId = feature?.getId?.(); + const isFacilityLayer = typeof featureId === 'string' && featureId.startsWith('tb_il_swage_prcs_fclt.'); + const featureFacilityCode = feature?.get?.('swtrmt_fclt_cd'); + + return isFacilityLayer && String(featureFacilityCode ?? '').trim() === normalizedFacilityCode; + }); + + if (targetFeature) { + const originalFeatureStyle = targetFeature.getStyle?.(); + const featureLayer = targetFeature.getLayer?.(); + const layerStyleFn = featureLayer?.getStyleFunction?.() ?? null; + + linkedFacilityOriginalStyleRef.current = originalFeatureStyle; + linkedFacilityFeatureRef.current = targetFeature; + targetFeature.setStyle((feature: any, resolution: number) => { + let styles: any[] = []; + + if (typeof originalFeatureStyle === 'function') { + const originalStyles = originalFeatureStyle(feature, resolution); + if (Array.isArray(originalStyles)) styles.push(...originalStyles); + else if (originalStyles) styles.push(originalStyles); + } else if (originalFeatureStyle) { + if (Array.isArray(originalFeatureStyle)) styles.push(...originalFeatureStyle); + else styles.push(originalFeatureStyle); + } else if (layerStyleFn) { + const layerStyles = layerStyleFn(feature, resolution); + if (Array.isArray(layerStyles)) styles.push(...layerStyles); + else if (layerStyles) styles.push(layerStyles); + } + + styles.push(linkedFacilityHighlightStyle); + return styles; + }); + targetFeature.changed?.(); + return; + } + } + }; + + // 피쳐데이터의 현재 레이어 이름 가져오는 함수 + const getSelectedLayers = () => { + let result = null; + const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기 + let featureLayer: any = null; // 피처가 속한 레이어를 찾기 위한 변수 + layers.some((layer: any) => { + const source = layer.getSource(); + // if (source && featureData && featureData.fid) { + // featureLayer = layer; // 피처가 속한 레이어 찾기 + // } + if (source && source.hasFeature && featureData + && typeof featureData.getId === 'function' && source.hasFeature(featureData)) { + featureLayer = layer; // 피처가 속한 레이어 찾기 + return true; // 반복 종료 + } + return false; + }); + + if (featureData['ol_uid']) { + // Gis 레이어 클릭 이벤트 + setIsClicked(true); + const lyrTyNm = featureData.id_.split('.')[0]; + + switch (lyrTyNm) { + case 'tb_ms_swage_ppl': { + result = '하수관로'; + setHeadlist(MsSwagePplCol); + setLayerSpecificNm(featureData.values_.fclt_mng_no); + break; + } + case 'tb_ms_swage_mhl': { + result = '하수맨홀'; + setHeadlist(MsSwageMhlCol); + setLayerSpecificNm(featureData.values_.fclt_mng_no); + break; + } + case 'tb_ms_swage_pmstn': { + const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장'); + if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) { + setIsDrtwSewagePump(true); + setHeadlist(DrtwSewagePumpCol); + } + else { + setIsDrtwSewagePump(false); + setHeadlist(ExclnSewagePumpCol); + } + setLayerSpecificNm(featureData.values_.fclt_mng_no); + result = '하수펌프장'; + break; + } + case 'tb_il_swage_prcs_fclt': { + result = '하수처리시설'; + setHeadlist(SwtrmtFcltCol); + setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd); + break; + } + case 'tb_ms_swage_prcs_zone': { + result = '하수처리구역'; + setHeadlist(SwtrmtPrcsZoneCol); + setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd); + break; + } + case 'tb_ms_stofcbr': { + result = '우수토실'; + setHeadlist(stofcbrCol); + setLayerSpecificNm(featureData.values_.fclt_mng_no); + break; + } + case 'om_dt_untrt_swg_rain': { + result = '중점우수토실'; + setHeadlist(untrtSwgRainCol1); + setHeadlist2(untrtSwgRainCol2); + setLayerSpecificNm(featureData.values_.manage_no); + break; + } + default: { + result = featureLayer ? featureLayer.get('lyrNm') : null; + break; + } + } + } + else { + // SearchModal 클릭 이벤트 + setIsClicked(false); + const lyrTyNm = featureData.lyrTypeNm; + + switch (lyrTyNm) { + case 'tb_ms_swage_ppl': { + result = '하수관로'; + setHeadlist(MsSwagePplCol2) + setLayerSpecificNm(featureData.swtrmtFcltCd); + break; + } + case 'tb_ms_swage_mhl': { + result = '하수맨홀'; + setHeadlist(MsSwageMhlCol2) + setLayerSpecificNm(featureData.fcltMngNo); + break; + } + case 'tb_ms_swage_pmstn': { + const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장'); + if (drtwPumpCode && featureData.usgCd.trim() === drtwPumpCode.val) { + setIsDrtwSewagePump(false); + setHeadlist(ExclnSewagePumpCol2) + } + else { + setIsDrtwSewagePump(true); + setHeadlist(DrtwSewagePumpCol2) + } + setLayerSpecificNm(featureData.fcltMngNo); + result = '하수펌프장'; + break; + } + case 'tb_il_swage_prcs_fclt': { + result = '하수처리시설'; + setHeadlist(SwtrmtFcltCol2) + setLayerSpecificNm(featureData.swtrmtFcltCd); + break; + } + case 'tb_ms_swage_prcs_zone': { + result = '하수처리구역'; + setHeadlist(SwtrmtPrcsZoneCol2); + setLayerSpecificNm(featureData.swtrmtFcltCd); + break; + } + case 'tb_ms_stofcbr': { + result = '우수토실'; + setHeadlist(stofcbrCol2) + setLayerSpecificNm(featureData.fcltMngNo); + break; + } + } + } + + return result; + }; + + useEffect(() => { + const fetchData = async () => { + if (!featureId) return; + + try { + const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 + setLayerNm(selectedLayerName); // 상태 업데이트 + + let data; + if (featureData['ol_uid']) data = featureData.values_; // Gis 레이어 클릭 이벤트일 때 + else data = featureData; // SearchModal 클릭 이벤트일 때 + + // 하수맨홀인 경우 용도코드를 텍스트로 변환 + if (selectedLayerName === '하수맨홀' && data) { + // mqlt_cd 값 변환 (재질코드) + if (data.mqlt_cd) { + const materialCodeItem = MS_SWAGE_MHL_CODES.find( + code => code.val === data.mqlt_cd.trim() + ); + if (materialCodeItem) data.mqlt_cd = `${materialCodeItem.text}`; + } + } else { + // 중점우수토실인 경우 구분 값 + if (selectedLayerName === '중점우수토실' && data) { + if (data.mesure_spot_se) { + const mesureSpotSe = [{id:'0', text: '합류식'}, {id:'1', text: '분류식'}] + const mesureSpotSeItem = mesureSpotSe.find( + code => code.id === data.mesure_spot_se.trim() + ); + if (mesureSpotSeItem) data.mesure_spot_se = mesureSpotSeItem.text; + } + } + } + + // 🎯 기본 데이터 포맷팅 + const formattedData = formatDisplayData(data); + + setFetchData(formattedData); + setTableData(formattedData); + + switch (selectedLayerName) { + case '하수관로': + setButtonChk(true) + + // ✅ codeMapper 사용하여 API 결과도 변환 + setTableData(codeMapper({ + layerName: selectedLayerName, + data: { ...formattedData } + })); + break; + case '하수맨홀': + setButtonChk(true) + + // ✅ codeMapper 사용하여 API 결과도 변환 + setTableData(codeMapper({ + layerName: selectedLayerName, + data: { ...formattedData } + })); + break; + case '하수펌프장': + setButtonChk(true) + + setTableData(codeMapper({ + layerName: selectedLayerName, + data: { ...formattedData } + })); + break; + case '하수처리시설': + setButtonChk(false) + break; + case '하수처리구역': + setButtonChk(false) + break; + case '우수토실': + setButtonChk(true) + setIsMainSsc(false) + break; + case '중점우수토실': + setButtonChk(false) + setIsMainSsc(true) + // 🎯 processedData도 포맷팅하여 설정 + const processedFormattedData = formatDisplayData(processedData); + + setFetchData([processedFormattedData]) + setTableData(processedFormattedData); + // setIsLoading(false); // 기본 렌더링 완료 + + setTableData(codeMapper({ + layerName: selectedLayerName, + data: { ...processedFormattedData } + })); + + setChmbrXcnts(data.chmbr_xcnts); + setChmbrYdnts(data.chmbr_ydnts); + + break; + default: + break; + } + + // 자체입력정보 API 엔드포인트 선택 + let apiEndpoint = ''; + const fcltMngNo = featureData?.values_?.fclt_mng_no || null; + const mngInstCd = featureData?.values_?.mng_inst_cd || null; + if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${fcltMngNo}/${mngInstCd}`; + else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${fcltMngNo}/${mngInstCd}`; + else if (selectedLayerName === '하수펌프장') apiEndpoint = `v1/fcltpump/${fcltMngNo}/${mngInstCd}`; + else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${fcltMngNo}/${mngInstCd}`; + + // API에서 상세 데이터 가져오기 + if (apiEndpoint) { + try { + const result = await getData(apiEndpoint); + + if (result) { + const processedApiData = codeMapper({ + layerName: selectedLayerName, + data: { ...result, ...tableData } + }); + + setFetchData(processedApiData); + } + } catch (apiError) { + console.error('API 데이터 로딩 중 오류:', apiError); + } + } + } catch (error) { + console.error('데이터 로딩 중 오류:', error); + } + }; + + fetchData(); + }, [featureId, processedData]); + + useEffect(() => { + const layerTypeNm = featureData?.values_ + ? featureData?.id_?.split('.')?.[0] + : featureData?.lyrTypeNm; + + if (layerTypeNm === 'tb_ms_swage_prcs_zone') { + const facilityCode = featureData?.values_?.swtrmt_fclt_cd ?? featureData?.swtrmtFcltCd; + highlightLinkedFacility(facilityCode); + return () => { + clearLinkedFacilityHighlight(); + }; + } + + clearLinkedFacilityHighlight(); + + return () => { + clearLinkedFacilityHighlight(); + }; + }, [featureData, gisMap]); + + // 저장 시 모달 띄우기 + const handleSaveClick = () => { + setIsModalOpen(true); // 모달 열기 + }; + + const handleModalSave = () => { + setIsModalOpen(false); // 모달 닫기 + btnClickEvent(); // 저장 이벤트 실행 + }; + + const handleModalClose = () => { + setIsModalOpen(false); // 모달 닫기 + }; + + // 저장 버튼 이벤트 + const btnClickEvent = async () => { + const bodyData: Record = {} + + if (buttonChk && contentRef.current) { + const inputElements = contentRef.current.querySelectorAll('input'); + const selectElements = contentRef.current.querySelectorAll('select'); + + // 카멜케이스 변환 함수 + const toCamelCase = (str: string) => + str.replace(/_([a-z])/g, (_, char) => char.toUpperCase()); + + // select 태그의 값 가져오기 + selectElements.forEach((select) => { + const camelCaseId = toCamelCase(select.id); + bodyData[camelCaseId] = select.value; + }); + + // // input 태그의 값 가져오기 + inputElements.forEach((input) => { + const camelCaseId = toCamelCase(input.id); + bodyData[camelCaseId] = input.value; + }); + + // 레이어명에 따라 API 엔드포인트 분기처리 + let apiEndpoint = ''; + let result = null; + + if (layerNm === '하수관로') apiEndpoint = 'v1/fcltppl'; + else if (layerNm === '우수토실') apiEndpoint = 'v1/fcltstofcbr'; + else if (layerNm === '하수펌프장') apiEndpoint = 'v1/fcltpump'; + else if (layerNm === '하수맨홀') apiEndpoint = 'v1/fcltmhl'; + + bodyData['fcltMngNo'] = featureData.values_.fclt_mng_no.toString() || null; + bodyData['mngInstCd'] = featureData.values_.mng_inst_cd.toString() || null; + // 빈 키 제거 + if (bodyData[""] !== undefined) delete bodyData[""]; + + // 🎯 우수토실일 경우 prrtymngYn 값 변환 + if (layerNm === '우수토실' && bodyData.prrtymngYn) { + if (bodyData.prrtymngYn === '중점우수토실') { + bodyData.prrtymngYn = 'Y'; + } else if (bodyData.prrtymngYn === '우수토실') { + bodyData.prrtymngYn = 'N'; + } + } + + try { + result = await postData(apiEndpoint, bodyData); + + if (result) { + alert('저장되었습니다'); + setFetchData(result.data || result); + } else { + alert('저장에 실패했습니다'); + console.error('API 응답 오류:', result); + } + } catch (error) { + alert('저장 중 오류가 발생했습니다'); + console.error('API 요청 오류:', error); + } + } + } + + // 레이어명에 따라 상세페이지 렌더링 + const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null; + + return ( + + ) +} +export default FeatureClickEventPopup; diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/sewagePump.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/sewagePump.tsx new file mode 100644 index 000000000..07d78a7d1 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/sewagePump.tsx @@ -0,0 +1,69 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (하수펌프장) + * + * 📄 sewagePump + * 하수펌프장 정보 컴포넌트 + * + * - 빗물펌프장 및 오수펌프장 구분에 따른 자체 입력 정보 표시 및 편집 기능 + * - VerticalTable을 통한 입력 폼 렌더링 + * - 저장 버튼을 통한 데이터 저장 기능 + */ +import { VerticalTable } from '@/components/ui/verticalTable'; +import React, { useState, useEffect } from 'react' +// 빗물펌프장 자체 입력 정보 +const exclnInputColumn = [ + { id: "swtrmtZoneNm", title: "배수구역", type: "input" }, + { id: "mltvDtdvNm", title: "배수분구", type: "input" }, + { id: "designFrqYr", title: "설계빈도(년)", type: "input", placeholder: "예: 10", isNumber: true, length: 4 }, +] +// 오수펌프장 자체 입력 정보 +const drtwInputColumn = [ + { id: "swtrmtZoneNm", title: "처리구역", type: "input" }, + { id: "mltvDtdvNm", title: "처리분구", type: "input" }, +] + + +const sewagePump = ({ featureData, isDrtwSewagePump, onSaveClick }: { featureData: any, isDrtwSewagePump?: boolean, onSaveClick?: () => void }) => { + const [formData, setFormData] = useState({}); + + // featureData가 변경될 때 formData 초기화 + useEffect(() => { + if (featureData) { + setFormData(featureData); + } + }, [featureData]); + + // 폼 데이터가 변경될 때 호출될 핸들러 + const handleFormDataChange = (newFormData: any) => { + setFormData(newFormData); + }; + + // 저장 버튼 클릭 시 최신 formData를 부모에게 전달 + const handleSaveClick = () => { + if (onSaveClick) { + // 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달 + onSaveClick(); + } + }; + + return ( + + ) +} + +export default sewagePump diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/smh.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/smh.tsx new file mode 100644 index 000000000..2a0b11682 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/smh.tsx @@ -0,0 +1,146 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (하수맨홀) + * + * 📄 Smh + * 하수맨홀 정보 컴포넌트 + * + * - 하수맨홀의 자체입력정보 표시 및 편집 기능 + * - VerticalTable을 통한 입력 폼 렌더링 + * - 상류관로 검색 및 지도 포커스 기능 + * - 저장 버튼을 통한 데이터 저장 기능 + * + * 🔗 사용 API + * - 상류관로 조회: GET /api/v1/fcltmhl/upperPpls/{fcltMngNo} (sewerage-service) + */ +import { VerticalTable } from '@/components/ui/verticalTable'; +import { useEffect, useState } from 'react'; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; + +// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용) +export async function searchUpperPpls(gisMap: any, fcltMngNo: any) { + const BASE_URL = "/sewerage-service/api"; + const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, { + method: "GET", + }); + + let data: any = null; + if (res.ok) { + try { + // 응답이 비어있는지 확인 + const contentLength = res.headers.get("content-length"); + if (contentLength === "0") { + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + return; + } + + // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 + const text = await res.text(); + if (!text || text.trim() === "") { + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + return; + } + + // JSON 파싱 시도 + const response = JSON.parse(text); + data = response.data; + } catch (error) { + console.error("JSON parsing error:", error); + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + return; + } + } else { + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + return; + } + + const select: any = GisApp.Module.select; + let foundFeature: any = null; + + (data || []).forEach((item: any) => { + const pplMngNo = item.ppl_mng_no?.trim?.(); + + gisMap + .getLayers() + .getArray() + .filter((e: any) => e.get("lyrTyNm") == "tb_ms_swage_ppl") + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatures) { + const features = source.getFeatures(); + for (const f of features) { + if (f.values_.fclt_mng_no?.trim?.() === pplMngNo) { + foundFeature = f; + break; + } + } + } + }); + + if (foundFeature) select?.selectMultiFeature?.(foundFeature, true); + }); + + if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + + const geometry = foundFeature?.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + gisMap.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 14, + constrainResolution: true, + nearest: true, + }); + } +} + +const inputColumn = [ + { id: "swtrmtZoneNm", title: "처리구역", type: "input" }, + { id: "mltvZoneNm", title: "배수구역", type: "input" }, + { id: "mltvDtdvNm", title: "배수분구", type: "input" }, + { id: "layCd", title: "매설위치", type: "select", options: ["도로", "하상"] }, +]; +const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () => void }) => { + const [formData, setFormData] = useState({}); + + // featureData가 변경될 때 formData 초기화 + useEffect(() => { + if (featureData) { + setFormData(featureData); + } + }, [featureData]); + + // 폼 데이터가 변경될 때 호출될 핸들러 + const handleFormDataChange = (newFormData: any) => { + setFormData(newFormData); + }; + + // 저장 버튼 클릭 시 최신 formData를 부모에게 전달 + const handleSaveClick = () => { + if (onSaveClick) { + // 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달 + onSaveClick(); + } + }; + + return ( + + ) +} + +export default Smh; \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/components/popupComponent/stormWaterPump.tsx b/apps/sn/app/html/(map-layout)/map/components/popupComponent/stormWaterPump.tsx new file mode 100644 index 000000000..92973b133 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/components/popupComponent/stormWaterPump.tsx @@ -0,0 +1,37 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (빗물펌프) + * + * 📄 StormWaterPump + * 빗물펌프 정보 컴포넌트 + * + * - 빗물펌프의 자체입력정보 표시 및 편집 기능 + * - VerticalTable을 통한 입력 폼 렌더링 + */ +import { VerticalTable } from '@/components/ui/verticalTable'; +import React from 'react' +const inputColumn = [ + { id: "swtrmtZoneNm", title: "처리구역", type: "input" }, + { id: "mltvDtdvNm", title: "배수분구", type: "input" }, +] +// 빗물펌프 +const StormWaterPump = ({ featureData }: { featureData: Record }) => { + const validKey = inputColumn.map((item) => item.id); + const filteredData = featureData && Object.entries(featureData).filter(([key]) => validKey.includes(key)).reduce((obj: Record, [key, value]) => { obj[key] = value; return obj; }, {}); + return ( + + ) +} + +export default StormWaterPump \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/page.tsx b/apps/sn/app/html/(map-layout)/map/page.tsx new file mode 100644 index 000000000..7f48a9243 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/page.tsx @@ -0,0 +1,184 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 + * + * 📄 Map Page + * 지도 페이지 컴포넌트 + * + * - OpenLayers 기반 GIS 지도 렌더링 및 상호작용 + * - 레이어 트리 데이터 관리 및 동적 레이어 표시/숨김 + * - 피처 검색 및 선택 기능 (SearchModal, ProcSearchBar) + * - 피처 클릭 이벤트 처리 및 상세 정보 팝업 표시 + * - 수심(WaterDepth) 레이어 애니메이션 재생 기능 + * - Zustand를 통한 검색 모달 상태 관리 + * + * 🔗 사용 API + * - 레이어 설정 조회: GET /api/v1/map/lyrStngs/{lyrGrp} (sewerage-service) + */ + +"use client"; + +import Gis from "@common/business_components/ui/map/Gis"; +import { getData } from "../../../components/lib/api"; +import { useCallback, useEffect, useRef, useState } from "react"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +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"; + +const Map = () => { + const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore(); + + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const gisMaps = useRef(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부 + const [isPlaying, setIsPlaying] = useState(false); + + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { + try { + let data; + const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", { + method: "GET", + }); + if (res.ok) { + // 응답이 비어있는지 확인 + const contentLength = res.headers.get('content-length'); + if (contentLength === '0') data = null; + + // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 + const text = await res.text(); + if (!text || text.trim() === '') data = null; + + // JSON 파싱 시도 + data = JSON.parse(text); + } + setTreeData(data); + } catch (error) { + console.error('JSON parsing error:', error); + // JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환 + throw new Error('Invalid JSON response from server'); + } + }; + + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: any) => { + let map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature) { + let geometry = foundFeature.getGeometry(); + if (geometry) { + let extent = geometry.getExtent(); + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; + + const facilityTypes = [{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" }]; + // WaterDepth 표시 여부를 결정하는 함수 + const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + const waterDepthLayers = checkedNodesData.filter(node => + WATER_DEPTH_LAYER_TYPES.includes(node.data?.lyrTyNm) + ); + + const hasWaterDepthLayer = waterDepthLayers.length > 0; + setShowWaterDepth(hasWaterDepthLayer); + }, []); + + // 체크된 레이어 변경 처리 함수 + const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + handleWaterDepthVisibility(checkedIds, checkedNodesData); + }, [handleWaterDepthVisibility]); + + + return ( + <> +
+ } + waterDepth={showWaterDepth ? WaterDepth : undefined} + isPlaying={isPlaying} + setIsPlaying={setIsPlaying as any} + targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any} + checkInterval={2500} + showZoomLevel={true} + + defaultZoom={7.5} + defaultLonLat={[127.75, 36]} + /> +
+ + ); +}; + +export default Map; diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/basicTab.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/basicTab.tsx new file mode 100644 index 000000000..c21dc8ca4 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/basicTab.tsx @@ -0,0 +1,66 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 + * + * 📄 BasicTab Component + * Next.js App Router (app/) 기반 검색 모달의 기본정보 탭 컴포넌트 + * + * - 기본정보 검색을 위한 구분 선택 및 조건 입력 UI 제공 + * - 하수관로, 하수처리시설, 하수처리구역, 하수맨홀, 하수펌프장, 우수토실 등 다양한 유형별 컴포넌트 렌더링 + * - useSearchModalStore를 통해 상태 관리 및 사용자 상호작용 처리 + */ +import React from 'react'; +import { SelectBox } from '@common/business_components/ui'; +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 useSearchModalStore from '@/store/useSearchModalStore'; + + +// 기본정보 '구분' 선택 목록 +export const BASIC_TYPES = [ + { val: "tb_ms_swage_ppl", text: "하수관로" }, + { val: "tb_il_swage_prcs_fclt", text: "하수처리시설" }, + { val: "tb_ms_swage_prcs_zone", text: "하수처리구역" }, + { val: "tb_ms_swage_mhl", text: "하수맨홀" }, + { val: "tb_ms_swage_pmstn", text: "하수펌프장" }, + { val: "tb_ms_stofcbr", text: "우수토실" }, +]; + +const BasicTab: React.FC = () => { + const { basicType, setBasicType } = useSearchModalStore(); + + return ( +
+ {/* 구분 */} +
+
+ + + setBasicType(String(value)) + } + value={basicType} + selectContentClass="z-[101]" + /> +
+
+ {/* 조회 조건 */} + {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" && ()} {/* 우수토실 */} +
+ ); +}; + +export default React.memo(BasicTab); \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/operationTab.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/operationTab.tsx new file mode 100644 index 000000000..63c18875d --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/operationTab.tsx @@ -0,0 +1,98 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 유지관리 정보 탭 + * + * 📄 Operation Tab + * Next.js App Router (app/) 기반 검색 모달 유지관리 정보 탭 컴포넌트 + * + * - 업무구분 선택 (보수 및 갱신, 점검 및 진단, 청소 및 준설) + * - 사업명 입력 및 수행기간 범위 선택 + * - 사업 유형 필터링 기능 + * - 조회 버튼을 통한 데이터 검색 트리거 + * - Zustand를 통한 상태 관리 및 필터 값 저장 + * - Enter 키 입력 시 자동 조회 지원 + */ +import React 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 from './useSearchModal'; +import useOperationTab from './useOperationTab'; + + +const OperationTab: React.FC = () => { + const { BIZ_TY_CD } = useStore(); + const { + workType, setWorkType, + var1, setVar1, var2, setVar2, + yearFilter, setYearFilter, + } = useSearchModalStore(); + const { handleSearch } = useSearchModal(); + const { WORK_TYPES } = useOperationTab(); + + const BIZ_TY_CD_SELECT_ARRAY = BIZ_TY_CD.map(item => ({ + val: item.lwpoCd, + text: item.lwpoCdNm, + })); + + return ( +
+
+ + ({ + val: w.val, + text: w.text, + }))} + onSelectValue={(val) => setWorkType(String(val))} + value={workType} + selectContentClass="z-[101]" + /> +
+
+ + { setVar1(e.target.value); }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="수행기간" + endLabel="" + /> + + { + setVar2(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ) +}; + +export default React.memo(OperationTab); \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/reportFilesModal.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/reportFilesModal.tsx new file mode 100644 index 000000000..2b7218332 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/reportFilesModal.tsx @@ -0,0 +1,108 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 하수도정비기본계획보고서 탭 (셀 클릭) + * + * 📄 Report Files Modal + * Next.js App Router (app/) 기반 보고서 파일 모달 컴포넌트 + * + * - 하수도정비 기본계획 보고서 파일 목록 표시 + * - 파일명 및 등록일자 정보 테이블 출력 + * - 페이지네이션 및 로딩 오버레이 지원 + * - 파일 클릭 이벤트 처리 및 상세 조회 + * - Zustand를 통한 모달 상태 및 데이터 관리 + * - 모달 열림/닫힘 시 데이터 초기화 기능 + */ +'use client' +import React, { useEffect } 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/html/(map-layout)/map/searchModal/reportTab.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/reportTab.tsx new file mode 100644 index 000000000..ad57c0a5f --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/reportTab.tsx @@ -0,0 +1,272 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 하수도정비기본계획보고서 탭 + * + * 📄 Report Tab + * Next.js App Router (app/) 기반 검색 모달 보고서 탭 컴포넌트 + * + * - 하수도정비 기본계획 보고서 목록 표시 및 필터링 + * - 시도/시군구/보고서 구분/승인년도 기반 검색 기능 + * - 테이블 데이터 표시 및 페이지네이션 지원 + * - 보고서 클릭 시 파일 목록 모달 열기 + * - Zustand를 통한 모달 상태 및 데이터 관리 + * - 로딩 오버레이 및 선택 초기화 기능 + */ +'use client'; +import React, { useEffect, useState } from 'react'; +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 useReportTab from './useReportTab'; +import useStore from '@/store/useStore'; +import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; +import useSearchModalStore from '@/store/useSearchModalStore'; +import { useReportTabStore } from '@/store/useReportTabStore'; +import ReportFilesModal from './reportFilesModal'; // 파일목록모달 + + +// 하수도정비 기본계획 보고서 +export const reportTableHeadlist = [ + { id: "no", title: "NO" }, + { id: "sido", title: "시도" }, + { id: "sigungu", title: "시군구" }, + { id: "approvalYear", title: "승인년도" }, + { id: "plantZoneGubun", title: "구분" }, + { id: "reportGubun", title: "보고서구분" }, + { id: "fileName", title: "파일명" }, + { id: "regDate", title: "등록일자" }, +]; +interface ReportTabProps { + open?: boolean; + isSearch?: boolean; + ctpvNm?: string; + sggNm?: string; +} + +const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sggNm }) => { + + // const [isLoading, setIsLoading] = useState(false); + const [isResetPage, setIsResetPage] = useState(false); + const [isResetSelection, setIsResetSelection] = useState(false); + const [showTable, setShowTable] = useState(false); + // const [tableData, setTableData] = useState([]); + // const [tableCount, setTableCount] = useState(0); + // const [curPage, setCurPage] = useState(1); + // const [yearFilter, setYearFilter] = useState({ start: "", end: "" }); + const [ctpvCd, setCtpvCd] = useState('0'); + const [sggCd, setSggCd] = useState('0'); + const [reportGubun, setReportGubun] = useState("0"); + const [clsfSystmSn, setClsfSystmSn] = useState(''); // 자료관 기본계획 일련번호 + + const { + tableData, + tableCount, + isLoading, + yearFilter, + curPage, + setTableData, + setTableCount, + setIsLoading, + setYearFilter, + setCurPage + } = useSearchModalStore(); + const { ctpv, sgg } = useStore(); + const { + isOpen: isReportFilesModalOpen, + setIsOpen: setIsReportFilesModalOpen, + dcTree: reportOriginData, + fetchDcTree + } = useReportTabStore(); // 모달 상태 관리 스토어 + const { REPORT_TYPES, flattenFolders, addFileName } = useReportTab(); + + useEffect(() => { + fetchDcTree("기본계획"); // 하수정비기본계획 report + }, []); + + useEffect(() => { + // filterParams 초기화 + setCtpvCd('0'); + setSggCd('0'); + setReportGubun('0'); + setYearFilter({ start: "", end: "" }); + setShowTable(false); + setTableData([]); + setTableCount(0); + setCurPage(1); // 페이지를 1로 초기화 + setIsResetPage(true); // 페이지를 초기화 + setIsResetSelection(true); // 선택 초기화 + }, [open]); + + useEffect(() => { + const newCtpvCd = ctpv.find((n) => n.text === ctpvNm)?.val || '0'; + const newSggCd = sgg.find((n) => n.text === sggNm)?.val || '0'; + + setCtpvCd(newCtpvCd); + setSggCd(newSggCd); + }, [ctpvNm, sggNm]); + + // ctpvCd, sggCd가 업데이트된 후에 fetchFilteredData 실행 + useEffect(() => { + if (!isSearch && ctpvCd && sggCd) { + fetchFilteredData(); + } + }, [reportOriginData, ctpvCd, sggCd, isSearch]); + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + // showTable이 true일 때만 fetch (최초 렌더 방지) + setIsResetPage(false); + + if (showTable) fetchFilteredData(); + }, [curPage]); + + const handleCtpvCdChange = (sidoId: string) => { + setCtpvCd(sidoId); + setSggCd('0'); + } + + // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 + const handleSearch = () => { + setCurPage(1); // 페이지를 1로 초기화 + fetchFilteredData(); + setIsResetPage(true); // 페이지를 초기화 + setIsResetSelection(false); // 선택 초기화 + setShowTable(true); + }; + + // 하수도정비기본계획 테이블 cellClick 핸들러 + const handleReportCellClick = (index: number | undefined) => { + if (typeof index === "number" && tableData[index]) { + const reportData = tableData[index]; + if (reportData.fileSn !== "") { + setClsfSystmSn(reportData.clsfSystmSn); // 자료관 기본계획 일련번호 설정 + setIsReportFilesModalOpen(true); // 모달 열기 + } + } + }; + + // 검색 API 호출 함수 + const fetchFilteredData = async () => { + setIsLoading(true); // 로딩 시작 + setIsResetSelection(true); // 선택 초기화 + + try { + const reportData = flattenFolders(reportOriginData); + let filterReportData = reportData; + + if (ctpvCd !== '0') { // 시도검색 + const sidoOption = ctpv.find((n) => n.val.trim() === ctpvCd.trim()); + filterReportData = filterReportData.filter(item => item.sido === sidoOption?.text); + } + if (sggCd !== '0') { // 시군구검색 + const sigunguOption = sgg.find((n) => String(n.val) === String(sggCd)); + filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.text); + } + if (reportGubun !== "0") { // 보고서구분검색 + const reportGubunOption = REPORT_TYPES.find((n) => n.val === reportGubun); + filterReportData = filterReportData.filter(item => item.reportGubun === reportGubunOption?.text); + } + + const { start, end } = yearFilter; + if (start && !end) //승인년도검색 + filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" : null); + else if (!start && end) + filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear <= yearFilter.end + "년" : null); + else if (start && end) { + filterReportData = filterReportData.filter(item => { + return (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" && item.approvalYear <= yearFilter.end + "년" : null; + }); + } + // if (gubun !== "") { // 구분검색 (추후 사용할 수도 있을 것 같아 남겨둠) + // filterReportData = filterReportData.filter(item => (item.plantZoneGubun) ? String(item.plantZoneGubun).includes(String(gubun)) : null); + // } + + setTableCount(filterReportData.length || 0); + + filterReportData = filterReportData.slice((curPage - 1) * 10, curPage * 10); // paging + filterReportData = filterReportData?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 })); + + addFileName(filterReportData).then(data => { + filterReportData = data; + setTableData(filterReportData || []); + setIsResetSelection(false); // 데이터 설정 후 선택 초기화 해제 + }); + + setTableData(filterReportData || []); + setShowTable(true); + } catch (err) { + console.error('API ERROR:', err); + setTableData([]); + setIsResetSelection(false); // 에러 시에도 선택 초기화 해제 + } finally { + setIsLoading(false); // 로딩 종료 + } + }; + + + return ( + <> +
+ {isSearch && ( + <> +
+ { handleCtpvCdChange(sidoId) }} + onSigunguChange={setSggCd} + setIsLoading={setIsLoading} + /> +
+ + )} +
+ + setReportGubun(String(val))} + selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2' + /> + {/* 승인년도 필터 */} + setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="승인년도" endLabel="" + /> + {/* 조회 버튼 */} +
+ +
+
+
+ +
+ {/* 테이블2 영역 (가로/세로 스크롤 가능) */} +
+ + { + return (<>조회 결과); + })() + } + tableData={tableData} + headlist={reportTableHeadlist} + pageSize={10} + total={tableCount} + cellClick={handleReportCellClick} + onPageChange={setCurPage} + resetPageNo={isResetPage} + resetSelection={isResetSelection} + /> +
+
+ {/* 모달 컴포넌트 */} + {isReportFilesModalOpen && } + + ); +}; + +export default React.memo(ReportTab); \ No newline at end of file diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/searchModal.tsx new file mode 100644 index 000000000..3ce334518 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/searchModal.tsx @@ -0,0 +1,238 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 + * + * 📄 Search Modal + * Next.js App Router (app/) 기반 검색 모달 컴포넌트 + * + * - 탭 기반 검색 인터페이스 (기본정보, 유지관리 정보, 보고서) + * - 테이블 데이터 표시 및 페이지네이션 기능 + * - 시설물 조회 결과 및 유지관리 사업 현황 표시 + * - 엑셀 다운로드 및 지도에서 보기 기능 + * - Zustand를 통한 검색 모달 상태 관리 + * - 로딩 오버레이 및 선택 초기화 지원 + */ +import LoadingOverlay from '@/components/ui/LoadingBar'; +import { UiModal, UITab, UiTable } from '@common/business_components/ui'; +import { Button } from '@common/components/ui'; +import React, { useEffect, useMemo } from 'react'; +import { Toaster } from "react-hot-toast"; +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 = () => { + const { + showTable, + isSearchModalOpen, setIsSearchModalOpen, + activeTab, basicType, workType, + tableData, detailTableData, + tableCount, detailTableCount, + curPage, detailCurPage, + detailBizId, + isLoading, isDetailLoading, + isResetPage, isResetDetailPage, + isResetSelection, isResetDetailSelection, + setActiveTab, + setCurPage, setDetailCurPage, + setIsResetPage, setIsResetDetailPage, + setDetailBizId, + setIsResetDetailSelection, + resetFilters + } = useSearchModalStore(); + const { TABS, viewMultipleFeature, fetchFilteredData } = 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, + handleOperationPageNation + } = useOperationTab(); + + + const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => { + if (activeTab === 'basic') { // 기본정보 + if (basicType === 'tb_ms_swage_ppl') return MsSwagePplCol2;// 하수관로 + if (basicType === 'tb_il_swage_prcs_fclt') return SwtrmtFcltCol2;// 하수처리시설 + if (basicType === 'tb_ms_swage_prcs_zone') return SwtrmtPrcsZoneCol2; // 하수처리구역 + if (basicType === 'tb_ms_swage_mhl') return MsSwageMhlCol2; // 하수맨홀 + if (basicType === 'tb_ms_swage_pmstn') return ExclnSewagePumpCol2; // 하수펌프장 + if (basicType === 'tb_ms_stofcbr') return stofcbrCol2; // 우수토실 + + return []; + } + 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 MAINTENANACE_TABLE_HEADLIST; // 보수 및 갱신 + if (workType === WORK_TYPES[1]?.val) return CLEANING_TABLE_HEADLIST; // 점검 및 진단 + if (workType === WORK_TYPES[2]?.val) return INSPECTION_TABLE_HEADLIST; // 청소 및 준설 + } + + return []; + }, [activeTab, workType]); + + 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]); + + + const renderBody = () => ( + <> + {/* 탭 영역 */} + ({ + value: tab.val, + label: tab.text + }))} + onValueChange={(value: string) => setActiveTab(value)} + /> + {activeTab === 'basic' && ()} {/* 기본정보 탭 */} + {activeTab === 'operation' && ()} {/* 유지관리 정보 탭 */} + {activeTab === 'report' && ()} {/* 하수도정비기본계획보고서 탭 */} + + + {/* 📋 결과 리스트 */} + {/* 기본정보 탭 */} + {activeTab === 'basic' && ( +
+ + { + const label = BASIC_TYPES.find((type) => type.val === basicType)?.text + return (<>{label} 조회 결과); + })() + } + tableData={tableData} + headlist={dynamicHeadlist} + pageSize={10} + total={tableCount} + cellClick={handleBasicCellClick} + onPageChange={setCurPage} + resetPageNo={isResetPage} + resetSelection={isResetSelection} + curPage={curPage} + headButton={ +
+ + +
+ } + /> +
+ )} + {/* 유지관리 정보 탭 */} + {activeTab === 'operation' && ( + <> +
+ {/*
*/} + + { + const label = WORK_TYPES.find((type) => type.val === workType)?.text + return (<>{label} 사업 현황); + })() + } + tableData={tableData} + headlist={dynamicHeadlist} + pageSize={OPERATION_TABLE_SIZE} + total={tableCount} + cellClick={handleOperationCellClick} + onPageChange={setCurPage} + resetPageNo={isResetPage} + resetSelection={isResetSelection} + curPage={curPage} + headButton={ + + } + /> + {/*
*/} +
+ +
+ {/* 테이블2 영역 */} + {/*
*/} + + { + const label = WORK_TYPES.find((type) => type.val === workType)?.text + return (<>{label} 결과표); + })() + } + tableData={detailTableData} + headlist={dynamicHeadlist2} + pageSize={OPERATION_TABLE_SIZE} + total={detailTableCount} + cellClick={handleOperationResultCellClick} + onPageChange={setDetailCurPage} + resetPageNo={isResetDetailPage} + resetSelection={isResetDetailSelection} + curPage={detailCurPage} + headButton={ +
+ + +
+ } + /> + {/*
*/} +
+ + )} + + ); + + return ( +
+ + +
+ ); +}; + +export default React.memo(SearchModal); diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/stofcbr.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/stofcbr.tsx new file mode 100644 index 000000000..102b21d64 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/stofcbr.tsx @@ -0,0 +1,92 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 우수토실) + * + * 📄 Stofcbr Tab + * Next.js App Router (app/) 기반 검색 모달 우수토실 탭 컴포넌트 + * + * - 시도/시군구 선택 및 설치년도 범위 필터링 + * - CQL 필터 생성을 통한 쿼리 조건 설정 + * - 조회 버튼을 통한 데이터 검색 트리거 + * - Zustand를 통한 상태 관리 및 필터 값 저장 + * - Enter 키 입력 시 자동 조회 지원 + */ +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { Button } from '@common/components'; +import React from 'react'; +import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; +import useSearchModalStore from "@/store/useSearchModalStore"; +import useSearchModal from './useSearchModal'; + + +export const stofcbrCqlFilters = () => { + const { + ctpvCd, sggCd, + yearFilter, + todayYear, todayMonth, todayDate, + getDateString + } = useSearchModalStore.getState(); + + const cqlFilters: string[] = []; + // 시도, 시군구 + if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`); + else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`); + // 설치년도 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, + ); + + return cqlFilters; +}; + +const Stofcbr: React.FC = () => { + const { + ctpvCd, setCtpvCd, + sggCd, setSggCd, + yearFilter, setYearFilter, + } = useSearchModalStore(); + const { handleSearch } = useSearchModal(); + + + return ( +
+
+ {/* 시도, 시군구 */} + { + setCtpvCd(sidoId); + setSggCd("0"); + }} + onSigunguChange={setSggCd} + /> + {/* 설치년도 */} + setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="설치년도" + endLabel="" + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +export default React.memo(Stofcbr); diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/swageMhl.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/swageMhl.tsx new file mode 100644 index 000000000..266012dff --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/swageMhl.tsx @@ -0,0 +1,156 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수맨홀) + * + * 📄 SwageMhl Tab + * Next.js App Router (app/) 기반 검색 모달 하수맨홀 탭 컴포넌트 + * + * - 시도/시군구 선택 및 설치년도 범위 필터링 + * - 용도 및 구경 범위 기반 검색 기능 + * - CQL 필터 생성을 통한 쿼리 조건 설정 + * - 조회 버튼을 통한 데이터 검색 트리거 + * - Zustand를 통한 상태 관리 및 필터 값 저장 + * - Enter 키 입력 시 자동 조회 지원 + */ +import React from 'react'; +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { Button, Input, Label } from '@common/components'; +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 from './useSearchModal'; + + +export const swageMhlCqlFilters = () => { + const { + ctpvCd, sggCd, + yearFilter, + start1, end1, + var1, + todayYear, todayMonth, todayDate, + getDateString + } = useSearchModalStore.getState(); + + const cqlFilters: string[] = []; + // 시도, 시군구 + if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`); + else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`); + // // 설치년도 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, + ); + if (var1) cqlFilters.push(`usg_cd='${var1}'`); + // 구경(mm) 범위 + if (start1 && end1) cqlFilters.push(`swage_mhL_diam_vl BETWEEN ${start1} AND ${end1}`); + else if (start1) cqlFilters.push(`swage_mhL_diam_vl >= ${start1}`); + else if (end1) cqlFilters.push(`swage_mhL_diam_vl <= ${end1}`); + + return cqlFilters; +}; + +const SwageMhl: React.FC = () => { + const { + var1, setVar1, + start1, setStart1, end1, setEnd1, + ctpvCd, setCtpvCd, + sggCd, setSggCd, + yearFilter, setYearFilter, + } = useSearchModalStore(); + const { handleSearch } = useSearchModal(); + + return ( +
+
+ { + setCtpvCd(sidoId); + setSggCd("0"); + }} + onSigunguChange={setSggCd} + /> + {/* 설치년도 */} + + setYearFilter({ + ...yearFilter, + start: range.start, + end: range.end, + }) + } + startLabel="설치년도" + endLabel="" + /> + {/* 용도 */} + + { + setVar1(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 하수맨홀 구경 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +export default React.memo(SwageMhl); diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/swagePmstn.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/swagePmstn.tsx new file mode 100644 index 000000000..3512b4fa3 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/swagePmstn.tsx @@ -0,0 +1,175 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수펌프장) + * + * 📄 SwagePmstn Tab + * Next.js App Router (app/) 기반 검색 모달 하수펌프장 탭 컴포넌트 + * + * - 시도/시군구 선택 및 설치년도 범위 필터링 + * - 일일처리용량 및 양수능력 범위 기반 검색 기능 + * - CQL 필터 생성을 통한 쿼리 조건 설정 + * - 조회 버튼을 통한 데이터 검색 트리거 + * - Zustand를 통한 상태 관리 및 필터 값 저장 + * - Enter 키 입력 시 자동 조회 지원 + */ +import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; +import { Button, Input, Label } from '@common/components'; +import React from 'react'; +import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; +import useSearchModalStore from "@/store/useSearchModalStore"; +import useSearchModal from './useSearchModal'; + + +export const swagePmstnCqlFilters = () => { + const { + ctpvCd, sggCd, + yearFilter, + start1, start2, end1, end2, + todayYear, todayMonth, todayDate, + getDateString + } = useSearchModalStore.getState(); + + const cqlFilters: string[] = []; + // 시도, 시군구 + if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`); + else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`); + // 설치년도 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, + ); + // 일일처리용량 범위 + if (start1 && end1) + cqlFilters.push(`daily_prcs_cpct BETWEEN ${start1} AND ${end1}`); + else if (start1) cqlFilters.push(`daily_prcs_cpct >= ${start1}`); + else if (end1) cqlFilters.push(`daily_prcs_cpct <= ${end1}`); + // 양수능력 범위 + if (start2 && end2) + cqlFilters.push(`excln_wpmp_ablt_vl BETWEEN ${start2} AND ${end2}`); + else if (start2) cqlFilters.push(`excln_wpmp_ablt_vl >= ${start2}`); + else if (end2) cqlFilters.push(`excln_wpmp_ablt_vl <= ${end2}`); + + return cqlFilters; +}; + +const SwagePmstn: React.FC = () => { + const { + start1, setStart1, + end1, setEnd1, + ctpvCd, setCtpvCd, + sggCd, setSggCd, + yearFilter, setYearFilter, + } = useSearchModalStore(); + const { handleSearch } = useSearchModal(); + + return ( +
+
+ {/* 시도, 시군구 */} + { + setCtpvCd(sidoId); + setSggCd("0"); + }} + onSigunguChange={setSggCd} + /> + {/* 설치년도 */} + setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="설치년도" + endLabel="" + /> + {/* 일일처리용량 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + {/* 양수능력 */} + {/* + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart2(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd2(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> */} + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +export default React.memo(SwagePmstn); diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/swagePpl.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/swagePpl.tsx new file mode 100644 index 000000000..5bc8f900b --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/swagePpl.tsx @@ -0,0 +1,209 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수관로) + * + * 📄 SwagePpl Tab + * Next.js App Router (app/) 기반 검색 모달 하수관로 탭 컴포넌트 + * + * - 시도/시군구 선택 및 설치년도 범위 필터링 + * - 용도 및 관경 범위 기반 검색 기능 + * - CQL 필터 생성을 통한 쿼리 조건 설정 + * - 조회 버튼을 통한 데이터 검색 트리거 + * - Zustand를 통한 상태 관리 및 필터 값 저장 + * - Enter 키 입력 시 자동 조회 지원 + */ +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_USE_CODES } from '../components/common/table'; +import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; +import useSearchModalStore from "@/store/useSearchModalStore"; +import useSearchModal from './useSearchModal'; + + +// 하수관로용 CQL 필터 생성 +export const swagePplCqlFilters = () => { + const { + var1, var2, + ctpvCd, sggCd, + yearFilter, + start1, start2, end1, end2, + todayYear, todayMonth, todayDate, + getDateString + } = useSearchModalStore.getState(); + + const cqlFilters: string[] = []; + if (var1) cqlFilters.push(`usg_cd='${var1}'`); + if (var2) cqlFilters.push(`mqlt_cd='${var2}'`); + if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`); + else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`); + // 설치년도 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`, + ); + // 관경(mm) 범위 + if (start1 && end1) cqlFilters.push(`pdmt_vl BETWEEN ${start1} AND ${end1}`); + else if (start1) cqlFilters.push(`pdmt_vl >= ${start1}`); + else if (end1) cqlFilters.push(`pdmt_vl <= ${end1}`); + // 연장(m) 범위 + if (start2 && end2) cqlFilters.push(`prlg_vl BETWEEN ${start2} AND ${end2}`); + else if (start2) cqlFilters.push(`prlg_vl >= ${start2}`); + else if (end2) cqlFilters.push(`prlg_vl <= ${end2}`); + + return cqlFilters; +}; + +const SwagePpl: React.FC = () => { + const { + var1, setVar1, + start1, setStart1, end1, setEnd1, + ctpvCd, setCtpvCd, sggCd, setSggCd, + yearFilter, setYearFilter, + } = useSearchModalStore(); + const { handleSearch } = useSearchModal(); + + + return ( +
+
+ { + setCtpvCd(sidoId); + setSggCd("0"); + }} + onSigunguChange={setSggCd} + /> + {/* 설치년도 */} + + setYearFilter({ + ...yearFilter, + start: range.start, + end: range.end, + }) + } + startLabel="설치년도" + endLabel="" + /> + {/* 용도 */} + + { + setVar1(String(val)); + }} + selectContentClass="z-[101]" + /> + {/* 재질 */} + {/* + { + setVar2(String(val)); + }} + selectContentClass="z-[101]" + /> */} + {/* 관경 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + {/* 연장 */} + {/* + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart2(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd2(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> */} + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +export default React.memo(SwagePpl); diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/swagePrcsFclt.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/swagePrcsFclt.tsx new file mode 100644 index 000000000..c541a4c2f --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/swagePrcsFclt.tsx @@ -0,0 +1,150 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수처리시설) + * + * 📄 SwagePrcsFclt Tab + * Next.js App Router (app/) 기반 검색 모달 하수처리시설 탭 컴포넌트 + * + * - 시도/시군구 선택 및 가동개시일자 범위 필터링 + * - 시설용량 범위 기반 검색 기능 + * - CQL 필터 생성을 통한 쿼리 조건 설정 + * - 조회 버튼을 통한 데이터 검색 트리거 + * - Zustand를 통한 상태 관리 및 필터 값 저장 + * - Enter 키 입력 시 자동 조회 지원 + */ +import React from 'react'; +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 from './useSearchModal'; + + +// 하수처리시설용 CQL 필터 생성 +export const swagePrcsFcltCqlFilters = () => { + const { + ctpvName, sggName, + yearFilter, + start1, end1, + todayYear, todayMonth, todayDate, + getDateString + } = useSearchModalStore.getState(); + + const cqlFilters: string[] = []; + // 가동개시일자 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + cqlFilters.push( + `oprtng_strt_ymd >= '${startDate}' and oprtng_strt_ymd <= '${endDate}'`, + ); + // 시설용량 범위 + if (start1 && end1) + cqlFilters.push(`fclt_cpct BETWEEN ${start1} AND ${end1}`); + else if (start1) cqlFilters.push(`fclt_cpct >= ${start1}`); + else if (end1) cqlFilters.push(`fclt_cpct <= ${end1}`); + + // 시도 + if (ctpvName) cqlFilters.push(`ctpv_nm ILIKE '%${ctpvName}%'`); + if (sggName) cqlFilters.push(`sgg_nm ILIKE '%${sggName}%'`); + // // 처리구역 면적 + // if (start2 && end2) + // cqlFilters.push(`prcsZoneAreaSum BETWEEN ${start2} AND ${end2}`); + // else if (start2) cqlFilters.push(`prcsZoneAreaSum >= ${start2}`); + // else if (end2) cqlFilters.push(`prcsZoneAreaSum <= ${end2}`); + // // 하수처리시설명 + // if (var1) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${var1}%'`); + return cqlFilters; +}; + +const SwagePrcsFclt: React.FC = () => { + const { + start1, setStart1, end1, setEnd1, + ctpvCd, setCtpvCd, setCtpvName, + sggCd, setSggCd, setSggName, + yearFilter, setYearFilter, + } = useSearchModalStore(); + const { handleSearch } = useSearchModal(); + + return ( +
+
+ { + setCtpvCd(sidoId); + setSggCd("0"); + }} + onSigunguChange={setSggCd} + setCtpvName={setCtpvName} + setSggName={setSggName} + /> + {/* 가동개시일자 */} + + + setYearFilter({ + ...yearFilter, + start: range.start, + end: range.end, + }) + } + startLabel="" + endLabel="" + /> + {/* 시설용량 */} + + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setStart1(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + ~ + { + // 숫자만 입력 허용 + const val = e.target.value.replace(/[^0-9]/g, ""); + setEnd1(val); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +export default React.memo(SwagePrcsFclt); diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/swagePrcsZone.tsx b/apps/sn/app/html/(map-layout)/map/searchModal/swagePrcsZone.tsx new file mode 100644 index 000000000..e433662d1 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/swagePrcsZone.tsx @@ -0,0 +1,93 @@ +/** + * ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수처리구역) + * + * 📄 SwagePrcsZone Tab + * Next.js App Router (app/) 기반 검색 모달 하수처리구역 탭 컴포넌트 + * + * - 시도/시군구 선택 및 하수처리시설명/구역명 기반 검색 기능 + * - CQL 필터 생성을 통한 쿼리 조건 설정 + * - 조회 버튼을 통한 데이터 검색 트리거 + * - Zustand를 통한 상태 관리 및 필터 값 저장 + * - Enter 키 입력 시 자동 조회 지원 + */ +import React from 'react'; +import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; +import { Button, Input, Label } from '@common/components'; +import useSearchModalStore from "@/store/useSearchModalStore"; +import useSearchModal from './useSearchModal'; + + +// 하수처리구역용 CQL 필터 생성 +export const swagePrcsZoneCqlFilters = () => { + const { + ctpvCd, sggCd, + var1, var2 + } = useSearchModalStore.getState(); + const cqlFilters: string[] = []; + if (ctpvCd !== '' && ctpvCd !== '0') cqlFilters.push(`ctpv_cd='${ctpvCd?.trim()}'`); + if (sggCd !== '' && sggCd !== '0') cqlFilters.push(`sgg_cd='${sggCd}'`); + if (var1) cqlFilters.push(`swtrmt_zone_nm ILIKE '%${var1}%'`); + if (var2) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${var2}%'`); + return cqlFilters; +}; + +const swagePrcsZone: React.FC = () => { + const { + var1, setVar1, var2, setVar2, + ctpvCd, setCtpvCd, + sggCd, setSggCd, + } = useSearchModalStore(); + const { handleSearch } = useSearchModal(); + + return ( +
+
+ { + setCtpvCd(sidoId); + setSggCd("0"); + }} + onSigunguChange={setSggCd} + /> + + { + setVar2(e.target.value); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + + { + setVar1(e.target.value); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); // Enter 키 기본 동작 방지 + handleSearch(); + } + }} + /> + {/* 조회 버튼 */} +
+ +
+
+
+ ); +}; + +export default React.memo(swagePrcsZone); diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/useBasicTab.ts b/apps/sn/app/html/(map-layout)/map/searchModal/useBasicTab.ts new file mode 100644 index 000000000..9184286ed --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/useBasicTab.ts @@ -0,0 +1,348 @@ +/** + * 📄 useBasicTab Hook + * 검색 모달 기본정보 탭 훅 + * + * - 필터링된 시설물 데이터 조회 및 포맷팅 + * - 테이블 셀 클릭 시 지도 피처 선택 및 줌 기능 + * - 엑셀 다운로드 기능 (한글 헤더 지원) + * - CQL 필터 생성을 통한 쿼리 조건 설정 + * - Zustand를 통한 상태 관리 및 로딩 처리 + * - 데이터 매핑 및 코드 변환 (시도/시군구, 용도 등) + * + * 🔗 사용 API + * - 관망도 시설물 목록 조회: GET /api/v1/fclts (sewerage-service) + */ +import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui'; +import { + MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_SHAPE, 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 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 matchedMqltCode = basicType === 'tb_ms_swage_ppl' + ? MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim()) // 하수관로 재질 + : basicType === 'tb_ms_swage_mhl' + ? MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim()) // 맨홀 재질 + : undefined; + const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd); // 맨홀 용도 + const matchedPplShpCode = MS_SWAGE_PPL_SHAPE.find(code => code.val === item?.fcltyShpCd); // 하수관로 시설물형태코드 + + + 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: matchedMqltCode ? matchedMqltCode.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 || '-', + fcltyShpCd: matchedPplShpCode? matchedPplShpCode.text : item?.fcltyShpCd, + }; + }); + + setTableData(dataWithRowNumber); + // console.log(tableData); + 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:Q1"; + 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 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 pplMqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val?.trim(), code.text])); + const pplShpCdMap = new Map(MS_SWAGE_PPL_SHAPE.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 || []).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 ?? '-', + vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-', + wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-', + fcltyShpCd: pplShpCdMap.get(item?.fcltyShpCd?.trim()) || item.fcltyShpCd || formattedItem.fcltyShpCd || '-', + mqltCd: pplMqltCdMap.get(item?.mqltCd?.trim()) || item.mqltCd || formattedItem.mqltCd || '-', + }; + } + 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 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); + } + } + }; + + + return { fetchBasicFilteredData, handleBasicCellClick, basicExcelDownload }; +}; diff --git a/apps/sn/app/html/(map-layout)/map/searchModal/useOperationTab.ts b/apps/sn/app/html/(map-layout)/map/searchModal/useOperationTab.ts new file mode 100644 index 000000000..669dd4cb5 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/useOperationTab.ts @@ -0,0 +1,452 @@ +/** + * 📄 useOperationTab Hook + * 검색 모달 유지관리 정보 탭 훅 + * + * - 필터링된 사업 현황 데이터 조회 및 포맷팅 + * - 사업 현황 테이블 셀 클릭 시 상세 결과표 조회 + * - 결과표 셀 클릭 시 지도 피처 선택 및 줌 기능 + * - 엑셀 다운로드 기능 (사업 현황 및 결과표) + * - CQL 필터 생성을 통한 쿼리 조건 설정 + * - Zustand를 통한 상태 관리 및 로딩 처리 + * - 데이터 매핑 및 코드 변환 (사업 유형, 용도, 재질 등) + * + * 🔗 사용 API + * - 유지관리 정보 목록 조회: GET /api/v1/bizInfo (sewerage-service) + * - 유지관리 정보(사업정보) 관련 시설 목록 조회: GET /api/v1/bizInfo/fclts (sewerage-service) + */ +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: '100', + bizId: String(detailBizId), + }; + + const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); + + // 헤더 매핑 정의 + 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/html/(map-layout)/map/searchModal/useReportTab.ts b/apps/sn/app/html/(map-layout)/map/searchModal/useReportTab.ts new file mode 100644 index 000000000..8ffe06de8 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/useReportTab.ts @@ -0,0 +1,188 @@ +/** + * 📄 useReportTab Hook + * 검색 모달 보고서 탭 훅 + * + * - 계층적 폴더 구조 평탄화 및 데이터 변환 + * - 파일 목록 조회 및 페이징 처리 + * - 파일 다운로드 기능 + * - 셀 클릭 이벤트 처리 + * - Zustand를 통한 상태 관리 및 로딩 처리 + * - 데이터 매핑 및 코드 변환 (시도/시군구, 보고서 유형 등) + * + * 🔗 사용 API + * - 보고서 파일 조회: GET /api/v1/file (dc-service) + * - 파일 다운로드: GET /api/v1/fileDownload (dc-service) + */ +import { getDcData } from '@/components/lib/api'; +import { fileDownload } from '@/utils'; +import { useReportTabStore } from '@/store/useReportTabStore'; +import useSearchModalStore from '@/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/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/fileDownload${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/html/(map-layout)/map/searchModal/useSearchModal.ts b/apps/sn/app/html/(map-layout)/map/searchModal/useSearchModal.ts new file mode 100644 index 000000000..7e4b2c9dd --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/searchModal/useSearchModal.ts @@ -0,0 +1,161 @@ +/** + * 📄 useSearchModal Hook + * 검색 모달 훅 + * + * - 탭 기반 검색 인터페이스 관리 (기본정보, 유지관리 정보, 보고서) + * - 필터링된 데이터 조회 및 포맷팅 + * - 조회 버튼을 통한 데이터 검색 트리거 + * - 여러 시설 보기 기능 + * - Zustand를 통한 상태 관리 및 로딩 처리 + * - 데이터 매핑 및 코드 변환 (사업 유형 등) + * + * 🔗 사용 API + * - 유지관리 정보 목록 조회: GET /api/v1/bizInfo (sewerage-service) + */ +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, workType, + tableData, detailTableData, + curPage, + var1, var2, + yearFilter, + todayYear, todayMonth, todayDate, + setIsSearchModalOpen, + setShowTable, + setTableData, setDetailTableData, + setTableCount, setDetailTableCount, + setCurPage, + setIsLoading, + setIsResetPage, setIsResetDetailPage, + setIsResetSelection, setIsResetDetailSelection, + getDateString + } = useSearchModalStore(); + const { fetchBasicFilteredData } = useBasicTab(); + const { OPERATION_TABLE_SIZE } = useOperationTab(); + const { BIZ_TY_CD } = useStore(); + + const TABS = [ + { val: "basic", text: "기본 정보" }, + { val: "operation", text: "유지관리 정보" }, + { val: "report", text: "하수도 정비 기본계획 보고서" }, + ]; + + // 검색 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, fetchFilteredData }; +}; diff --git a/apps/sn/app/html/(map-layout)/map/waterDepth/waterDepth.tsx b/apps/sn/app/html/(map-layout)/map/waterDepth/waterDepth.tsx new file mode 100644 index 000000000..4ea769ff7 --- /dev/null +++ b/apps/sn/app/html/(map-layout)/map/waterDepth/waterDepth.tsx @@ -0,0 +1,98 @@ +/** + * ➡️ 경로 : 하수관망도 메인페이지 내 침수심 범례 + * + * 📄 WaterDepth Component + * 침수심 범례 컴포넌트 + * + * - 침수심 레이어의 범례 표시 및 애니메이션 재생 제어 + * - 진행률 바를 통한 애니메이션 상태 시각화 + * - 플레이/일시정지 버튼을 통한 사용자 상호작용 + * - 범례 항목별 색상과 레이블 표시 + */ + +"use client"; + +import { Label } from "@common/components/ui/label"; +import { useEffect } from "react"; + +export const WATER_DEPTH_LAYER_TYPES: string[] = [ + 'tb_ms_flood_dngr_map', // 홍수위험지도 + 'tb_ms_sbmrg_dngr_map' // 침수위험지도 +]; + +interface WaterDepthProps { + onClose?: () => void; + isPlaying: boolean; + setIsPlaying: (isPlaying: boolean) => void; + totalSteps?: number; + currentStep?: number; +} + +export function WaterDepth({ + onClose, + isPlaying, + setIsPlaying, + totalSteps = 0, + currentStep = 0, +}: WaterDepthProps) { + + useEffect(() => { + if (setIsPlaying) setIsPlaying(isPlaying); + }, [isPlaying, setIsPlaying]); + + const handlePlayToggle = () => { + setIsPlaying(!isPlaying); + }; + + // 진행률 계산 (0-100%) + const progressPercentage = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 100; + + + return ( + <> +
+
+ + +
+
+
+
+
+
+
+
침 수 심
+
+ {/* 코드: N330 */} + {/* 코드: N331 */} + {/* 코드: N332 */} + {/* 코드: N333 */} + {/* 코드: N334 */} +
+
+
+
+ + ); +} + +function LegendItem({ color, label }: { color: string; label: string }) { + return ( +
+ + {label} +
+ ); +} \ No newline at end of file