From b47c0dff2b696f4ff2a31607cd77e8ef9b3ae764 Mon Sep 17 00:00:00 2001 From: jsyang Date: Fri, 5 Sep 2025 13:49:22 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EB=9D=BC=EC=9D=B8=EB=B0=94=20=EC=B0=A8?= =?UTF-8?q?=ED=8A=B8=20=ED=81=AC=EA=B8=B0=20=EC=88=98=EC=A0=95,=20custom?= =?UTF-8?q?=20left=20=EC=B6=95=20=EB=A7=8C=EB=93=9C=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/chart/LineBarChart.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/common/business_components/src/ui/chart/LineBarChart.tsx b/packages/common/business_components/src/ui/chart/LineBarChart.tsx index eee09fb65..91270037e 100644 --- a/packages/common/business_components/src/ui/chart/LineBarChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineBarChart.tsx @@ -118,15 +118,21 @@ export default function LineBarchart({ if (!isFinite(maxValue) || maxValue <= 0) { return { max: 0, ticks: [0] }; } - + const exponent = Math.floor(Math.log10(maxValue)); const base = Math.pow(10, exponent); const fraction = maxValue / base; let niceFraction: number; if (fraction <= 1) niceFraction = 1; - else if (fraction <= 2) niceFraction = 3; + else if (fraction <= 2) niceFraction = 2; + else if (fraction <= 3) niceFraction = 3; + else if (fraction <= 4) niceFraction = 4; else if (fraction <= 5) niceFraction = 5; + else if (fraction <= 6) niceFraction = 6; + else if (fraction <= 7) niceFraction = 7; + else if (fraction <= 8) niceFraction = 8; + else if (fraction <= 9) niceFraction = 9; else niceFraction = 10; const niceMax = niceFraction * base; // round up to 1/2/5 * 10^k @@ -164,7 +170,7 @@ export default function LineBarchart({ return ( ) : null}
- + {setXAxisYearTick? @@ -210,7 +216,7 @@ export default function LineBarchart({ {/* 왼쪽 Y축: lineVal */} {yFieldText && typeof value === "number" ? value.toLocaleString() : value } @@ -218,7 +224,6 @@ export default function LineBarchart({ ticks={leftScale.ticks} orientation="left" label={(props) => } - />} {/* 오른쪽 Y축: barVal */} {yFieldText2 && Date: Fri, 5 Sep 2025 14:21:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=EB=9D=BC=EC=9D=B8=EB=B0=94=20=EC=B0=A8?= =?UTF-8?q?=ED=8A=B8=20=EC=A2=8C=EC=B8=A1=20=EC=97=AC=EB=B0=B1=20=EB=B0=8F?= =?UTF-8?q?=20=EB=9D=BC=EB=B2=A8=20=EC=A2=8C=EC=B8=A1=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A2=80=20=EB=8D=94=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/business_components/src/ui/chart/LineBarChart.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/common/business_components/src/ui/chart/LineBarChart.tsx b/packages/common/business_components/src/ui/chart/LineBarChart.tsx index 91270037e..f26cb976c 100644 --- a/packages/common/business_components/src/ui/chart/LineBarChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineBarChart.tsx @@ -170,7 +170,7 @@ export default function LineBarchart({ return ( ) : null}
- + Date: Mon, 8 Sep 2025 14:49:23 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor,=20add:=20-=20searchModal=20Store?= =?UTF-8?q?=20=ED=8C=A8=ED=84=B4=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20UI,=20=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=20-=20tsconfig.json=EC=97=90=20import=20p?= =?UTF-8?q?aths=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/popupComponent/MsSwagePpl.tsx | 2 +- apps/sn/app/(map-layout)/map/page.tsx | 34 +- .../(map-layout)/map/searchModal/basicTab.tsx | 20 +- .../map/searchModal/operationTab.tsx | 74 +- .../map/searchModal/reportFiles.tsx | 108 --- .../map/searchModal/reportFilesModal.tsx | 96 ++ .../map/searchModal/reportTab.tsx | 43 +- .../map/searchModal/searchModal.tsx | 83 +- .../map/searchModal/searchModalReportApi.ts | 198 ----- .../(map-layout)/map/searchModal/stofcbr.tsx | 20 +- .../(map-layout)/map/searchModal/swageMhl.tsx | 23 +- .../map/searchModal/swagePmstn.tsx | 24 +- .../(map-layout)/map/searchModal/swagePpl.tsx | 27 +- .../map/searchModal/swagePrcsFclt.tsx | 21 +- .../map/searchModal/swagePrcsZone.tsx | 16 +- .../map/searchModal/useBasicTab.ts | 329 +++++++ .../map/searchModal/useOperationTab.ts | 439 +++++++++ .../map/searchModal/useReportTab.ts | 173 ++++ .../map/searchModal/useSearchModal.ts | 171 ++++ .../map/searchModal/useSearchModal.tsx | 841 ------------------ .../map/waterDepth/waterDepth.tsx | 2 +- .../(with-base-layout)/ii/fileupload/page.tsx | 10 +- .../(with-base-layout)/oper/floodPre/page.tsx | 2 +- apps/sn/app/components/lib/api/index.ts | 7 +- .../app/components/lib/utils/fileDownload.ts | 47 +- apps/sn/app/store/useReportTabStore.ts | 43 + apps/sn/app/store/useSearchModalStore.ts | 15 +- apps/sn/tsconfig.json | 6 +- 28 files changed, 1418 insertions(+), 1456 deletions(-) delete mode 100644 apps/sn/app/(map-layout)/map/searchModal/reportFiles.tsx create mode 100644 apps/sn/app/(map-layout)/map/searchModal/reportFilesModal.tsx delete mode 100644 apps/sn/app/(map-layout)/map/searchModal/searchModalReportApi.ts create mode 100644 apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts create mode 100644 apps/sn/app/(map-layout)/map/searchModal/useOperationTab.ts create mode 100644 apps/sn/app/(map-layout)/map/searchModal/useReportTab.ts create mode 100644 apps/sn/app/(map-layout)/map/searchModal/useSearchModal.ts delete mode 100644 apps/sn/app/(map-layout)/map/searchModal/useSearchModal.tsx create mode 100644 apps/sn/app/store/useReportTabStore.ts diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx index fa1d770e7..04490f741 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/MsSwagePpl.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react' import InputSection from '../InputSection'; import { handleInputChange } from '../common'; import { getData } from "@/components/lib/api"; -import useStore from '../../../../store/useStore'; +import useStore from '@/store/useStore'; // 하수관로 const inputColumn = [ diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index e1bbc45ad..7ca34053b 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -8,37 +8,15 @@ import SearchModal from "./searchModal/searchModal"; import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup"; import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth"; import { ProcSearchBar } from "@common/business_components/ui/map/searchBar"; +import useSearchModalStore from "@/store/useSearchModalStore"; -// 타입 정의 추가 -export type LyrStngProps = { - lyr_sn: string; - lyr_nm: string; - upper_lyr_sn: string; - drawOrdr: string; - flterSql: string; - gobjTy: string; - lyrTpNm: string; - useYn: string; - minViewSize: string; - iconNm: string; - lblAtrbNm: string; - fontNm: string; - fontIntnColor: string; - fontExtnColor: string; - fontExtnWdth: string; - lnWdth: string; - lnColor: string; - polgnColor: string; - initEprssYn: string; - clickPsblYn: string; -}; const Map = () => { + const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore(); + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 - const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 const gisMaps = useRef(null); const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 - const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부 const [isPlaying, setIsPlaying] = useState(false); @@ -148,12 +126,12 @@ const Map = () => { searchView={SearchModal} detailView={FeatureClickEventPopup} onFeatureSelect={onFeatureSelect} - setIsOpen={setIsOpen} - setFeatureData={setFeatureData} featureData={featureData} - setIsSearchModalOpen={setIsSearchModalOpen} + setFeatureData={setFeatureData} isOpen={isOpen} + setIsOpen={setIsOpen} isSearchModalOpen={isSearchModalOpen} + setIsSearchModalOpen={setIsSearchModalOpen} onCheckedLayersChange={handleCheckedLayersChange} searchBar={} waterDepth={showWaterDepth ? WaterDepth : undefined} diff --git a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx index 5443bb65d..62fed6a28 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/basicTab.tsx @@ -1,15 +1,13 @@ import React from 'react'; -import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; import { SelectBox } from '@common/business_components/ui'; -import { Button, Label } from '@common/components'; +import { Label } from '@common/components'; import SwagePpl from './swagePpl'; import SwagePrcsFclt from './swagePrcsFclt'; import SwagePrcsZone from './swagePrcsZone'; import SwageMhl from './swageMhl'; import SwagePmstn from './swagePmstn'; import Stofcbr from './stofcbr'; -import { useSearchModal, type SearchModalProps } from './useSearchModal'; -import useSearchModalStore from '../../../store/useSearchModalStore'; +import useSearchModalStore from '@/store/useSearchModalStore'; // 기본정보 '구분' 선택 목록 @@ -22,7 +20,7 @@ export const BASIC_TYPES = [ { val: "tb_ms_stofcbr", text: "우수토실" }, ]; -const BasicTab: React.FC = (props) => { +const BasicTab: React.FC = () => { const { basicType, setBasicType } = useSearchModalStore(); return ( @@ -45,12 +43,12 @@ const BasicTab: React.FC = (props) => {
{/* 조회 조건 */} - {basicType === "tb_ms_swage_ppl" && ()} {/* 하수관로 */} - {basicType === "tb_il_swage_prcs_fclt" && ()} {/* 하수처리시설 */} - {basicType === "tb_ms_swage_prcs_zone" && ()} {/* 하수처리구역 */} - {basicType === "tb_ms_swage_mhl" && ()} {/* 하수맨홀 */} - {basicType === "tb_ms_swage_pmstn" && ()} {/* 하수펌프장 */} - {basicType === "tb_ms_stofcbr" && ()} {/* 우수토실 */} + {basicType === "tb_ms_swage_ppl" && ()} {/* 하수관로 */} + {basicType === "tb_il_swage_prcs_fclt" && ()} {/* 하수처리시설 */} + {basicType === "tb_ms_swage_prcs_zone" && ()} {/* 하수처리구역 */} + {basicType === "tb_ms_swage_mhl" && ()} {/* 하수맨홀 */} + {basicType === "tb_ms_swage_pmstn" && ()} {/* 하수펌프장 */} + {basicType === "tb_ms_stofcbr" && ()} {/* 우수토실 */} ); }; diff --git a/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx index 6f9e532a1..c77cc58cc 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/operationTab.tsx @@ -1,82 +1,22 @@ -import React from 'react'; +import React, { use } from 'react'; import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; import { SelectBox } from '@common/business_components/ui'; import { Button, Input, Label } from '@common/components'; import useStore from '../../../store/useStore'; import useSearchModalStore from '../../../store/useSearchModalStore'; -import { useSearchModal, type SearchModalProps } from './useSearchModal'; - -// 운영정보 '구분' 선택 목록 -export const WORK_TYPES = [ - { val: "['200', '201', '204']", text: "보수 및 갱신" }, - { val: "['100']", text: "점검 및 진단" }, - { val: "['203']", text: "청소 및 준설" }, -]; -// 운영정보 - 사업 현황 -export const operationTableHeadlist = [ - { id: 'rowNumber', title: '일련번호' }, - { id: "bizInfoNm", title: "사업명" }, - { id: "flfmtBgngYmd", title: "수행시작일자" }, - { id: "flfmtEndYmd", title: "수행종료일자" }, - { id: "flfmtYr", title: "수행년도" }, - { id: "bizTypeCd", title: "사업유형" }, - { id: "bizAmt", title: "사업금액" }, -]; -// 운영정보 - 보수 및 갱신 결과표 -export const operationMaintenanceTableHeadlist = [ - { id: 'rowNumber', title: '일련번호' }, - { id: "fcltMngNo", title: "관리번호" }, - { id: "swtrmt_zone_nm", title: "처리구역" }, - { id: "usgCd", title: "용도" }, - // { id: "lgalDrblyr", title: "내용연수" }, - // { id: "acqsPrmpc", title: "취득금액" }, - { id: "mqltCd", title: "재질" }, - { id: "pdmtVl", title: "관경(mm)" }, - { id: "prlgVl", title: "연장(m)" }, - { id: "instlYmd", title: "설치일자" }, - // { id: "instlYmd", title: "사업구분" }, -]; -// 운영정보 - 청소 및 준설 결과표 -export const operationCleaningTableHeadlist = [ - { id: 'rowNumber', title: '일련번호' }, - { id: "fcltMngNo", title: "관리번호" }, - { id: "swtrmt_zone_nm", title: "처리구역" }, - { id: "usgCd", title: "용도" }, - // { id: "lgalDrblyr", title: "내용연수" }, - // { id: "acqsPrmpc", title: "취득금액" }, - { id: "mqltCd", title: "재질" }, - { id: "pdmtVl", title: "관경(mm)" }, - { id: "prlgVl", title: "연장(m)" }, - { id: "instlYmd", title: "설치일자" }, -]; -// 운영정보 - 점검 및 진단 결과표 -export const operationInspectionTableHeadlist = [ - { id: 'rowNumber', title: '일련번호' }, - { id: "fcltMngNo", title: "관리번호" }, - { id: "swtrmt_zone_nm", title: "처리구역" }, - { id: "usgCd", title: "용도" }, - // { id: "riskEval", title: "위험평가" }, - // { id: "acqsPrmpc", title: "취득금액" }, - // { id: "lgalDrblyr", title: "내용연수" }, - // { id: "remLife", title: "잔존수명" }, - { id: "mqltCd", title: "재질" }, - { id: "pdmtVl", title: "관경(mm)" }, - { id: "prlgVl", title: "연장(m)" }, - { id: "instlYmd", title: "설치일자" }, - { id: "strc_stts_grd", title: "구조적 상태등급" }, - { id: "oper_stts_grd", title: "운영적 상태등급" }, -]; +import useSearchModal from './useSearchModal'; +import useOperationTab from './useOperationTab'; -const OperationTab: React.FC = (props) => { +const OperationTab: React.FC = () => { + const { BIZ_TY_CD } = useStore(); const { workType, setWorkType, var1, setVar1, var2, setVar2, yearFilter, setYearFilter, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); - - const { BIZ_TY_CD } = useStore(); + const { handleSearch } = useSearchModal(); + const { WORK_TYPES } = useOperationTab(); const BIZ_TY_CD_SELECT_ARRAY = BIZ_TY_CD.map(item => ({ val: item.lwpoCd, diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportFiles.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportFiles.tsx deleted file mode 100644 index 0746d6489..000000000 --- a/apps/sn/app/(map-layout)/map/searchModal/reportFiles.tsx +++ /dev/null @@ -1,108 +0,0 @@ -'use client' -import React, { useEffect, useState } from 'react'; - -import LoadingOverlay from '@/components/ui/LoadingBar'; -import { getFileDownload, getDcFiles, useDcFiles } from './searchModalReportApi'; -import { type ReportDataFile } from './searchModalReportApi'; -import { useReportFileModalStore } from './searchModalReportApi'; // 모달 상태 관리 스토어 -import { UiTable } from '@common/business_components/ui'; -import { UiModal } from "@common/business_components"; - -type ModalOpenProp = { - clsfSystmSn: string, // 자료관 기본계획 일련번호 -} -// 하수도정비 기본계획 보고서 파일목록 -export const headlist = [ - { id: "no", title: "NO" }, - { id: "fileTitle", title: "파일명" }, - { id: "regDt", title: "등록일자" }, -]; - -const ReportFiles: React.FC = ({ clsfSystmSn }: ModalOpenProp) => { - const [isLoading, setIsLoading] = useState(false); - const [isResetPage, setIsResetPage] = useState(false); - const [isResetSelection, setIsResetSelection] = useState(false); - const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useReportFileModalStore(); // 모달 상태 관리 스토어 - const [files, setFiles] = useState([]); - const [curPage, setCurPage] = useState(1); - - const filesOrigin = useDcFiles(clsfSystmSn); // 자료관 기본계획 일련번호에 해당하는 파일 목록을 가져오기 위한 훅 - useEffect(() => { - if (isModalOpen === true) { - getFiles(); // 파일 목록 조회 - } else { - setFiles([]); // 모달이 닫힐 때 파일 목록 초기화 - setCurPage(1); // 페이지를 1로 초기화 - setIsResetPage(true); // 페이지를 초기화 - setIsResetSelection(true); // 선택 초기화 - } - }, [filesOrigin, isModalOpen, curPage]); - - // 파일 목록 조회 - const getFiles = async () => { - setIsLoading(true); // 로딩 시작 - try { - //let data = await getDcFiles(clsfSystmSn); - let data = filesOrigin; - data = data?.slice((curPage - 1) * 10, curPage * 10); // paging - data = data?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 })); - setFiles(data); - } catch (error) { - console.error("파일 목록 조회 에러:", error); - } finally { - setIsLoading(false); // 로딩 종료 - } - }; - - // cellClick 핸들러 - const handleCellClick = (index: number|undefined) => { - if (typeof index === "number" && files[index]) { - const fileName = files[index].fileTitle; - const filePath = files[index].filePath; - if (files[index].fileSn !== "") { - getFileDownload(files[index].fileSn, fileName, filePath); - } - } - }; - - const renderBody = () => ( - <> -
- - { - return (<>조회 결과 ({filesOrigin.length.toLocaleString('ko-KR')})건); - })() - } - tableData={files} - headlist={headlist} - pageSize={10} - total={filesOrigin.length} - cellClick={handleCellClick} - onPageChange={setCurPage} - resetPageNo={isResetPage} - resetSelection={isResetSelection} - /> -
- - ); - return ( - - ); -}; - -export default React.memo(ReportFiles); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportFilesModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportFilesModal.tsx new file mode 100644 index 000000000..62c62b811 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/reportFilesModal.tsx @@ -0,0 +1,96 @@ +'use client' +import React, { useEffect, useState } from 'react'; + +import LoadingOverlay from '@/components/ui/LoadingBar'; +import useReportTab from './useReportTab'; +import { useReportTabStore } from '@/store/useReportTabStore'; +import { UiTable } from '@common/business_components/ui'; +import { UiModal } from "@common/business_components"; +import useSearchModalStore from '@/store/useSearchModalStore'; + +type ModalOpenProp = { clsfSystmSn: string } // 자료관 기본계획 일련번호 +const ReportFilesModal: React.FC = ({ clsfSystmSn }: ModalOpenProp) => { + // 하수도정비 기본계획 보고서 파일목록 + const REPORT_FILES_HEADLIST = [ + { id: "no", title: "NO" }, + { id: "fileTitle", title: "파일명" }, + { id: "regDt", title: "등록일자" }, + ]; + + const { + curPage, setCurPage, + isLoading, + isResetPage, + isResetSelection, + setIsResetPage, + setIsResetSelection + } = useSearchModalStore(); + const { + isOpen: isModalOpen, + files, + dcFiles: filesOrigin, + setIsOpen: setIsModalOpen, + setFiles, + fetchDcFiles + } = useReportTabStore(); + const { getFiles, handleCellClick } = useReportTab(); + + + useEffect(() => { + fetchDcFiles(clsfSystmSn); + }, [clsfSystmSn]); + + useEffect(() => { + if (isModalOpen === true) { + getFiles(); // 파일 목록 조회 + } else { + setFiles([]); // 모달이 닫힐 때 파일 목록 초기화 + setCurPage(1); // 페이지를 1로 초기화 + setIsResetPage(true); // 페이지를 초기화 + setIsResetSelection(true); // 선택 초기화 + } + }, [filesOrigin, isModalOpen, curPage]); + + const renderBody = () => ( + <> +
+ + { + return (<>조회 결과 ({filesOrigin.length.toLocaleString('ko-KR')})건); + })() + } + tableData={files} + headlist={REPORT_FILES_HEADLIST} + pageSize={10} + total={filesOrigin.length} + cellClick={handleCellClick} + onPageChange={setCurPage} + resetPageNo={isResetPage} + resetSelection={isResetSelection} + /> +
+ + ); + + + return ( + + ); +}; + +export default React.memo(ReportFilesModal); \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx index d476fedd7..1885cd8e9 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx @@ -4,11 +4,11 @@ import { Button, Label } from '@common/components'; import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; import { SelectBox, UiTable } from '@common/business_components/ui'; import LoadingOverlay from '@/components/ui/LoadingBar'; -import { addFileName, flattenFolders, getFileDownload, useDcTree, reportGubuns } from './searchModalReportApi'; -import useStore from '../../../store/useStore'; +import useReportTab from './useReportTab'; +import useStore from '@/store/useStore'; import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; -import { useReportFileModalStore } from './searchModalReportApi'; // 모달 상태 관리 스토어 -import ReportFiles from './reportFiles'; // 파일목록모달 +import { useReportTabStore } from '@/store/useReportTabStore'; +import ReportFilesModal from './reportFilesModal'; // 파일목록모달 // 하수도정비 기본계획 보고서 @@ -42,13 +42,20 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg const [ctpvCd, setCtpvCd] = useState('0'); const [sggCd, setSggCd] = useState('0'); const [reportGubun, setReportGubun] = useState("0"); - // const [gubun, setGubun] = useState(""); - const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useReportFileModalStore(); // 모달 상태 관리 스토어 const [clsfSystmSn, setClsfSystmSn] = useState(''); // 자료관 기본계획 일련번호 - const { ctpv, sgg } = useStore(); - // 하수정비기본계획 report - const reportOriginData = useDcTree(""); //:Folder[] + const { ctpv, sgg } = useStore(); + const { + isOpen: isReportFilesModalOpen, + setIsOpen: setIsReportFilesModalOpen, + dcTree: reportOriginData, + fetchDcTree + } = useReportTabStore(); // 모달 상태 관리 스토어 + const { REPORT_TYPES, flattenFolders, addFileName } = useReportTab(); + + useEffect(() => { + fetchDcTree(""); // 하수정비기본계획 report + }, []); useEffect(() => { // filterParams 초기화 @@ -105,12 +112,9 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg const handleReportCellClick = (index: number | undefined) => { if (typeof index === "number" && tableData[index]) { const reportData = tableData[index]; - const fileName = reportData.fileName; - const filePath = reportData.filePath; if (reportData.fileSn !== "") { - //getFileDownload(reportData.fileSn, fileName, filePath); - setIsModalOpen(true); // 모달 열기 setClsfSystmSn(reportData.clsfSystmSn); // 자료관 기본계획 일련번호 설정 + setIsReportFilesModalOpen(true); // 모달 열기 } } }; @@ -133,15 +137,16 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.text); } if (reportGubun !== "0") { // 보고서구분검색 - const reportGubunOption = reportGubuns.find((n) => n.val === reportGubun); + const reportGubunOption = REPORT_TYPES.find((n) => n.val === reportGubun); filterReportData = filterReportData.filter(item => item.reportGubun === reportGubunOption?.text); } - if (yearFilter.start != "" && yearFilter.end == "") //승인년도검색 + const { start, end } = yearFilter; + if (start && !end) //승인년도검색 filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" : null); - if (yearFilter.start == "" && yearFilter.end != "") + else if (!start && end) filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear <= yearFilter.end + "년" : null); - if (yearFilter.start != "" && yearFilter.end != "") { + else if (start && end) { filterReportData = filterReportData.filter(item => { return (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" && item.approvalYear <= yearFilter.end + "년" : null; }); @@ -194,7 +199,7 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg setReportGubun(String(val))} selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2' /> @@ -233,7 +238,7 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg {/* 모달 컴포넌트 */} - {isModalOpen && } + {isReportFilesModalOpen && } ); }; diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx index 078f3d473..da1f05169 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -1,37 +1,24 @@ -import React, { useEffect, useMemo } from 'react'; import LoadingOverlay from '@/components/ui/LoadingBar'; import { UiModal, UITab, UiTable } from '@common/business_components/ui'; import { Button } from '@common/components/ui'; -import axios from 'axios'; -import { - ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2, - MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES, -} from '../components/common/table'; -import { getData } from "@/components/lib/api"; -import BasicTab, { BASIC_TYPES } from './basicTab'; -import OperationTab, { operationCleaningTableHeadlist, operationInspectionTableHeadlist, operationMaintenanceTableHeadlist, operationTableHeadlist, WORK_TYPES } from './operationTab'; -import ReportTab from './reportTab'; -import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; +import React, { useMemo } from 'react'; import { Toaster } from "react-hot-toast"; -import { useSearchModal, OPERATION_TABLE_SIZE, type SearchModalProps } from './useSearchModal'; import useSearchModalStore from '../../../store/useSearchModalStore'; +import { + ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 +} from '../components/common/table'; +import BasicTab, { BASIC_TYPES } from './basicTab'; +import OperationTab from './operationTab'; +import ReportTab from './reportTab'; +import useSearchModal from './useSearchModal'; +import useBasicTab from './useBasicTab'; +import useOperationTab from './useOperationTab'; -const SearchModal: React.FC = (props) => { - const { - open, onOpenChange, - TABS, - handleSearch, - handleBasicCellClick, - handleOperationCellClick, - handleOperationResultCellClick, - basicExcelDownload, - operationExcelDownload, - operationResultExcelDownload, - viewMultipleFeature, - } = useSearchModal(props); +const SearchModal: React.FC = () => { const { + isSearchModalOpen, setIsSearchModalOpen, activeTab, basicType, workType, - tableData, detailTableData, + tableData, detailTableData, tableCount, detailTableCount, curPage, detailCurPage, isLoading, isDetailLoading, @@ -40,6 +27,21 @@ const SearchModal: React.FC = (props) => { setActiveTab, setCurPage, setDetailCurPage, } = useSearchModalStore(); + const { TABS, viewMultipleFeature } = useSearchModal(); + const { handleBasicCellClick, basicExcelDownload } = useBasicTab(); + const { + WORK_TYPES, + OPERATION_TABLE_SIZE, + OPERATION_TABLE_HEADLIST, + MAINTENANACE_TABLE_HEADLIST, + CLEANING_TABLE_HEADLIST, + INSPECTION_TABLE_HEADLIST, + handleOperationCellClick, + handleOperationResultCellClick, + operationExcelDownload, + operationResultExcelDownload + } = useOperationTab(); + const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => { if (activeTab === 'basic') { // 기본정보 @@ -52,16 +54,16 @@ const SearchModal: React.FC = (props) => { return []; } - if (activeTab === 'operation') return operationTableHeadlist; // 운영정보 + if (activeTab === 'operation') return OPERATION_TABLE_HEADLIST; // 운영정보 return []; }, [activeTab, basicType, workType]); const dynamicHeadlist2: { id: string, title: string }[] = useMemo(() => { if (activeTab === 'operation') { - if (workType === WORK_TYPES[0]?.val) return operationMaintenanceTableHeadlist; // 보수 및 갱신 - if (workType === WORK_TYPES[1]?.val) return operationInspectionTableHeadlist; // 점검 및 진단 - if (workType === WORK_TYPES[2]?.val) return operationCleaningTableHeadlist; // 청소 및 준설 + if (workType === WORK_TYPES[0]?.val) return MAINTENANACE_TABLE_HEADLIST; // 보수 및 갱신 + if (workType === WORK_TYPES[1]?.val) return CLEANING_TABLE_HEADLIST; // 점검 및 진단 + if (workType === WORK_TYPES[2]?.val) return INSPECTION_TABLE_HEADLIST; // 청소 및 준설 } return []; @@ -79,20 +81,9 @@ const SearchModal: React.FC = (props) => { }))} onValueChange={(value: string) => setActiveTab(value)} /> - {/* 기본정보 탭 */} - {activeTab === 'basic' && ( - - )} - {/* 운영정보 탭 */} - {activeTab === 'operation' && ( - - )} - {/* 하수도정비기본계획보고서 탭 */} - {activeTab === 'report' && ( - - )} + {activeTab === 'basic' && ()} {/* 기본정보 탭 */} + {activeTab === 'operation' && ()} {/* 운영정보 탭 */} + {activeTab === 'report' && ()} {/* 하수도정비기본계획보고서 탭 */} {/* 📋 결과 리스트 */} @@ -192,8 +183,8 @@ const SearchModal: React.FC = (props) => { return ( <> { - return /^[0-9]{4}년$/.test(value); -}; -export const reportGubuns = [{ val: '0', text: '전체' }, { val: '1', text: '보고서' } - , { val: '2', text: '요약보고서' }, { val: '3', text: '부록' }, { val: '4', text: '기타' }]; - - -/** - * Flatten a hierarchical folder structure into a flat array. - * @param data - The hierarchical folder data to flatten. - * @returns An array of flattened folders with their paths and levels. - */ -export function flattenFolders(data: Folder[]): FlattenedFolder[] { - const result: FlattenedFolder[] = []; - const exceptNodeNm = ["부분", "통합", "전체"]; - const addNodeNm = ["보고서", "요약보고서", "부록", "기타"]; // level5 - const columnNms: { [key: number]: string } = { - 0: "root", 1: "sido", 2: "sigungu", 3: "approvalYear" - , 4: "plantZoneGubun", 5: "reportGubun", 7: "fileName", 8: "regDate" - }; - - function traverse( - node: Folder, - path: { [key: string]: string | number }, - level: number - ) { - if (level === 2 && isYear(node.clsfSystmNm)) { - level++; - } - if (level === 4 && addNodeNm.includes(node.clsfSystmNm)) { - level++; // 레벨을 증가시켜 하위값에 추가되도록 함. - } - - const newPath = { - ...path, - [`${columnNms[level]}`]: node.clsfSystmNm, - }; - - if (node.children.length === 0) { - newPath.no = result.length + 1; - newPath.clsfSystmSn = node.clsfSystmSn; // 파일 조회를 위한 고유번호 - result.push(newPath); - } else { - if (level === 4) { - if (exceptNodeNm.includes(node.clsfSystmNm)) { - level--; // 제외할 노드 이름인 경우, 현재의 레벨에 하위값이 써지도록 함. - } - } - node.children.forEach((child) => { - traverse(child, newPath, level + 1); - }); - } - } - - if (data && data.length > 0 && data[0]?.clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리 - data.forEach((root) => traverse(root, {}, 0)); - } else { - data.forEach((root) => traverse(root, {}, 1)); - } - return result; -} - -export const getDcTree = async (clsfSystmNm: string) => { - const result = await getDcData("/api/v1/dc/clsf?clsfSystmNm=" + clsfSystmNm); - return result; -}; -export const useDcTree = (clsfSystmNm: string) => { - const [data, setData] = useState([]); - - useEffect(() => { - const fetch = async () => { - const result = await getDcTree(clsfSystmNm); - if (result) { - setData(result); - } - console.log("useDcTree호출"); - console.log("clsfSystmNm:", clsfSystmNm, "data:", result); - }; - fetch(); - }, [clsfSystmNm]); - - return data; -}; - -export const getDcFiles = async (clsfSystmSn: string) : Promise => { - if (!clsfSystmSn) { - console.error("clsfSystmSn is required to get files."); - return []; - } - const queryString = 'clsfSystmSn=' + clsfSystmSn; - const result = await getDcData(`/api/v1/dc/file${queryString ? `?${queryString}` : ''}`); - return result; -}; - -export const useDcFiles = (clsfSystmNm: string) => { - const [data, setData] = useState([]); - - useEffect(() => { - const fetch = async () => { - const result = await getDcFiles(clsfSystmNm); - if (result) { - setData(result); - } - console.log("useDcFiles 호출"); - console.log("clsfSystmNm:", clsfSystmNm, "data:", result); - }; - fetch(); - }, [clsfSystmNm]); - - return data; -}; - -// 파일 이름 추가 -export const addFileName = async (data: FlattenedFolder[]) => { - const fileReportData = await Promise.all( - data.map(async (item) => { - const fileArr: ReportDataFile[] = await getDcFiles(String(item.clsfSystmSn)); - let result:FlattenedFolder; - if (fileArr.length > 0) { - const f = fileArr[0]; - const fCount = fileArr.length; - result = { ...item, fileName: f?.fileTitle+" 외 "+fCount+"건", regDate: f?.regDt, fileSn: f?.fileSn, filePath: f?.filePath }; - } else { - result = { ...item, fileName: "", regDate: "", fileSn: "", filePath: "" }; - } - return result; - }) - ); - return fileReportData; -}; - -export const getDcFileprcs = async (fileSn: string) => { - const queryString = 'fileSn=' + fileSn; - const result = await getDcData(`/api/v1/dc/fileprcs${queryString ? `?${queryString}` : ''}`); - return result; -}; - -export const getDcFileDownload = async (filePath: string) => { - const queryString = 'filePath=' + encodeURIComponent(filePath); - const response = await fetch(`/dc-service/api/v1/dc/file/download?${queryString}`, { - method: 'GET', - }); - if (!response.ok) { - throw new Error(`File download failed: ${response.status}`); - } - const blob = await response.blob(); // PDF, image 등은 blob으로 받아야 함 - return blob; -}; -export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터 임시link로 연결 - const url = window.URL.createObjectURL(data); - const link = document.createElement('a'); - link.href = url; - link.download = fileName; // 원하는 파일명 지정 - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(url); -} -export const getFileDownload = async (fileSn: string, fileName: string, filePath: string) => { - if (filePath === "") alert("filePath 정보 없으므로 다운로드 불가:filesn" + fileSn); - const blobData = await getDcFileDownload(filePath); // 실제 다운로드 API - fileDownload(blobData, fileName); -} - -interface ModalStore { - isOpen: boolean; - setIsOpen: (open: boolean) => void; -} -export const useReportFileModalStore = create((set) => ({ - isOpen: false, - setIsOpen: (open: boolean) => set({ isOpen: open }), -})); diff --git a/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx b/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx index 53d8866f7..11c4c8bfa 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/stofcbr.tsx @@ -5,24 +5,16 @@ import { Button } from '@common/components'; import React from 'react'; import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; import useSearchModalStore from '../../../store/useSearchModalStore'; -import { useSearchModal, type SearchModalProps } from './useSearchModal'; +import useSearchModal from './useSearchModal'; -interface stofcbrCqlFiltersParams { - ctpvCd: string; - sggCd: string; - yearFilter: { start: string; end: string }; - todayYear: string; - todayMonth: string; - todayDate: string; - getDateString: (year: string, month: string, day: string, defaultDate: string) => string; -} -export const stofcbrCqlFilters = (params: stofcbrCqlFiltersParams) => { + +export const stofcbrCqlFilters = () => { const { ctpvCd, sggCd, yearFilter, todayYear, todayMonth, todayDate, getDateString - } = params; + } = useSearchModalStore.getState(); const cqlFilters: string[] = []; // 시도, 시군구 @@ -48,13 +40,13 @@ export const stofcbrCqlFilters = (params: stofcbrCqlFiltersParams) => { return cqlFilters; }; -const Stofcbr: React.FC = (props) => { +const Stofcbr: React.FC = () => { const { ctpvCd, setCtpvCd, sggCd, setSggCd, yearFilter, setYearFilter, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return ( diff --git a/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx b/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx index 302105c63..04ffeb1fa 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swageMhl.tsx @@ -7,21 +7,10 @@ import { MS_SWAGE_MHL_USE_CODES } from '../components/common/table'; import { SelectBox } from '@common/business_components/ui'; import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; import useSearchModalStore from '../../../store/useSearchModalStore'; -import { useSearchModal, type SearchModalProps } from './useSearchModal'; +import useSearchModal from './useSearchModal'; -interface swageMhlCqlFiltersParams { - ctpvCd: string; - sggCd: string; - yearFilter: { start: string; end: string }; - start1: string; - end1: string; - var1: string; - todayYear: string; - todayMonth: string; - todayDate: string; - getDateString: (year: string, month: string, day: string, defaultDate: string) => string; -} -export const swageMhlCqlFilters = (params: swageMhlCqlFiltersParams) => { + +export const swageMhlCqlFilters = () => { const { ctpvCd, sggCd, yearFilter, @@ -29,7 +18,7 @@ export const swageMhlCqlFilters = (params: swageMhlCqlFiltersParams) => { var1, todayYear, todayMonth, todayDate, getDateString - } = params; + } = useSearchModalStore.getState(); const cqlFilters: string[] = []; // 시도, 시군구 @@ -60,7 +49,7 @@ export const swageMhlCqlFilters = (params: swageMhlCqlFiltersParams) => { return cqlFilters; }; -const SwageMhl: React.FC = (props) => { +const SwageMhl: React.FC = () => { const { var1, setVar1, start1, setStart1, end1, setEnd1, @@ -68,7 +57,7 @@ const SwageMhl: React.FC = (props) => { sggCd, setSggCd, yearFilter, setYearFilter, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return (
diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx index 9286ccbdb..f3eef5d66 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePmstn.tsx @@ -5,29 +5,17 @@ import { Button, Input, Label } from '@common/components'; import React from 'react'; import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; import useSearchModalStore from '../../../store/useSearchModalStore'; -import { useSearchModal, type SearchModalProps } from './useSearchModal'; +import useSearchModal from './useSearchModal'; -interface swagePmstnCqlFiltersParams { - ctpvCd: string; - sggCd: string; - yearFilter: { start: string; end: string }; - start1: string; - start2: string; - end1: string; - end2: string; - todayYear: string; - todayMonth: string; - todayDate: string; - getDateString: (year: string, month: string, day: string, defaultDate: string) => string; -} -export const swagePmstnCqlFilters = (params: swagePmstnCqlFiltersParams) => { + +export const swagePmstnCqlFilters = () => { const { ctpvCd, sggCd, yearFilter, start1, start2, end1, end2, todayYear, todayMonth, todayDate, getDateString - } = params; + } = useSearchModalStore.getState(); const cqlFilters: string[] = []; // 시도, 시군구 @@ -63,7 +51,7 @@ export const swagePmstnCqlFilters = (params: swagePmstnCqlFiltersParams) => { return cqlFilters; }; -const SwagePmstn: React.FC = (props) => { +const SwagePmstn: React.FC = () => { const { start1, setStart1, end1, setEnd1, @@ -71,7 +59,7 @@ const SwagePmstn: React.FC = (props) => { sggCd, setSggCd, yearFilter, setYearFilter, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return (
diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx index 71a0e9935..0a79d8f81 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePpl.tsx @@ -4,29 +4,14 @@ import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; import { SelectBox } from '@common/business_components/ui'; import { Button, Input, Label } from '@common/components'; import React from 'react'; -import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table'; +import { MS_SWAGE_PPL_USE_CODES } from '../components/common/table'; import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; import useSearchModalStore from '../../../store/useSearchModalStore'; -import { useSearchModal, type SearchModalProps } from './useSearchModal'; +import useSearchModal from './useSearchModal'; -interface swagePplCqlFiltersParams { - var1: string; - var2: string; - ctpvCd: string; - sggCd: string; - yearFilter: { start: string; end: string }; - start1: string; - start2: string; - end1: string; - end2: string; - todayYear: string; - todayMonth: string; - todayDate: string; - getDateString: (year: string, month: string, day: string, defaultDate: string) => string; -} // 하수관로용 CQL 필터 생성 -export const swagePplCqlFilters = (params: swagePplCqlFiltersParams) => { +export const swagePplCqlFilters = () => { const { var1, var2, ctpvCd, sggCd, @@ -34,7 +19,7 @@ export const swagePplCqlFilters = (params: swagePplCqlFiltersParams) => { start1, start2, end1, end2, todayYear, todayMonth, todayDate, getDateString - } = params; + } = useSearchModalStore.getState(); const cqlFilters: string[] = []; if (var1) cqlFilters.push(`usg_cd='${var1}'`); @@ -69,14 +54,14 @@ export const swagePplCqlFilters = (params: swagePplCqlFiltersParams) => { return cqlFilters; }; -const SwagePpl: React.FC = (props) => { +const SwagePpl: React.FC = () => { const { var1, setVar1, start1, setStart1, end1, setEnd1, ctpvCd, setCtpvCd, sggCd, setSggCd, yearFilter, setYearFilter, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return ( diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx index dcc5546ad..97486e4a3 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsFclt.tsx @@ -5,29 +5,18 @@ import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox'; import { Button, Input, Label } from '@common/components'; import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; import useSearchModalStore from '../../../store/useSearchModalStore'; -import { useSearchModal, type SearchModalProps } from './useSearchModal'; +import useSearchModal from './useSearchModal'; -interface swagePrcsFcltCqlFiltersParams { - ctpvName: string; - sggName: string; - yearFilter: { start: string; end: string }; - start1: string; - end1: string; - todayYear: string; - todayMonth: string; - todayDate: string; - getDateString: (year: string, month: string, day: string, defaultDate: string) => string; -} // 하수처리시설용 CQL 필터 생성 -export const swagePrcsFcltCqlFilters = (params: swagePrcsFcltCqlFiltersParams) => { +export const swagePrcsFcltCqlFilters = () => { const { ctpvName, sggName, yearFilter, start1, end1, todayYear, todayMonth, todayDate, getDateString - } = params; + } = useSearchModalStore.getState(); const cqlFilters: string[] = []; // 가동개시일자 범위 @@ -65,14 +54,14 @@ export const swagePrcsFcltCqlFilters = (params: swagePrcsFcltCqlFiltersParams) = return cqlFilters; }; -const SwagePrcsFclt: React.FC = (props) => { +const SwagePrcsFclt: React.FC = () => { const { start1, setStart1, end1, setEnd1, ctpvCd, setCtpvCd, setCtpvName, sggCd, setSggCd, setSggName, yearFilter, setYearFilter, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return (
diff --git a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx index 84e30c5ee..dcda670f2 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/swagePrcsZone.tsx @@ -4,21 +4,15 @@ import React from 'react'; import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; import { Button, Input, Label } from '@common/components'; import useSearchModalStore from '../../../store/useSearchModalStore'; -import { useSearchModal, type SearchModalProps } from './useSearchModal'; +import useSearchModal from './useSearchModal'; -interface swagePrcsZoneCqlFiltersParams { - ctpvCd: string; - sggCd: string; - var1: string; - var2: string; -} // 하수처리구역용 CQL 필터 생성 -export const swagePrcsZoneCqlFilters = (params: swagePrcsZoneCqlFiltersParams) => { +export const swagePrcsZoneCqlFilters = () => { const { ctpvCd, sggCd, var1, var2 - } = params; + } = useSearchModalStore.getState(); const cqlFilters: string[] = []; if (ctpvCd !== '' && ctpvCd !== '0') cqlFilters.push(`ctpv_cd='${ctpvCd?.trim()}'`); if (sggCd !== '' && sggCd !== '0') cqlFilters.push(`sgg_cd='${sggCd}'`); @@ -27,13 +21,13 @@ export const swagePrcsZoneCqlFilters = (params: swagePrcsZoneCqlFiltersParams) = return cqlFilters; }; -const swagePrcsZone: React.FC = (props) => { +const swagePrcsZone: React.FC = () => { const { var1, setVar1, var2, setVar2, ctpvCd, setCtpvCd, sggCd, setSggCd, } = useSearchModalStore(); - const { handleSearch } = useSearchModal(props); + const { handleSearch } = useSearchModal(); return (
diff --git a/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts new file mode 100644 index 000000000..1d49529da --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/useBasicTab.ts @@ -0,0 +1,329 @@ +import { useEffect, useState } from 'react'; +import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui'; +import axios from 'axios'; +import { + MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES, + MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol +} from '../components/common/table'; +import { getData } from "@/components/lib/api"; +import { BASIC_TYPES } from './basicTab'; +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; +import useStore from '../../../store/useStore'; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { confirmToast } from '@/components/ui/toast' +import { toast } from "react-hot-toast"; +import useSearchModalStore from '../../../store/useSearchModalStore'; +import { swagePplCqlFilters } from './swagePpl'; +import { swagePrcsFcltCqlFilters } from './swagePrcsFclt'; +import { swagePrcsZoneCqlFilters } from './swagePrcsZone'; +import { swageMhlCqlFilters } from './swageMhl'; +import { swagePmstnCqlFilters } from './swagePmstn'; +import { stofcbrCqlFilters } from './stofcbr'; + + +export default function useBasicTab() { + const { + basicType, + tableData, + curPage, + todayYear, todayMonth, todayDate, + setIsSearchModalOpen, + setTableData, + setTableCount, + setIsLoading, + } = useSearchModalStore(); + const { ctpv, sgg } = useStore(); + + const fetchBasicFilteredData = async () => { + let cqlFilters: string[] = []; + const queryParams = new URLSearchParams({ + lyrTypeNm: basicType, + page: `${curPage - 1}`, + size: '10', + sort: 'string', + }); + + if (basicType === 'tb_ms_swage_ppl') cqlFilters = swagePplCqlFilters(); // 하수관로 + else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = swagePrcsFcltCqlFilters(); // 하수처리시설 + else if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = swagePrcsZoneCqlFilters(); // 하수처리구역 + else if (basicType === 'tb_ms_swage_mhl') cqlFilters = swageMhlCqlFilters(); // 하수맨홀 + else if (basicType === 'tb_ms_swage_pmstn') cqlFilters = swagePmstnCqlFilters(); // 하수펌프장 + else if (basicType === 'tb_ms_stofcbr') cqlFilters = stofcbrCqlFilters(); // 우수토실 + if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and ')); + + const res = await getData(`v1/fclts?${queryParams.toString()}`); + // const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`); + console.log('fetch res', res); + console.log('fetch res.data', res.data); + console.log('fetch res.count', res.count); + console.log('fetch res.length', res.length); + + // 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가 + const dataWithRowNumber = (res.data || []).map((item: any, index: number) => { + // 🎯 먼저 각 아이템을 포맷팅 + const formattedItem = formatDisplayData(item); + + let ctpvCd = item?.ctpvNm ?? formattedItem?.ctpvNm ?? ''; + let sggCd = item?.sggNm ?? formattedItem?.sggNm ?? ''; + + if (!ctpvCd || !sggCd) { + const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim(); + if (!ctpvCd && code) ctpvCd = code.slice(0, 2); + if (!sggCd && code) sggCd = code.slice(0, 5); + } + + const matchedMhlCode = MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim()); + const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd); + const ctpvNm = ctpv.find(code => code.val.trim() === ctpvCd)?.text || item?.ctpvNm; + const sggNm = sgg.find(code => code.val.trim() === sggCd)?.text || item?.sggNm; + const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim()); + + // 추가 처리 및 일련번호 추가 + return { + ...formattedItem, + rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가 + scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item?.scsYn, // scsYn이 null이면 'N'으로 변경 + mqltCd: matchedMhlCode ? matchedMhlCode.text : item?.mqltCd, + usgCd: matchedMhlUsgCode ? matchedMhlUsgCode.text : (usgCdObj ? usgCdObj.text : item?.usgCd), + ctpvNm: ctpvNm ?? '-', + sggNm: sggNm ?? '-', + swtrmt_zone_nm: item.swtrmtZoneNm || item.swtrmt_zone_nm || '-', + }; + }); + + setTableData(dataWithRowNumber); + setTableCount(res.count || []); + }; + + // 기본정보 테이블 cellClick 핸들러 + const handleBasicCellClick = async (index?: number | undefined, headId?: String) => { + if (typeof index === "number") { + setIsLoading(true); + + const row = tableData[index]; + const layerNm = row.lyrTypeNm; + const fid = row.fid; + const core = GisApp.Module.layer as any; + await core.showSearchResultsFromGeoJSON(layerNm, fid); + setIsLoading(false); + setIsSearchModalOpen(false); // SearchModal 닫기 + } + }; + + // 기본정보 Excel 다운로드 핸들러 + const basicExcelDownload = async () => { + if (tableData.length === 0) { + alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); + return; + } + + toast.dismiss("global-confirm-toast"); + + confirmToast( + "파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?", + onDownload ?? (() => { }), + "text-xl font-medium text-gray-800" + ); + + async function onDownload() { + setIsLoading(true); + + try { + const queryParams = new URLSearchParams({ + lyrTypeNm: basicType, + size: '10000', + }); + + let cqlFilters: string[] = []; + let headerMapping: { [key: string]: string } = {}; + let headerRange: string = "A1:F1"; + + switch (basicType) { + case 'tb_ms_swage_ppl': { + // 하수관로 + cqlFilters = swagePplCqlFilters() + headerMapping = MsSwagePplExcelCol; + headerRange = "A1:M1"; + break; + } + case 'tb_il_swage_prcs_fclt': { + // 하수처리시설 + cqlFilters = swagePrcsFcltCqlFilters(); + headerMapping = SwtrmtFcltExcelCol; + headerRange = "A1:J1"; + break; + } + case 'tb_ms_swage_prcs_zone': { + // 하수처리구역 + cqlFilters = swagePrcsZoneCqlFilters(); + headerMapping = SwtrmtPrcsZoneExcelCol; + headerRange = "A1:D1"; + break; + } + case 'tb_ms_swage_mhl': { + // 하수맨홀 + cqlFilters = swageMhlCqlFilters(); + headerMapping = MsSwageMhlExcelCol; + headerRange = "A1:J1"; + break; + } + case 'tb_ms_swage_pmstn': { + // 하수펌프장 + cqlFilters = swagePmstnCqlFilters(); + headerMapping = ExclnSewagePumpExcelCol; + headerRange = "A1:J1"; + break; + } + case 'tb_ms_stofcbr': { + // 우수토실 + cqlFilters = stofcbrCqlFilters(); + headerMapping = stofcbrExcelCol; + headerRange = "A1:H1"; + break; + } + } + + if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and ')); + + const res = await getData(`v1/fclts?${queryParams.toString()}`); + // const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`); + console.log('onDownload res', res); + + const mhlUsgCdMap = new Map(MS_SWAGE_MHL_USE_CODES.map(code => [code.val?.trim(), code.text])); + const pplUsgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val?.trim(), code.text])); + const ctpvMap = new Map(ctpv.map(code => [code.val?.trim(), code.text])); + const sggMap = new Map(sgg.map(code => [code.val?.trim(), code.text])); + + const mappedContent = (res.data.data || []).map((item: any) => { + const formattedItem = formatDisplayData(item); + + let ctpvCd = item?.ctpvCd ?? formattedItem?.ctpvCd ?? ''; + let sggCd = item?.sggCd ?? formattedItem?.sggCd ?? ''; + + if (!ctpvCd || !sggCd) { + const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim(); + if (!ctpvCd && code) ctpvCd = code.slice(0, 2); + if (!sggCd && code) sggCd = code.slice(0, 5); + } + + // trim 한 번만 수행 + const trimmedUsgCd = item?.usgCd?.trim(); + // const trimmedMqltCd = item?.mqltCd?.trim(); + + switch (basicType) { + case 'tb_ms_swage_ppl': { + // 하수관로 + return { + fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-', + instMngNo: formattedItem.instMngNo ?? item.instMngNo ?? '-', + ctpvNm: ctpvMap.get(ctpvCd) ?? item.ctpvNm ?? '-', + sggNm: sggMap.get(sggCd) ?? item.sggNm ?? '-', + swtrmt_zone_nm: formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '-', + instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-', + usgCd: pplUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-', + pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '-', + prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '-', + tmptDpth: formattedItem.tmptDpth ?? item.tmptDpth ?? '-', + tmptMhlMngNo: formattedItem.tmptMhlMngNo ?? item.tmptMhlMngNo ?? '-', + scendDpth: formattedItem.scendDpth ?? item.scendDpth ?? '-', + scendMhlMngNo: formattedItem.scendMhlMngNo ?? item.scendMhlMngNo ?? '-', + }; + } + case 'tb_il_swage_prcs_fclt': { + // 하수처리시설 + return { + swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-', + fcltCpct: formattedItem.fcltCpct ?? item.fcltCpct ?? '-', + rgnExpln: formattedItem.rgnExpln ?? item.rgnExpln ?? '-', + oprtngStrtYmd: formattedItem.oprtngStrtYmd ?? item.oprtngStrtYmd ?? '-', + laeNm: formattedItem.laeNm ?? item.laeNm ?? '-', + sareaNm: formattedItem.sareaNm ?? item.sareaNm ?? '-', + rvrNm: formattedItem.rvrNm ?? item.rvrNm ?? '-', + dschgWtstNm: formattedItem.dschgWtstNm ?? item.dschgWtstNm ?? '-', + swtrmtPpltnCnt: formattedItem.swtrmtPpltnCnt ?? item.swtrmtPpltnCnt ?? '-', + rprsMsrcCn: formattedItem.rprsMsrcCn ?? item.rprsMsrcCn ?? '-' + }; + } + case 'tb_ms_swage_prcs_zone': { + // 하수처리구역 + return { + swtrmtZoneNm: formattedItem.swtrmtZoneNm ?? item.swtrmtZoneNm ?? '-', + ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-', + sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-', + swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-' + }; + } + case 'tb_ms_swage_mhl': { + // 하수맨홀 + return { + fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-', + ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-', + sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-', + instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-', + usgCd: mhlUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-', + swageMhlDiamVl: formattedItem.swageMhlDiamVl ?? item.swageMhlDiamVl ?? '-', + wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-', + vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-', + altdVl: formattedItem.altdVl ?? item.altdVl ?? '-', + dpthlVl: formattedItem.dpthlVl ?? item.dpthlVl ?? '-' + }; + } + case 'tb_ms_swage_pmstn': { + // 하수펌프장 + return { + fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-', + ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-', + sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-', + swagePmstnNm: formattedItem.swagePmstnNm ?? item.swagePmstnNm ?? '-', + instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-', + dailyPrcsCpct: formattedItem.dailyPrcsCpct ?? item.dailyPrcsCpct ?? '-', + maxImpndCpct: formattedItem.maxImpndCpct ?? item.maxImpndCpct ?? '-', + altdVl: formattedItem.altdVl ?? item.altdVl ?? '-', + wtlvVl: formattedItem.wtlvVl ?? item.wtlvVl ?? '-', + exclnWpmpAbltVl: formattedItem.exclnWpmpAbltVl ?? item.exclnWpmpAbltVl ?? '-' + }; + } + case 'tb_ms_stofcbr': { + // 우수토실 + return { + fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-', + mapshtNo: formattedItem.mapshtNo ?? item.mapshtNo ?? '-', + ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-', + sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-', + instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-', + worflwWallHgt: formattedItem.worflwWallHgt ?? item.worflwWallHgt ?? '-', + swageMhlMngNo: formattedItem.swageMhlMngNo ?? item.swageMhlMngNo ?? '-', + drtnAngl: formattedItem.drtnAngl ?? item.drtnAngl ?? '-' + }; + } + } + }); + + // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 + const excelData = mappedContent; // 배열로 감싸서 전달 + + // 파일명 생성 (분석번호 기반) + const label = BASIC_TYPES.find((type) => type.val === basicType)?.text + const fileName = `${label} 조회 결과_${todayYear}${todayMonth}${todayDate}`; + + console.log(`excelData: ${excelData}, label: ${label}, fileName: ${fileName}`); + + await downloadExcelWithKoreanHeaders({ + data: excelData as JSON[], + fileName: fileName, + sheetNameArray: [fileName], + headerRange: headerRange, + headerMapping: headerMapping + }); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + console.error("Excel 다운로드 중 오류 발생:", error); + alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); + } + } + }; + + + return { fetchBasicFilteredData, handleBasicCellClick, basicExcelDownload }; +}; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/useOperationTab.ts b/apps/sn/app/(map-layout)/map/searchModal/useOperationTab.ts new file mode 100644 index 000000000..dd7b8d3cc --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/useOperationTab.ts @@ -0,0 +1,439 @@ +import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui'; +import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table'; +import { getData } from "@/components/lib/api"; +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import { confirmToast } from '@/components/ui/toast' +import { toast } from "react-hot-toast"; +import useSearchModalStore from '@/store/useSearchModalStore'; +import useStore from '@/store/useStore'; + + +export default function useOperationTab() { + const OPERATION_TABLE_SIZE = 5; + + const { + workType, + tableData, detailTableData, + curPage, detailCurPage, + detailBizId, + var1, var2, + yearFilter, + todayYear, todayMonth, todayDate, + setIsSearchModalOpen, + setTableData, setDetailTableData, + setTableCount, setDetailTableCount, + setDetailCurPage, + setIsLoading, setIsDetailLoading, + setIsResetDetailPage, + setIsResetDetailSelection, + setDetailBizId, + getDateString + } = useSearchModalStore(); + const { BIZ_TY_CD } = useStore(); + + const WORK_TYPES = [ // 운영정보 '구분' 선택 목록 + { val: "['200', '201', '204']", text: "보수 및 갱신" }, + { val: "['100']", text: "점검 및 진단" }, + { val: "['203']", text: "청소 및 준설" }, + ]; + const OPERATION_TABLE_HEADLIST = [ // 운영정보 - 사업 현황 + { id: 'rowNumber', title: '일련번호' }, + { id: "bizInfoNm", title: "사업명" }, + { id: "flfmtBgngYmd", title: "수행시작일자" }, + { id: "flfmtEndYmd", title: "수행종료일자" }, + { id: "flfmtYr", title: "수행년도" }, + { id: "bizTypeCd", title: "사업유형" }, + { id: "bizAmt", title: "사업금액" }, + ]; + const MAINTENANACE_TABLE_HEADLIST = [ // 운영정보 - 보수 및 갱신 결과표 + { id: 'rowNumber', title: '일련번호' }, + { id: "fcltMngNo", title: "관리번호" }, + { id: "swtrmt_zone_nm", title: "처리구역" }, + { id: "usgCd", title: "용도" }, + // { id: "lgalDrblyr", title: "내용연수" }, + // { id: "acqsPrmpc", title: "취득금액" }, + { id: "mqltCd", title: "재질" }, + { id: "pdmtVl", title: "관경(mm)" }, + { id: "prlgVl", title: "연장(m)" }, + { id: "instlYmd", title: "설치일자" }, + // { id: "instlYmd", title: "사업구분" }, + ]; + const CLEANING_TABLE_HEADLIST = [ // 운영정보 - 청소 및 준설 결과표 + { id: 'rowNumber', title: '일련번호' }, + { id: "fcltMngNo", title: "관리번호" }, + { id: "swtrmt_zone_nm", title: "처리구역" }, + { id: "usgCd", title: "용도" }, + // { id: "lgalDrblyr", title: "내용연수" }, + // { id: "acqsPrmpc", title: "취득금액" }, + { id: "mqltCd", title: "재질" }, + { id: "pdmtVl", title: "관경(mm)" }, + { id: "prlgVl", title: "연장(m)" }, + { id: "instlYmd", title: "설치일자" }, + ]; + const INSPECTION_TABLE_HEADLIST = [ // 운영정보 - 점검 및 진단 결과표 + { id: 'rowNumber', title: '일련번호' }, + { id: "fcltMngNo", title: "관리번호" }, + { id: "swtrmt_zone_nm", title: "처리구역" }, + { id: "usgCd", title: "용도" }, + // { id: "riskEval", title: "위험평가" }, + // { id: "acqsPrmpc", title: "취득금액" }, + // { id: "lgalDrblyr", title: "내용연수" }, + // { id: "remLife", title: "잔존수명" }, + { id: "mqltCd", title: "재질" }, + { id: "pdmtVl", title: "관경(mm)" }, + { id: "prlgVl", title: "연장(m)" }, + { id: "instlYmd", title: "설치일자" }, + { id: "strc_stts_grd", title: "구조적 상태등급" }, + { id: "oper_stts_grd", title: "운영적 상태등급" }, + ]; + + const fetchOperationFilteredData = async () => { + const RequestDTO: Record = { + page: `${curPage - 1}`, + size: `${OPERATION_TABLE_SIZE}`, + }; + if (var1) RequestDTO.bizInfoNm = var1; + if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2; + // 수행기간 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + RequestDTO.fromflfmtEndYmd = startDate; + RequestDTO.toflfmtEndYmd = endDate; + + // JSON.parse 사용으로 문자열 -> 배열 변환 + RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); + + const params = new URLSearchParams(RequestDTO).toString(); + + const res = await getData(`v1/bizInfo?${params}`); + + const mappedContent = (res.content || []).map((item: any, index: number) => { + const formattedItem = formatDisplayData(item); + const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd); + return { + ...formattedItem, + rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가 + bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd, + }; + }); + + setTableData(mappedContent || []); + setTableCount(res.totalElements || 0); + setDetailTableData([]); + setDetailTableCount(0); + }; + + // 운영정보 테이블 cellClick 핸들러 + const handleOperationCellClick = async (index?: number | undefined, headId?: String) => { + setIsResetDetailPage(true); // 페이지를 1로 초기화 + setIsResetDetailSelection(true); // 선택 초기화 + setIsResetDetailPage(true); // 디테일 페이지를 초기화 + + if (typeof index === "number") { + setIsDetailLoading(true); + setDetailBizId(tableData[index].bizId); + setDetailCurPage(1); // 상세 페이지 초기화 + try { + const RequestDTO: Record = { + page: `${detailCurPage - 1}`, + size: `${OPERATION_TABLE_SIZE}`, + bizId: tableData[index % OPERATION_TABLE_SIZE].bizId, + }; + + RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); + + const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); + + const mappedData = (res.data || []).map((item: any, detailIndex: number) => { + const formattedItem = formatDisplayData(item); + + const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim()); + const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim()); + return { + ...formattedItem, + rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1, + usgCd: usgCdObj ? usgCdObj.text : item.usgCd, + mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd, + swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-', + }; + }); + + setDetailTableData(mappedData); + setDetailTableCount(res.count || 0); + } catch (error) { + console.error('운영정보 cellClick 에러:', error); + } finally { + setIsDetailLoading(false); + setIsResetDetailPage(false); + setIsResetDetailSelection(false); + } + } + }; + + // 운영정보 디테일 테이블 cellClick 핸들러 + const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => { + if (typeof index === "number") { + setIsLoading(true); + + const row = detailTableData[index]; + const layerNm = row.lyrTypeNm; + const fid = row.fid; + const core = GisApp.Module.layer as any; + await core.showSearchResultsFromGeoJSON(layerNm, fid); + setIsLoading(false); + setIsSearchModalOpen(false); // SearchModal 닫기 + } + }; + + // 운영정보 디테일 테이블 페이지네이션 핸들러 + const handleOperationPageNation = async (index?: number | undefined) => { + if (typeof index === "number") { + setIsDetailLoading(true); + try { + const RequestDTO: Record = { + page: `${detailCurPage - 1}`, + size: `${OPERATION_TABLE_SIZE}`, + bizId: String(detailBizId), + }; + const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); + + const mappedData = (res.data || []).map((item: any, detailIndex: number) => { + const formattedItem = formatDisplayData(item); + + const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim()); + const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim()); + return { + ...formattedItem, + rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1, + usgCd: usgCdObj ? usgCdObj.text : item.usgCd, + mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd, + swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-', + }; + }); + + setDetailTableData(mappedData); + setDetailTableCount(res.count || 0); + } catch (error) { + console.error('운영정보 페이지네이션 에러:', error); + } finally { + setIsDetailLoading(false); + } + } + }; + + // 운영정보 Excel 다운로드 핸들러 추가 + const operationExcelDownload = async () => { + if (tableData.length === 0) { + alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); + return; + } + + toast.dismiss("global-confirm-toast"); + + confirmToast( + "파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?", + onAnalysis ?? (() => { }), + "text-xl font-medium text-gray-800" + ); + + async function onAnalysis() { + setIsLoading(true); + + try { + const RequestDTO: Record = { + size: '10000', + }; + if (var1) RequestDTO.bizInfoNm = var1; + if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2; + // 수행기간 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + RequestDTO.fromflfmtEndYmd = startDate; + RequestDTO.toflfmtEndYmd = endDate; + + // JSON.parse 사용으로 문자열 -> 배열 변환 + RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); + + const params = new URLSearchParams(RequestDTO).toString(); + const res = await getData(`v1/bizInfo?${params}`); + + // 헤더 매핑 정의 + const headerMapping: { [key: string]: string } = { + 'bizInfoNm': '사업명', + 'flfmtBgngYmd': '수행시작일자', + 'flfmtEndYmd': '수행종료일자', + 'flfmtYr': '수행년도', + 'bizTypeCd': '사업유형', + 'bizAmt': '사업금액(원)', + }; + + const bizTyCdMap = new Map(BIZ_TY_CD.map(code => [code.lwpoCd, code.lwpoCdNm])); + + const mappedContent = (res.content || []).map((item: any) => { + const formattedItem = formatDisplayData(item); + + return { + bizInfoNm: formattedItem.bizInfoNm ?? item.bizInfoNm ?? '', + flfmtBgngYmd: formattedItem.flfmtBgngYmd ?? item.flfmtBgngYmd ?? '', + flfmtEndYmd: formattedItem.flfmtEndYmd ?? item.flfmtEndYmd ?? '', + flfmtYr: formattedItem.flfmtYr ?? item.flfmtYr ?? '', + bizTypeCd: bizTyCdMap.get(item.bizTypeCd) || item.bizTypeCd || formattedItem.bizTypeCd || '', + bizAmt: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.bizAmt ?? item.bizAmt), + }; + }); + + // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 + const excelData = mappedContent; // 배열로 감싸서 전달 + + // 파일명 생성 (분석번호 기반) + const label = WORK_TYPES.find((type) => type.val === workType)?.text + const fileName = `${label} 사업 현황_${todayYear}${todayMonth}${todayDate}`; + + await downloadExcelWithKoreanHeaders({ + data: excelData as JSON[], + fileName: fileName, + sheetNameArray: [fileName], + headerRange: "A1:F1", + headerMapping: headerMapping + }); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + console.error("Excel 다운로드 중 오류 발생:", error); + alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); + } + } + }; + + // 운영정보 결과표 Excel 다운로드 핸들러 추가 + const operationResultExcelDownload = async () => { + if (detailTableData.length === 0) { + alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); + return; + } + + toast.dismiss("global-confirm-toast"); + + confirmToast( + "파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?", + onAnalysis ?? (() => { }), + "text-xl font-medium text-gray-800" + ); + + async function onAnalysis() { + + setIsLoading(true); + + try { + const RequestDTO: Record = { + size: '10000', + bizId: String(detailBizId), + }; + + const apiStartTime = performance.now(); + const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); + const apiEndTime = performance.now(); + + const mappingStartTime = performance.now(); + // 헤더 매핑 정의 + const headerMapping: { [key: string]: string } = { + 'fcltMngNo': '관리번호', + 'swtrmt_zone_nm': '처리구역', + 'usgCd': '용도', + 'mqltCd': '재질', + 'pdmtVl': '관경(mm)', + 'prlgVl': '연장(m)', + 'instlYmd': '설치일자', + 'strc_stts_grd': '구조적 상태등급', + 'oper_stts_grd': '운영적 상태등급', + }; + + const usgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val, code.text])); + const mqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val, code.text])); + + // 🎯 단일 반복문으로 최적화 - 각 아이템마다 필요한 필드만 직접 매핑 + const mappedData = (res.data || []).map((item: any) => { + const formattedItem = formatDisplayData(item); + + // trim 한 번만 수행 + const trimmedUsgCd = item?.usgCd?.trim(); + const trimmedMqltCd = item?.mqltCd?.trim(); + + // zone 값 한 번만 계산 + const zone = item.swtrmtZoneNm ?? formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? ''; + + // 직접 매핑 - 반복문 없이 한 번에 객체 생성 + return { + fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '', + swtrmt_zone_nm: (zone === null || zone === '') ? '-' : zone, + usgCd: usgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '', + mqltCd: mqltCdMap.get(trimmedMqltCd) || item.mqltCd || formattedItem.mqltCd || '', + pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '', + prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '', + instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '', + strc_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.strc_stts_grd ?? item.strc_stts_grd), + oper_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.oper_stts_grd ?? item.oper_stts_grd), + }; + }); + + // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 + const excelData = mappedData; // 배열로 감싸서 전달 + + // 파일명 생성 (분석번호 기반) + const label = WORK_TYPES.find((type) => type.val === workType)?.text + const fileName = `${label} 결과표_${todayYear}${todayMonth}${todayDate}`; + + await downloadExcelWithKoreanHeaders({ + data: excelData as JSON[], + fileName: fileName, + sheetNameArray: [fileName], + headerRange: "A1:I1", + headerMapping: headerMapping + }); + + setIsLoading(false); + } catch (error) { + setIsLoading(false); + console.error("Excel 다운로드 중 오류 발생:", error); + alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); + } + } + }; + + + return { + WORK_TYPES, + OPERATION_TABLE_HEADLIST, + MAINTENANACE_TABLE_HEADLIST, + CLEANING_TABLE_HEADLIST, + INSPECTION_TABLE_HEADLIST, + OPERATION_TABLE_SIZE, + fetchOperationFilteredData, + handleOperationCellClick, + handleOperationResultCellClick, + handleOperationPageNation, + operationExcelDownload, + operationResultExcelDownload + }; +}; \ No newline at end of file diff --git a/apps/sn/app/(map-layout)/map/searchModal/useReportTab.ts b/apps/sn/app/(map-layout)/map/searchModal/useReportTab.ts new file mode 100644 index 000000000..52fabc9f0 --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/useReportTab.ts @@ -0,0 +1,173 @@ +import { getDcData } from '@/components/lib/api'; +import { fileDownload } from '@/utils'; +import { useReportTabStore } from '@/store/useReportTabStore'; +import useSearchModalStore from '@/app/store/useSearchModalStore'; + +export type Folder = { + clsfSystmSn: string; + upClsfSystmSn: string; + clsfSystmNm: string; + children: Folder[]; +}; +export type FlattenedFolder = { + [key: string]: string | number | undefined; +}; +export type SelectDataType = { + text: string; + val: string; + sido?: string; // 시도 코드 (선택 사항) +} +export type ReportDataFile = { + no: number; + fileTitle: string; + regDt: string; + fileSn: string; + filePath: string; +} + +export default function useReportTab() { + const REPORT_TYPES = [ + { val: '0', text: '전체' }, + { val: '1', text: '보고서' }, + { val: '2', text: '요약보고서' }, + { val: '3', text: '부록' }, + { val: '4', text: '기타' } + ]; + + const { curPage, setIsLoading } = useSearchModalStore(); + const { dcFiles: filesOrigin, files, setFiles } = useReportTabStore(); + + /** + * Flatten a hierarchical folder structure into a flat array. + * @param data - The hierarchical folder data to flatten. + * @returns An array of flattened folders with their paths and levels. + */ + const flattenFolders = (data: Folder[]): FlattenedFolder[] => { + const result: FlattenedFolder[] = []; + const exceptNodeNm = ["부분", "통합", "전체"]; + const addNodeNm = ["보고서", "요약보고서", "부록", "기타"]; // level5 + const columnNms: { [key: number]: string } = { + 0: "root", 1: "sido", 2: "sigungu", 3: "approvalYear" + , 4: "plantZoneGubun", 5: "reportGubun", 7: "fileName", 8: "regDate" + }; + + const traverse = ( + node: Folder, + path: { [key: string]: string | number }, + level: number + ) => { + const isYear = (value: string): boolean => { + return /^[0-9]{4}년$/.test(value); + }; + + if (level === 2 && isYear(node.clsfSystmNm)) { + level++; + } + if (level === 4 && addNodeNm.includes(node.clsfSystmNm)) { + level++; // 레벨을 증가시켜 하위값에 추가되도록 함. + } + + const newPath = { + ...path, + [`${columnNms[level]}`]: node.clsfSystmNm, + }; + + if (node.children.length === 0) { + newPath.no = result.length + 1; + newPath.clsfSystmSn = node.clsfSystmSn; // 파일 조회를 위한 고유번호 + result.push(newPath); + } else { + if (level === 4) { + if (exceptNodeNm.includes(node.clsfSystmNm)) { + level--; // 제외할 노드 이름인 경우, 현재의 레벨에 하위값이 써지도록 함. + } + } + node.children.forEach((child) => { + traverse(child, newPath, level + 1); + }); + } + } + + if (data && data.length > 0 && data[0]?.clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리 + data.forEach((root) => traverse(root, {}, 0)); + } else { + data.forEach((root) => traverse(root, {}, 1)); + } + return result; + } + + const getDcFiles = async (clsfSystmSn: string): Promise => { + if (!clsfSystmSn) { + console.error("clsfSystmSn is required to get files."); + return []; + } + const queryString = 'clsfSystmSn=' + clsfSystmSn; + const result = await getDcData(`/api/v1/dc/file${queryString ? `?${queryString}` : ''}`); + return result; + }; + + // 파일 이름 추가 + const addFileName = async (data: FlattenedFolder[]) => { + const fileReportData = await Promise.all( + data.map(async (item) => { + const fileArr: ReportDataFile[] = await getDcFiles(String(item.clsfSystmSn)); + let result: FlattenedFolder; + if (fileArr.length > 0) { + const f = fileArr[0]; + const fCount = fileArr.length; + result = { ...item, fileName: f?.fileTitle + " 외 " + fCount + "건", regDate: f?.regDt, fileSn: f?.fileSn, filePath: f?.filePath }; + } else { + result = { ...item, fileName: "", regDate: "", fileSn: "", filePath: "" }; + } + return result; + }) + ); + return fileReportData; + }; + + const getDcFileDownload = async (fileSn: string, fileName: string, filePath: string) => { + if (filePath === "") { + alert(`filePath 정보가 없으므로 다운로드가 불가합니다.\nfilesn: ${fileSn}`); + return; + } + const queryString = 'filePath=' + encodeURIComponent(filePath); + const responseBlob = await getDcData(`/api/v1/dc/file/download${queryString ? `?${queryString}` : ''}`, "blob"); + fileDownload(responseBlob, fileName); + } + + // 파일 목록 조회 + const getFiles = async () => { + setIsLoading(true); // 로딩 시작 + try { + let data = filesOrigin; + data = data?.slice((curPage - 1) * 10, curPage * 10); // paging + data = data?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 })); + setFiles(data); + } catch (error) { + console.error("파일 목록 조회 에러:", error); + } finally { + setIsLoading(false); // 로딩 종료 + } + }; + + // cellClick 핸들러 + const handleCellClick = (index: number | undefined) => { + if (typeof index === "number" && files[index]) { + const fileName = files[index].fileTitle; + const filePath = files[index].filePath; + if (files[index].fileSn !== "") { + getDcFileDownload(files[index].fileSn, fileName, filePath); + } + } + }; + + return { + REPORT_TYPES, + flattenFolders, + addFileName, + getDcFiles, + getDcFileDownload, + getFiles, + handleCellClick + }; +} diff --git a/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.ts b/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.ts new file mode 100644 index 000000000..ba376290d --- /dev/null +++ b/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.ts @@ -0,0 +1,171 @@ +import { useEffect } from 'react'; +import { getData } from "@/components/lib/api"; +import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; +import useStore from '../../../store/useStore'; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import useSearchModalStore from '../../../store/useSearchModalStore'; +import useBasicTab from './useBasicTab'; +import useOperationTab from './useOperationTab'; + +export default function useSearchModal() { + const { + activeTab, basicType, workType, + showTable, + tableData, detailTableData, + curPage, detailCurPage, + detailBizId, + var1, var2, + yearFilter, + todayYear, todayMonth, todayDate, + setIsSearchModalOpen, + setShowTable, + setTableData, setDetailTableData, + setTableCount, setDetailTableCount, + setCurPage, + setIsLoading, + setIsResetPage, setIsResetDetailPage, + setIsResetSelection, setIsResetDetailSelection, + setDetailBizId, + resetFilters, + getDateString + } = useSearchModalStore(); + const { fetchBasicFilteredData } = useBasicTab(); + const { OPERATION_TABLE_SIZE, handleOperationPageNation } = useOperationTab(); + const { BIZ_TY_CD } = useStore(); + + const TABS = [ + { val: "basic", text: "기본정보" }, + { val: "operation", text: "운영정보" }, + { val: "report", text: "하수도정비기본계획보고서" }, + ]; + + useEffect(() => { + resetFilters(); + }, [activeTab, basicType, workType]); + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + setIsResetPage(false); + setIsResetDetailPage(true); + setDetailBizId(0); + console.log(`curPage: ${curPage}, showTable: ${showTable}`); + if (showTable && curPage > 1) fetchFilteredData(); + }, [curPage]); + + // 페이지 변경 시마다 데이터 fetch + useEffect(() => { + setIsResetDetailSelection(false); + if (showTable) handleOperationPageNation(Number(detailBizId)); + }, [detailCurPage]); + + // 검색 API 호출 함수 + const fetchFilteredData = async () => { + setIsLoading(true); // 로딩 시작 + setIsResetSelection(true); // 선택 초기화 + setIsResetDetailSelection(true); // 선택 초기화 + + try { + if (activeTab === 'basic') { + fetchBasicFilteredData(); + + } else if (activeTab === 'operation') { + const RequestDTO: Record = { + page: `${curPage - 1}`, + size: `${OPERATION_TABLE_SIZE}`, + }; + if (var1) RequestDTO.bizInfoNm = var1; + if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2; + // 수행기간 범위 + const startDate = getDateString( + yearFilter.start, + '', + '', + "19000101", + ); + const endDate = getDateString( + yearFilter.end, + '', + '', + `${todayYear}${todayMonth}${todayDate}`, + ); + RequestDTO.fromflfmtEndYmd = startDate; + RequestDTO.toflfmtEndYmd = endDate; + + // JSON.parse 사용으로 문자열 -> 배열 변환 + RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); + + const params = new URLSearchParams(RequestDTO).toString(); + + const res = await getData(`v1/bizInfo?${params}`); + + const mappedContent = (res.content || []).map((item: any, index: number) => { + const formattedItem = formatDisplayData(item); + const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd); + return { + ...formattedItem, + rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가 + bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd, + }; + }); + + setTableData(mappedContent || []); + setTableCount(res.totalElements || 0); + setDetailTableData([]); + setDetailTableCount(0); + } + + setShowTable(true); + setIsResetSelection(false); + } catch (err) { + console.error('API ERROR:', err); + setTableData([]); + setIsResetSelection(false); + } finally { + setIsLoading(false); // 로딩 종료 + setIsResetSelection(false); + // setIsResetDetailSelection(false); + } + }; + + // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 + const handleSearch = () => { + setCurPage(1); // 페이지를 1로 초기화 + fetchFilteredData(); + setIsResetPage(true); // 페이지를 초기화 + setIsResetDetailPage(true); // 디테일 페이지를 초기화 + setIsResetSelection(false); // 선택 초기화 + setIsResetDetailSelection(false); // 선택 초기화 + setShowTable(true); + }; + + // 여러 시설 보기 + const viewMultipleFeature = async () => { + if (activeTab === 'basic') { + if (!(tableData.length === 0)) { + setIsLoading(true); + + const row = tableData[0]; + const layerNm = row.lyrTypeNm; + const fids = tableData.map((item) => item.fid); + const core = GisApp.Module.layer as any; + await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids); + setIsLoading(false); + setIsSearchModalOpen(false); + } + } else if (activeTab === 'operation') { + if (!(detailTableData.length === 0)) { + setIsLoading(true); + + const row = detailTableData[0]; + const layerNm = row.lyrTypeNm; + const fids = detailTableData.map((item) => item.fid); + const core = GisApp.Module.layer as any; + await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids); + setIsLoading(false); + setIsSearchModalOpen(false); + } + } + } + + return { TABS, getDateString, handleSearch, viewMultipleFeature }; +}; diff --git a/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.tsx deleted file mode 100644 index 30e7c232a..000000000 --- a/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.tsx +++ /dev/null @@ -1,841 +0,0 @@ -import { useEffect, useState } from 'react'; -import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui'; -import axios from 'axios'; -import { - MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES, - MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol -} from '../components/common/table'; -import { getData } from "@/components/lib/api"; -import { BASIC_TYPES } from './basicTab'; -import { WORK_TYPES } from './operationTab'; -import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue'; -import useStore from '../../../store/useStore'; -import GisApp from "@common/business_components/ui/map/lib/gis.core"; -import { confirmToast } from '@/components/ui/toast' -import { toast } from "react-hot-toast"; -import useSearchModalStore from '../../../store/useSearchModalStore'; -import { swagePplCqlFilters } from './swagePpl'; -import { swagePrcsFcltCqlFilters } from './swagePrcsFclt'; -import { swagePrcsZoneCqlFilters } from './swagePrcsZone'; -import { swageMhlCqlFilters } from './swageMhl'; -import { swagePmstnCqlFilters } from './swagePmstn'; -import { stofcbrCqlFilters } from './stofcbr'; - -const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; -export const OPERATION_TABLE_SIZE = 5; - -export interface SearchModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onFeatureSelect: (row: any) => void; -} - -export function useSearchModal({ open, onOpenChange, onFeatureSelect }: SearchModalProps) { - const { - activeTab, basicType, workType, - showTable, - tableData, detailTableData, - curPage, detailCurPage, - detailBizId, - ctpvCd, ctpvName, sggCd, sggName, - start1, start2, end1, end2, - var1, var2, - yearFilter, - todayYear, todayMonth, todayDate, - setShowTable, - setTableData, setDetailTableData, - setTableCount, setDetailTableCount, - setCurPage, setDetailCurPage, - setIsLoading, setIsDetailLoading, - setIsResetPage, setIsResetDetailPage, - setIsResetSelection, setIsResetDetailSelection, - setDetailBizId, - resetFilters, - getDateString - } = useSearchModalStore(); - const { ctpv, sgg, BIZ_TY_CD } = useStore(); - - const TABS = [ - { val: "basic", text: "기본정보" }, - { val: "operation", text: "운영정보" }, - { val: "report", text: "하수도정비기본계획보고서" }, - ]; - - - useEffect(() => { - resetFilters(); - }, [activeTab, basicType, workType]); - - // 페이지 변경 시마다 데이터 fetch - useEffect(() => { - setIsResetPage(false); - setIsResetDetailPage(true); - setDetailBizId(0); - if (showTable) fetchFilteredData(); - }, [curPage]); - - // 페이지 변경 시마다 데이터 fetch - useEffect(() => { - setIsResetDetailSelection(false); - if (showTable) handleOperationPageNation(Number(detailBizId)); - }, [detailCurPage]); - - - // 검색 API 호출 함수 - const fetchFilteredData = async () => { - setIsLoading(true); // 로딩 시작 - setIsResetSelection(true); // 선택 초기화 - setIsResetDetailSelection(true); // 선택 초기화 - - try { - let cqlFilters: string[] = []; - if (activeTab === 'basic') { - const queryParams = new URLSearchParams({ - lyrTypeNm: basicType, - page: `${curPage - 1}`, - size: '10', - sort: 'string', - }); - - if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = swagePrcsZoneCqlFilters({ - ctpvCd, sggCd, - var1, var2 - }); // 하수처리구역 - else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = swagePrcsFcltCqlFilters({ - ctpvName, sggName, - yearFilter, - start1, end1, - todayYear, todayMonth, todayDate, - getDateString - }); // 하수처리시설 - else if (basicType === 'tb_ms_swage_ppl') cqlFilters = swagePplCqlFilters({ - var1, var2, - ctpvCd, sggCd, - yearFilter, - start1, start2, end1, end2, - todayYear, todayMonth, todayDate, - getDateString - }); // 하수관로 - else if (basicType === 'tb_ms_swage_mhl') cqlFilters = swageMhlCqlFilters({ - ctpvCd, sggCd, - yearFilter, - start1, end1, - var1, - todayYear, todayMonth, todayDate, - getDateString - }); // 하수맨홀 - else if (basicType === 'tb_ms_swage_pmstn') cqlFilters = swagePmstnCqlFilters({ - ctpvCd, sggCd, - yearFilter, - start1, start2, end1, end2, - todayYear, todayMonth, todayDate, - getDateString - }); // 하수펌프장 - else if (basicType === 'tb_ms_stofcbr') cqlFilters = stofcbrCqlFilters({ - ctpvCd, sggCd, - yearFilter, - todayYear, todayMonth, todayDate, - getDateString - }); // 우수토실 - - if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and ')); - - const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`); - - // 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가 - const dataWithRowNumber = (res.data.data || []).map((item: any, index: number) => { - // 🎯 먼저 각 아이템을 포맷팅 - const formattedItem = formatDisplayData(item); - - let ctpvCd = item?.ctpvNm ?? formattedItem?.ctpvNm ?? ''; - let sggCd = item?.sggNm ?? formattedItem?.sggNm ?? ''; - - if (!ctpvCd || !sggCd) { - const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim(); - if (!ctpvCd && code) ctpvCd = code.slice(0, 2); - if (!sggCd && code) sggCd = code.slice(0, 5); - } - - const matchedMhlCode = MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim()); - const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd); - const ctpvNm = ctpv.find(code => code.val.trim() === ctpvCd)?.text || item?.ctpvNm; - const sggNm = sgg.find(code => code.val.trim() === sggCd)?.text || item?.sggNm; - const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim()); - - // 추가 처리 및 일련번호 추가 - return { - ...formattedItem, - rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가 - scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item?.scsYn, // scsYn이 null이면 'N'으로 변경 - mqltCd: matchedMhlCode ? matchedMhlCode.text : item?.mqltCd, - usgCd: matchedMhlUsgCode ? matchedMhlUsgCode.text : (usgCdObj ? usgCdObj.text : item?.usgCd), - ctpvNm: ctpvNm ?? '-', - sggNm : sggNm ?? '-', - swtrmt_zone_nm: item.swtrmtZoneNm || item.swtrmt_zone_nm || '-', - }; - }); - - setTableData(dataWithRowNumber); - setTableCount(res.data.count || []); - - } else if (activeTab === 'operation') { - const RequestDTO: Record = { - page: `${curPage - 1}`, - size: `${OPERATION_TABLE_SIZE}`, - }; - if (var1) RequestDTO.bizInfoNm = var1; - if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2; - // 수행기간 범위 - const startDate = getDateString( - yearFilter.start, - '', - '', - "19000101", - ); - const endDate = getDateString( - yearFilter.end, - '', - '', - `${todayYear}${todayMonth}${todayDate}`, - ); - RequestDTO.fromflfmtEndYmd = startDate; - RequestDTO.toflfmtEndYmd = endDate; - - // JSON.parse 사용으로 문자열 -> 배열 변환 - RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); - - const params = new URLSearchParams(RequestDTO).toString(); - - const res = await getData(`v1/bizInfo?${params}`); - - const mappedContent = (res.content || []).map((item: any, index: number) => { - const formattedItem = formatDisplayData(item); - const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd); - return { - ...formattedItem, - rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가 - bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd, - }; - }); - - setTableData(mappedContent || []); - setTableCount(res.totalElements || 0); - setDetailTableData([]); - setDetailTableCount(0); - } - - setShowTable(true); - setIsResetSelection(false); - } catch (err) { - console.error('API ERROR:', err); - setTableData([]); - setIsResetSelection(false); - } finally { - setIsLoading(false); // 로딩 종료 - setIsResetSelection(false); - // setIsResetDetailSelection(false); - } - }; - - // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 - const handleSearch = () => { - setCurPage(1); // 페이지를 1로 초기화 - fetchFilteredData(); - setIsResetPage(true); // 페이지를 초기화 - setIsResetDetailPage(true); // 디테일 페이지를 초기화 - setIsResetSelection(false); // 선택 초기화 - setIsResetDetailSelection(false); // 선택 초기화 - setShowTable(true); - }; - - // 기본정보 테이블 cellClick 핸들러 - const handleBasicCellClick = async (index?: number | undefined, headId?: String) => { - if (typeof index === "number") { - setIsLoading(true); - - const row = tableData[index]; - const layerNm = row.lyrTypeNm; - const fid = row.fid; - const core = GisApp.Module.layer as any; - await core.showSearchResultsFromGeoJSON(layerNm, fid); - setIsLoading(false); - onOpenChange(false); // SearchModal 닫기 - } - }; - // 운영정보 테이블 cellClick 핸들러 - const handleOperationCellClick = async (index?: number | undefined, headId?: String) => { - setIsResetDetailPage(true); // 페이지를 1로 초기화 - setIsResetDetailSelection(true); // 선택 초기화 - setIsResetDetailPage(true); // 디테일 페이지를 초기화 - - if (typeof index === "number") { - setIsDetailLoading(true); - setDetailBizId(tableData[index].bizId); - setDetailCurPage(1); // 상세 페이지 초기화 - try { - const RequestDTO: Record = { - page: `${detailCurPage - 1}`, - size: `${OPERATION_TABLE_SIZE}`, - bizId: tableData[index % OPERATION_TABLE_SIZE].bizId, - }; - - RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); - - const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); - - const mappedData = (res.data || []).map((item: any, detailIndex: number) => { - const formattedItem = formatDisplayData(item); - - const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim()); - const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim()); - return { - ...formattedItem, - rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1, - usgCd: usgCdObj ? usgCdObj.text : item.usgCd, - mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd, - swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-', - }; - }); - - setDetailTableData(mappedData); - setDetailTableCount(res.count || 0); - } catch (error) { - console.error('운영정보 cellClick 에러:', error); - } finally { - setIsDetailLoading(false); - setIsResetDetailPage(false); - setIsResetDetailSelection(false); - } - } - }; - // 운영정보 디테일 테이블 페이지네이션 핸들러 - const handleOperationPageNation = async (index?: number | undefined) => { - if (typeof index === "number") { - setIsDetailLoading(true); - try { - const RequestDTO: Record = { - page: `${detailCurPage - 1}`, - size: `${OPERATION_TABLE_SIZE}`, - bizId: String(detailBizId), - }; - const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); - - const mappedData = (res.data || []).map((item: any, detailIndex: number) => { - const formattedItem = formatDisplayData(item); - - const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim()); - const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim()); - return { - ...formattedItem, - rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1, - usgCd: usgCdObj ? usgCdObj.text : item.usgCd, - mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd, - swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-', - }; - }); - - setDetailTableData(mappedData); - setDetailTableCount(res.count || 0); - } catch (error) { - console.error('운영정보 페이지네이션 에러:', error); - } finally { - setIsDetailLoading(false); - } - } - }; - - const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => { - if (typeof index === "number") { - setIsLoading(true); - - const row = detailTableData[index]; - const layerNm = row.lyrTypeNm; - const fid = row.fid; - const core = GisApp.Module.layer as any; - await core.showSearchResultsFromGeoJSON(layerNm, fid); - setIsLoading(false); - onOpenChange(false); // SearchModal 닫기 - } - }; - - // 기본정보 Excel 다운로드 핸들러 - const basicExcelDownload = async () => { - if (tableData.length === 0) { - alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); - return; - } - - toast.dismiss("global-confirm-toast"); - - confirmToast( - "파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?", - onAnalysis ?? (() => { }), - "text-xl font-medium text-gray-800" - ); - - async function onAnalysis() { - setIsLoading(true); - - try { - const queryParams = new URLSearchParams({ - lyrTypeNm: basicType, - size: '10000', - }); - - let cqlFilters: string[] = []; - let headerMapping: { [key: string]: string } = {}; - let headerRange: string = "A1:F1"; - - switch (basicType) { - case 'tb_ms_swage_ppl': { - // 하수관로 - cqlFilters = swagePplCqlFilters({ - var1, var2, - ctpvCd, sggCd, - yearFilter, - start1, start2, end1, end2, - todayYear, todayMonth, todayDate, - getDateString - }) - headerMapping = MsSwagePplExcelCol; - headerRange = "A1:M1"; - break; - } - case 'tb_il_swage_prcs_fclt': { - // 하수처리시설 - cqlFilters = swagePrcsFcltCqlFilters({ - ctpvName, sggName, - yearFilter, - start1, end1, - todayYear, todayMonth, todayDate, - getDateString - }); - headerMapping = SwtrmtFcltExcelCol; - headerRange = "A1:J1"; - break; - } - case 'tb_ms_swage_prcs_zone': { - // 하수처리구역 - cqlFilters = swagePrcsZoneCqlFilters({ - ctpvCd, sggCd, - var1, var2 - }); - headerMapping = SwtrmtPrcsZoneExcelCol; - headerRange = "A1:D1"; - break; - } - case 'tb_ms_swage_mhl': { - // 하수맨홀 - cqlFilters = swageMhlCqlFilters({ - ctpvCd, sggCd, - yearFilter, - start1, end1, - var1, - todayYear, todayMonth, todayDate, - getDateString - }); - headerMapping = MsSwageMhlExcelCol; - headerRange = "A1:J1"; - break; - } - case 'tb_ms_swage_pmstn': { - // 하수펌프장 - cqlFilters = swagePmstnCqlFilters({ - ctpvCd, sggCd, - yearFilter, - start1, start2, end1, end2, - todayYear, todayMonth, todayDate, - getDateString - }); - headerMapping = ExclnSewagePumpExcelCol; - headerRange = "A1:J1"; - break; - } - case 'tb_ms_stofcbr': { - // 우수토실 - cqlFilters = stofcbrCqlFilters({ - ctpvCd, sggCd, - yearFilter, - todayYear, todayMonth, todayDate, - getDateString - }); - headerMapping = stofcbrExcelCol; - headerRange = "A1:H1"; - break; - } - } - - if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and ')); - - const res = await axios.get(`${API_URL}/v1/fclts?${queryParams.toString()}`); - - - - - // const mhlCdMap = new Map(MS_SWAGE_MHL_CODES.map(code => [code.val?.trim(), code.text])); - const mhlUsgCdMap = new Map(MS_SWAGE_MHL_USE_CODES.map(code => [code.val?.trim(), code.text])); - const pplUsgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val?.trim(), code.text])); - const ctpvMap = new Map(ctpv.map(code => [code.val?.trim(), code.text])); - const sggMap = new Map(sgg.map(code => [code.val?.trim(), code.text])); - - const mappedContent = (res.data.data || []).map((item: any) => { - const formattedItem = formatDisplayData(item); - - let ctpvCd = item?.ctpvCd ?? formattedItem?.ctpvCd ?? ''; - let sggCd = item?.sggCd ?? formattedItem?.sggCd ?? ''; - - if (!ctpvCd || !sggCd) { - const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim(); - if (!ctpvCd && code) ctpvCd = code.slice(0, 2); - if (!sggCd && code) sggCd = code.slice(0, 5); - } - - // trim 한 번만 수행 - const trimmedUsgCd = item?.usgCd?.trim(); - // const trimmedMqltCd = item?.mqltCd?.trim(); - - switch (basicType) { - case 'tb_ms_swage_ppl': { - // 하수관로 - return { - fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-', - instMngNo: formattedItem.instMngNo ?? item.instMngNo ?? '-', - ctpvNm: ctpvMap.get(ctpvCd) ?? item.ctpvNm ?? '-', - sggNm: sggMap.get(sggCd) ?? item.sggNm ?? '-', - swtrmt_zone_nm: formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '-', - instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-', - usgCd: pplUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-', - pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '-', - prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '-', - tmptDpth: formattedItem.tmptDpth ?? item.tmptDpth ?? '-', - tmptMhlMngNo: formattedItem.tmptMhlMngNo ?? item.tmptMhlMngNo ?? '-', - scendDpth: formattedItem.scendDpth ?? item.scendDpth ?? '-', - scendMhlMngNo: formattedItem.scendMhlMngNo ?? item.scendMhlMngNo ?? '-', - }; - } - case 'tb_il_swage_prcs_fclt': { - // 하수처리시설 - return { - swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-', - fcltCpct: formattedItem.fcltCpct ?? item.fcltCpct ?? '-', - rgnExpln: formattedItem.rgnExpln ?? item.rgnExpln ?? '-', - oprtngStrtYmd: formattedItem.oprtngStrtYmd ?? item.oprtngStrtYmd ?? '-', - laeNm: formattedItem.laeNm ?? item.laeNm ?? '-', - sareaNm: formattedItem.sareaNm ?? item.sareaNm ?? '-', - rvrNm: formattedItem.rvrNm ?? item.rvrNm ?? '-', - dschgWtstNm: formattedItem.dschgWtstNm ?? item.dschgWtstNm ?? '-', - swtrmtPpltnCnt: formattedItem.swtrmtPpltnCnt ?? item.swtrmtPpltnCnt ?? '-', - rprsMsrcCn: formattedItem.rprsMsrcCn ?? item.rprsMsrcCn ?? '-' - }; - } - case 'tb_ms_swage_prcs_zone': { - // 하수처리구역 - return { - swtrmtZoneNm: formattedItem.swtrmtZoneNm ?? item.swtrmtZoneNm ?? '-', - ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-', - sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-', - swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-' - }; - } - case 'tb_ms_swage_mhl': { - // 하수맨홀 - return { - fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-', - ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-', - sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-', - instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-', - usgCd: mhlUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-', - swageMhlDiamVl: formattedItem.swageMhlDiamVl ?? item.swageMhlDiamVl ?? '-', - wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-', - vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-', - altdVl: formattedItem.altdVl ?? item.altdVl ?? '-', - dpthlVl: formattedItem.dpthlVl ?? item.dpthlVl ?? '-' - }; - } - case 'tb_ms_swage_pmstn': { - // 하수펌프장 - return { - fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-', - ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-', - sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-', - swagePmstnNm: formattedItem.swagePmstnNm ?? item.swagePmstnNm ?? '-', - instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-', - dailyPrcsCpct: formattedItem.dailyPrcsCpct ?? item.dailyPrcsCpct ?? '-', - maxImpndCpct: formattedItem.maxImpndCpct ?? item.maxImpndCpct ?? '-', - altdVl: formattedItem.altdVl ?? item.altdVl ?? '-', - wtlvVl: formattedItem.wtlvVl ?? item.wtlvVl ?? '-', - exclnWpmpAbltVl: formattedItem.exclnWpmpAbltVl ?? item.exclnWpmpAbltVl ?? '-' - }; - } - case 'tb_ms_stofcbr': { - // 우수토실 - return { - fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-', - mapshtNo: formattedItem.mapshtNo ?? item.mapshtNo ?? '-', - ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-', - sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-', - instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-', - worflwWallHgt: formattedItem.worflwWallHgt ?? item.worflwWallHgt ?? '-', - swageMhlMngNo: formattedItem.swageMhlMngNo ?? item.swageMhlMngNo ?? '-', - drtnAngl: formattedItem.drtnAngl ?? item.drtnAngl ?? '-' - }; - } - } - }); - - // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 - const excelData = mappedContent; // 배열로 감싸서 전달 - - // 파일명 생성 (분석번호 기반) - const label = BASIC_TYPES.find((type) => type.val === basicType)?.text - const fileName = `${label} 조회 결과_${todayYear}${todayMonth}${todayDate}`; - - await downloadExcelWithKoreanHeaders({ - data: excelData as JSON[], - fileName: fileName, - sheetNameArray: [fileName], - headerRange: headerRange, - headerMapping: headerMapping - }); - setIsLoading(false); - } catch (error) { - setIsLoading(false); - console.error("Excel 다운로드 중 오류 발생:", error); - alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); - } - } - }; - - // 운영정보 Excel 다운로드 핸들러 추가 - const operationExcelDownload = async () => { - if (tableData.length === 0) { - alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); - return; - } - - toast.dismiss("global-confirm-toast"); - - confirmToast( - "파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?", - onAnalysis ?? (() => { }), - "text-xl font-medium text-gray-800" - ); - - async function onAnalysis() { - setIsLoading(true); - - try { - const RequestDTO: Record = { - size: '10000', - }; - if (var1) RequestDTO.bizInfoNm = var1; - if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2; - // 수행기간 범위 - const startDate = getDateString( - yearFilter.start, - '', - '', - "19000101", - ); - const endDate = getDateString( - yearFilter.end, - '', - '', - `${todayYear}${todayMonth}${todayDate}`, - ); - RequestDTO.fromflfmtEndYmd = startDate; - RequestDTO.toflfmtEndYmd = endDate; - - // JSON.parse 사용으로 문자열 -> 배열 변환 - RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); - - const params = new URLSearchParams(RequestDTO).toString(); - const res = await getData(`v1/bizInfo?${params}`); - - // 헤더 매핑 정의 - const headerMapping: { [key: string]: string } = { - 'bizInfoNm': '사업명', - 'flfmtBgngYmd': '수행시작일자', - 'flfmtEndYmd': '수행종료일자', - 'flfmtYr': '수행년도', - 'bizTypeCd': '사업유형', - 'bizAmt': '사업금액(원)', - }; - - const bizTyCdMap = new Map(BIZ_TY_CD.map(code => [code.lwpoCd, code.lwpoCdNm])); - - const mappedContent = (res.content || []).map((item: any) => { - const formattedItem = formatDisplayData(item); - - return { - bizInfoNm: formattedItem.bizInfoNm ?? item.bizInfoNm ?? '', - flfmtBgngYmd: formattedItem.flfmtBgngYmd ?? item.flfmtBgngYmd ?? '', - flfmtEndYmd: formattedItem.flfmtEndYmd ?? item.flfmtEndYmd ?? '', - flfmtYr: formattedItem.flfmtYr ?? item.flfmtYr ?? '', - bizTypeCd: bizTyCdMap.get(item.bizTypeCd) || item.bizTypeCd || formattedItem.bizTypeCd || '', - bizAmt: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.bizAmt ?? item.bizAmt), - }; - }); - - // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 - const excelData = mappedContent; // 배열로 감싸서 전달 - - // 파일명 생성 (분석번호 기반) - const label = WORK_TYPES.find((type) => type.val === workType)?.text - const fileName = `${label} 사업 현황_${todayYear}${todayMonth}${todayDate}`; - - await downloadExcelWithKoreanHeaders({ - data: excelData as JSON[], - fileName: fileName, - sheetNameArray: [fileName], - headerRange: "A1:F1", - headerMapping: headerMapping - }); - setIsLoading(false); - } catch (error) { - setIsLoading(false); - console.error("Excel 다운로드 중 오류 발생:", error); - alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); - } - } - }; - - // 운영정보 Excel 다운로드 핸들러 추가 - const operationResultExcelDownload = async () => { - if (detailTableData.length === 0) { - alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); - return; - } - - toast.dismiss("global-confirm-toast"); - - confirmToast( - "파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?", - onAnalysis ?? (() => { }), - "text-xl font-medium text-gray-800" - ); - - async function onAnalysis() { - - setIsLoading(true); - - try { - const RequestDTO: Record = { - size: '10000', - bizId: String(detailBizId), - }; - - const apiStartTime = performance.now(); - const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); - const apiEndTime = performance.now(); - - const mappingStartTime = performance.now(); - // 헤더 매핑 정의 - const headerMapping: { [key: string]: string } = { - 'fcltMngNo': '관리번호', - 'swtrmt_zone_nm': '처리구역', - 'usgCd': '용도', - 'mqltCd': '재질', - 'pdmtVl': '관경(mm)', - 'prlgVl': '연장(m)', - 'instlYmd': '설치일자', - 'strc_stts_grd': '구조적 상태등급', - 'oper_stts_grd': '운영적 상태등급', - }; - - const usgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val, code.text])); - const mqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val, code.text])); - - // 🎯 단일 반복문으로 최적화 - 각 아이템마다 필요한 필드만 직접 매핑 - const mappedData = (res.data || []).map((item: any) => { - const formattedItem = formatDisplayData(item); - - // trim 한 번만 수행 - const trimmedUsgCd = item?.usgCd?.trim(); - const trimmedMqltCd = item?.mqltCd?.trim(); - - // zone 값 한 번만 계산 - const zone = item.swtrmtZoneNm ?? formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? ''; - - // 직접 매핑 - 반복문 없이 한 번에 객체 생성 - return { - fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '', - swtrmt_zone_nm: (zone === null || zone === '') ? '-' : zone, - usgCd: usgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '', - mqltCd: mqltCdMap.get(trimmedMqltCd) || item.mqltCd || formattedItem.mqltCd || '', - pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '', - prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '', - instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '', - strc_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.strc_stts_grd ?? item.strc_stts_grd), - oper_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.oper_stts_grd ?? item.oper_stts_grd), - }; - }); - - // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 - const excelData = mappedData; // 배열로 감싸서 전달 - - // 파일명 생성 (분석번호 기반) - const label = WORK_TYPES.find((type) => type.val === workType)?.text - const fileName = `${label} 결과표_${todayYear}${todayMonth}${todayDate}`; - - await downloadExcelWithKoreanHeaders({ - data: excelData as JSON[], - fileName: fileName, - sheetNameArray: [fileName], - headerRange: "A1:I1", - headerMapping: headerMapping - }); - - setIsLoading(false); - } catch (error) { - setIsLoading(false); - console.error("Excel 다운로드 중 오류 발생:", error); - alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); - } - } - }; - - // 여러 시설 보기 - const viewMultipleFeature = async () => { - if (activeTab === 'basic') { - if (!(tableData.length === 0)) { - setIsLoading(true); - - const row = tableData[0]; - const layerNm = row.lyrTypeNm; - const fids = tableData.map((item) => item.fid); - const core = GisApp.Module.layer as any; - await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids); - setIsLoading(false); - onOpenChange(false); - } - } else if (activeTab === 'operation') { - if (!(detailTableData.length === 0)) { - setIsLoading(true); - - const row = detailTableData[0]; - const layerNm = row.lyrTypeNm; - const fids = detailTableData.map((item) => item.fid); - const core = GisApp.Module.layer as any; - await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids); - setIsLoading(false); - onOpenChange(false); - } - } - } - - return { - open, onOpenChange, onFeatureSelect, - TABS, - getDateString, - handleSearch, - handleBasicCellClick, - handleOperationCellClick, - handleOperationPageNation, - handleOperationResultCellClick, - basicExcelDownload, - operationExcelDownload, - operationResultExcelDownload, - viewMultipleFeature, - }; -}; diff --git a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx index a1434d895..be78c069c 100644 --- a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx +++ b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx @@ -1,7 +1,7 @@ "use client"; -import React, { useRef, useEffect, useState } from "react"; import { Label } from "@common/components/ui/label"; +import { useEffect } from "react"; export const WATER_DEPTH_LAYER_TYPES: string[] = [ 'tb_ms_flood_dngr_map', diff --git a/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx b/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx index e72f7f4a5..13402d09b 100644 --- a/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx @@ -9,11 +9,11 @@ function FileUpload() { const [file, setFile] = useState(null); const [text, setText] = useState(''); - const handleFileChange = (e) => { + const handleFileChange = (e: any) => { setFile(e.target.files[0]); }; - const handleTextChange = (e) => { + const handleTextChange = (e: any) => { setText(e.target.value); }; @@ -24,7 +24,7 @@ function FileUpload() { } return null; } - const handleFindUser = (e) => { + const handleFindUser = () => { const auth = getCookie("Authorization"); if (auth) console.log("쿠키 auth : " + auth); fetch(SEWERAGE_URL+"/v1/inftwtAnls/users/65a00f65-8460-49af-98ec-042977e56f4b", { @@ -37,7 +37,7 @@ function FileUpload() { console.log(data); }); }; - const handleFindUserOna = (e) => { + const handleFindUserOna = () => { const auth = getCookie("Authorization"); if (auth) console.log("쿠키 auth : " + auth); fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b").then((data) => { @@ -45,7 +45,7 @@ function FileUpload() { }); }; - const handleSubmit = async (e) => { + const handleSubmit = async (e: any) => { e.preventDefault(); if (!file) { diff --git a/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx b/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx index a23f3330f..08ac017be 100644 --- a/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx +++ b/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx @@ -100,7 +100,7 @@ const Page = () => { alert('파일을 선택해주세요') return false } - handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm) + handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm); } // 선택 파일 삭제 diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index f70b0f3c0..0f19e8e94 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -80,12 +80,15 @@ export const deleteData = async (url: string, data?: object): Promise => { }; // DC API 호출 -export const getDcData = async (url: string, data?: object): Promise => { +export const getDcData = async (url: string, resType: string = "json"): Promise => { const res = await fetch('/dc-service' + url, { method: "GET", }); if (res.ok) { - const data = await res.json(); + let data; + if (resType === "json") data = await res.json(); + else if (resType === "blob") data = await res.blob(); + return data; } throw new Error('Failed to fetch data'); diff --git a/apps/sn/app/components/lib/utils/fileDownload.ts b/apps/sn/app/components/lib/utils/fileDownload.ts index 31fcfac76..6f293cc36 100644 --- a/apps/sn/app/components/lib/utils/fileDownload.ts +++ b/apps/sn/app/components/lib/utils/fileDownload.ts @@ -1,28 +1,27 @@ const BASE_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || ''; -export const handleDownload = async (api:string, fileNm:string) => { - try { - const response = await fetch(api, { - method: 'GET', - }); - if (!response.ok) { - throw new Error('파일 다운로드 실패'); - } +export const handleDownload = async (api: string, fileNm: string) => { + try { + const response = await fetch(api, { + method: 'GET', + }); + if (!response.ok) throw new Error('파일 다운로드 실패'); - const blob = await response.blob(); - const url = window.URL.createObjectURL(blob); + const blob = await response.blob(); + fileDownload(blob, fileNm); + } catch (error) { + console.error('다운로드 중 오류 발생:', error); + } +}; - // 다운로드를 트리거할 가짜 태그 생성 - const link = document.createElement('a'); - link.href = url; - link.download = fileNm.normalize('NFC'); // 원하는 파일명 지정 - document.body.appendChild(link); - link.click(); - - // 메모리 해제 - link.remove(); - window.URL.revokeObjectURL(url); - } catch (error) { - console.error('다운로드 중 오류 발생:', error); - } -}; \ No newline at end of file +export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터 임시link로 연결 + const url = window.URL.createObjectURL(data); + const link = document.createElement('a'); + link.href = url; + link.download = fileName; // 원하는 파일명 지정 + document.body.appendChild(link); + link.click(); + // 메모리 해제 + link.remove(); + window.URL.revokeObjectURL(url); +} \ No newline at end of file diff --git a/apps/sn/app/store/useReportTabStore.ts b/apps/sn/app/store/useReportTabStore.ts new file mode 100644 index 000000000..05f43ebc5 --- /dev/null +++ b/apps/sn/app/store/useReportTabStore.ts @@ -0,0 +1,43 @@ +import useReportTab, { type Folder, type ReportDataFile } from '../(map-layout)/map/searchModal/useReportTab'; +import { create } from 'zustand'; +import { getDcData } from '@/components/lib/api'; + +interface useReportTabStoreType { + isOpen: boolean; + files: ReportDataFile[]; + dcTree: Folder[]; + dcFiles: ReportDataFile[]; + setIsOpen: (open: boolean) => void; + setFiles: (files: ReportDataFile[]) => void; + fetchDcTree: (clsfSystmNm: string) => void; + fetchDcFiles: (clsfSystmNm: string) => void; +} +export const useReportTabStore = create((set, get) => ({ + isOpen: false, + files: [], + dcTree: [], + dcFiles: [], + setIsOpen: (open: boolean) => set({ isOpen: open }), + setFiles: (files: ReportDataFile[]) => set({ files }), + fetchDcTree: async (clsfSystmNm: string) => { + const { dcTree } = get(); + try { + const result = await getDcData("/api/v1/dc/clsf?clsfSystmNm=" + clsfSystmNm); + if (result && JSON.stringify(result) !== JSON.stringify(dcTree)) + set({ dcTree: result }); + } catch (error) { + console.error('Failed to fetch DC tree:', error); + } + }, + fetchDcFiles: async (clsfSystmNm: string) => { + const { getDcFiles } = useReportTab(); + const { dcFiles } = get(); + try { + const result = await getDcFiles(clsfSystmNm); + if (result && JSON.stringify(result) !== JSON.stringify(dcFiles)) + set({ dcFiles: result }); + } catch (error) { + console.error('Failed to fetch DC files:', error); + } + } +})); \ No newline at end of file diff --git a/apps/sn/app/store/useSearchModalStore.ts b/apps/sn/app/store/useSearchModalStore.ts index 8ad1c356c..acb93969d 100644 --- a/apps/sn/app/store/useSearchModalStore.ts +++ b/apps/sn/app/store/useSearchModalStore.ts @@ -23,6 +23,8 @@ const todayMonth = (today.getMonth() + 1).toString().padStart(2, "0"); const todayDate = today.getDate().toString().padStart(2, "0"); export interface SearchModalState { + // 모달 열림 상태 + isSearchModalOpen: boolean; // 탭 관련 activeTab: string; basicType: string; @@ -67,9 +69,9 @@ export interface SearchModalState { todayYear: string; todayMonth: string; todayDate: string; - getDateString: (year: string, month: string, day: string, defaultDate: string) => string; // setters + setIsSearchModalOpen: (open: boolean) => void; setActiveTab: (tab: string) => void; setBasicType: (type: string) => void; setWorkType: (type: string) => void; @@ -98,11 +100,13 @@ export interface SearchModalState { setVar1: (val: string) => void; setVar2: (val: string) => void; setYearFilter: (filter: { start: string; end: string }) => void; - // 초기화 액션 - resetFilters: () => void; + + getDateString: (year: string, month: string, day: string, defaultDate: string) => string; + resetFilters: () => void; // 초기화 액션 } const useSearchModalStore = create((set) => ({ + isSearchModalOpen: false, activeTab: "basic", basicType: "tb_ms_swage_ppl", workType: "['200', '201', '204']", @@ -135,7 +139,7 @@ const useSearchModalStore = create((set) => ({ todayYear, todayMonth, todayDate, - getDateString, + setIsSearchModalOpen: (isSearchModalOpen) => set({ isSearchModalOpen }), setActiveTab: (tab) => set({ activeTab: tab }), setBasicType: (type) => set({ basicType: type }), setWorkType: (type) => set({ workType: type }), @@ -164,7 +168,8 @@ const useSearchModalStore = create((set) => ({ setVar1: (val) => set({ var1: val }), setVar2: (val) => set({ var2: val }), setYearFilter: (filter) => set({ yearFilter: filter }), - + + getDateString: getDateString, // 초기화 액션 resetFilters: () => set({ diff --git a/apps/sn/tsconfig.json b/apps/sn/tsconfig.json index 078e6ae2b..1634b3f08 100644 --- a/apps/sn/tsconfig.json +++ b/apps/sn/tsconfig.json @@ -31,8 +31,10 @@ "@common/business_components/*": ["../../../packages/common/business_components/src/*"], "@common/network": ["../../../packages/common/network/"], "@common/config": ["../../../packages/common/config/dist/"], - "@utils": ["../app/components/lib/utils"], - "@/components/*": ["../app/components/*"] + "@/app/*": ["../app/*"], + "@/components/*": ["../app/components/*"], + "@/utils": ["../app/components/lib/utils"], + "@/store/*": ["../app/store/*"], } }, "include": [ From 0b8559891abb783587594521af84c5a080719cfd Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 9 Sep 2025 09:54:40 +0900 Subject: [PATCH 4/4] =?UTF-8?q?remove:=20Gis.js=EC=97=90=EC=84=9C=20Search?= =?UTF-8?q?View=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20props=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/business_components/src/ui/map/Gis.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 1b6bbc1f3..8ce5face8 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -331,11 +331,7 @@ function Gis({ )} {/* SearchView가 존재할 때만 렌더링 */} {SearchView && ( - + )} {/* 시도/시군구 조회 영역 */} {SearchBar && (