From 71fa8eb13f7097a726165cbcfd4847e73b1d12b8 Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 4 Aug 2025 10:14:26 +0900 Subject: [PATCH 01/20] =?UTF-8?q?refactor:=20=EC=9E=90=EC=B2=B4=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EC=A0=95=EB=B3=B4=20=EB=B3=80=EC=88=98=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../popupComponent/featureClickEventPopup.tsx | 11 +++++------ .../map/components/popupComponent/sewagePump.tsx | 8 ++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx index f50595c10..5e34dbad0 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx @@ -187,7 +187,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 setLayerNm(selectedLayerName); // 상태 업데이트 - // console.log('gis data', featureData.values_); let data; if (featureData['ol_uid']) data = featureData.values_; // Gis 레이어 클릭 이벤트일 때 else data = featureData; // SearchModal 클릭 이벤트일 때 @@ -271,11 +270,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, // 자체입력정보 API 엔드포인트 선택 let apiEndpoint = ''; - const getFeatureId = featureId.toString().split('.').pop() || null; - if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${getFeatureId}`; - else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${getFeatureId}`; - else if (selectedLayerName === '오수펌프' || selectedLayerName === '빗물펌프') apiEndpoint = `v1/fcltpump/${getFeatureId}`; - else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${getFeatureId}`; + const FcltMngNo = featureData?.values_?.fclt_mng_no || null; + if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${FcltMngNo}`; + else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${FcltMngNo}`; + else if (selectedLayerName === '하수펌프장') apiEndpoint = `v1/fcltpump/${FcltMngNo}`; + else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${FcltMngNo}`; // API에서 상세 데이터 가져오기 if (apiEndpoint) { diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx index 2cde31b64..54f10c47d 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx @@ -1,15 +1,15 @@ import { VerticalTable } from '@/components/ui/verticalTable'; import React, { useState, useEffect } from 'react' -// 빗물펌프 자체 입력 정보 +// 빗물펌프장 자체 입력 정보 const exclnInputColumn = [ { id: "swtrmtZoneCd", title: "배수구역", type: "input" }, { id: "mltvDtdvNm", title: "배수분구", type: "input" }, { id: "dsgnFrqnc", title: "설계빈도(년)", type: "input" }, ] -// 오수펌프 자체 입력 정보 +// 오수펌프장 자체 입력 정보 const drtwInputColumn = [ - { id: "swtrmt_zone_cd", title: "처리구역", type: "input" }, - { id: "mltv_dtdv_nm", title: "처리분구", type: "input" }, + { id: "swtrmtZoneCd", title: "처리구역", type: "input" }, + { id: "mltvDtdvNm", title: "처리분구", type: "input" }, ] // 하수펌프장 From 897d969799e33b5a4285503708b50e280bab34ae Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 4 Aug 2025 14:48:07 +0900 Subject: [PATCH 02/20] =?UTF-8?q?fix:=20auth=20=EA=B0=92=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20=EB=B6=80=EB=B6=84=20=EC=9E=AC=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx | 4 +++- apps/sn/app/(with-base-layout)/ii/result/page.tsx | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx index 66b8095db..43797b50a 100644 --- a/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/rdiiResult/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { deleteData, getData } from '@/components/lib/api' +import { deleteData, getData, useCookieAuth } from '@/components/lib/api' import { UiTable, downloadExcelWithKoreanHeaders } from '@common/business_components/ui' import { zodResolver } from '@hookform/resolvers/zod' import { useState, useEffect } from 'react' @@ -48,6 +48,7 @@ const tableColumn = [ ] const Result = () => { + const AUTH = useCookieAuth(); const form = useForm({ resolver: zodResolver(formSchema), @@ -123,6 +124,7 @@ const Result = () => { "ctpvCd": ctpvCd === 0 ? null : ctpvCd, "sggCd": sggCd === 0 ? null : sggCd, "swageTmplntCd": processedSwageTmplntCd, + "auth": AUTH, }; console.log("Search DTO:", inftwtAnlsSearchDto); diff --git a/apps/sn/app/(with-base-layout)/ii/result/page.tsx b/apps/sn/app/(with-base-layout)/ii/result/page.tsx index 7c7e4721a..07acff774 100644 --- a/apps/sn/app/(with-base-layout)/ii/result/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/result/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { deleteData, getData } from '@/components/lib/api' +import { deleteData, getData, useCookieAuth } from '@/components/lib/api' import { UiTable, downloadExcelWithKoreanHeaders } from '@common/business_components/ui' import { zodResolver } from '@hookform/resolvers/zod' import { useState, useEffect } from 'react' @@ -46,6 +46,8 @@ const tableColumn = [ ] const Result = () => { + const AUTH = useCookieAuth(); + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { @@ -119,6 +121,7 @@ const Result = () => { "ctpvCd": ctpvCd === 0 ? null : ctpvCd, "sggCd": sggCd === 0 ? null : sggCd, "swageTmplntCd": processedSwageTmplntCd, + "auth": AUTH, }; console.log("Search DTO:", inftwtAnlsSearchDto); From a062810bd0f1c18710179e088061289f0d92ee70 Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 4 Aug 2025 15:43:21 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EC=9E=90=EC=B2=B4=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=A0=95=EB=B3=B4=20=EC=84=A4=EA=B3=84=EB=B9=88?= =?UTF-8?q?=EB=8F=84(=EB=85=84)=20=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(map-layout)/map/components/popupComponent/sewagePump.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx index 54f10c47d..e19410d8d 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/sewagePump.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react' const exclnInputColumn = [ { id: "swtrmtZoneCd", title: "배수구역", type: "input" }, { id: "mltvDtdvNm", title: "배수분구", type: "input" }, - { id: "dsgnFrqnc", title: "설계빈도(년)", type: "input" }, + { id: "designFrqYr", title: "설계빈도(년)", type: "input" }, ] // 오수펌프장 자체 입력 정보 const drtwInputColumn = [ From f82a392ef6d457c0598c1e61351dfd1d6a23c766 Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 4 Aug 2025 18:15:11 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20=EC=9E=90=EC=B2=B4=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EC=A0=95=EB=B3=B4=EC=97=90=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EA=B8=B0=EA=B4=80=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../popupComponent/featureClickEventPopup.tsx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx index 5e34dbad0..85f614918 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/featureClickEventPopup.tsx @@ -4,7 +4,7 @@ import Modal from "@/components/ui/modal"; import { VerticalTable } from "@/components/ui/verticalTable"; import { useGisContext } from "@common/business_components/ui/map/Gis"; import { Button } from "@common/components/ui"; -import { useEffect, useRef, useState, useCallback, useMemo } from "react"; +import { useEffect, useRef, useState, useMemo } from "react"; import { getLayerContentComponent } from "../common"; import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table"; import InfilChart from './chart/infilChart'; @@ -61,9 +61,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, let featureLayer: { get: (key: string) => any }; // 피처가 속한 레이어를 찾기 위한 변수 layers.some((layer: any) => { const source = layer.getSource(); - // console.log('실행됨3 source', source); - // console.log('실행됨3 featureData', featureData); - // console.log('실행됨3 featureData.fid', featureData.fid); // if (source && featureData && featureData.fid) { // featureLayer = layer; // 피처가 속한 레이어 찾기 // } @@ -131,8 +128,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, setIsClicked(false); console.log('SearchModal data', featureData); const lyrTyNm = featureData.lyrTypeNm; - console.log('실행됨4 SearchModal 클릭 이벤트', lyrTyNm); - console.log('실행됨4 SearchModal 클릭 이벤트 featureData', featureData); if (featureData.lyrTypeNm === 'tb_ms_swage_ppl') { result = '하수관로'; setHeadlist(MsSwagePplCol2) @@ -270,11 +265,12 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, // 자체입력정보 API 엔드포인트 선택 let apiEndpoint = ''; - const FcltMngNo = featureData?.values_?.fclt_mng_no || null; - if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${FcltMngNo}`; - else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${FcltMngNo}`; - else if (selectedLayerName === '하수펌프장') apiEndpoint = `v1/fcltpump/${FcltMngNo}`; - else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${FcltMngNo}`; + const fcltMngNo = featureData?.values_?.fclt_mng_no || null; + const mngInstCd = featureData?.values_?.mng_inst_cd || null; + if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${fcltMngNo}/${mngInstCd}`; + else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${fcltMngNo}/${mngInstCd}`; + else if (selectedLayerName === '하수펌프장') apiEndpoint = `v1/fcltpump/${fcltMngNo}/${mngInstCd}`; + else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${fcltMngNo}/${mngInstCd}`; // API에서 상세 데이터 가져오기 if (apiEndpoint) { @@ -353,6 +349,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, else if (layerNm === '하수맨홀') apiEndpoint = 'v1/fcltmhl'; bodyData['fcltMngNo'] = featureData.values_.fclt_mng_no.toString() || null; + bodyData['mngInstCd'] = featureData.values_.mng_inst_cd.toString() || null; // 빈 키 제거 if (bodyData[""] !== undefined) delete bodyData[""]; From 3f039d34a98163e8d5895ef55ff170f083692227 Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 4 Aug 2025 18:38:34 +0900 Subject: [PATCH 05/20] =?UTF-8?q?remove:=20GovSidoSigunguSearchForm=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/govSidoSigunguSearchForm.tsx | 360 ------------------ 1 file changed, 360 deletions(-) delete mode 100644 apps/sn/app/components/ui/govSidoSigunguSearchForm.tsx diff --git a/apps/sn/app/components/ui/govSidoSigunguSearchForm.tsx b/apps/sn/app/components/ui/govSidoSigunguSearchForm.tsx deleted file mode 100644 index 1fe3d7bae..000000000 --- a/apps/sn/app/components/ui/govSidoSigunguSearchForm.tsx +++ /dev/null @@ -1,360 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { SelectBox } from '@common/business_components/ui/select'; -import { Label } from '@radix-ui/react-label'; - -export type comCodeState = { - code: { - isInit: boolean; - chBuCd: SelectDataType[] - rainFcltCd: SelectDataType[]; - bdstUsgCd: SelectDataType[]; - cityCd: SelectDataType[]; - sggCd: SelectDataType[]; - rvbsnCd: SelectDataType[]; - mdmbsnCd: SelectDataType[]; - lgbsnCd: SelectDataType[]; - lclgvrnmntCd: EnvPartSelectDataType[]; - orgwtKndCd: SelectDataType[]; - yn: SelectDataType[]; - buildingType: { - RST: string, - RNB: string, - HTB: string - } - }; -}; - -type SelectDataType = { - text: string; - val: string; - sido?: string; // 시도 코드 (선택 사항) -} - -type EnvPartSelectDataType = SelectDataType & { - adstDliStrCd: string, -} - -type SearchFormLeftProps = { - isShowPart?: boolean | undefined; - children: React.ReactNode; - selectSidoData?: SelectDataType[] | []; - selectSigunData?: SelectDataType[] | []; - selectRegionData?: SelectDataType[] | []; - selectUnitWaterShedData?: SelectDataType[] | []; - selectMiddleRegionData?: SelectDataType[] | []; - selectEnvPartData?: EnvPartSelectDataType[] | []; - selectEnvPartSidoData?: SelectDataType[] | []; - selectEnvPartSigunData?: SelectDataType[] | []; - - setRtnPartValue?: (value: string) => void; - setRtnSidoValue?: (value: string) => void; - setRtnSigunValue?: (value: string) => void; - setRtnRegionValue?: (value: string) => void; - setRtnUnitWaterShedValue?: (value: string) => void; - setRtnMiddleRegionValue?: (value: string) => void; - setRtnEnvPartValue?: (value: string) => void; - setRtnEnvPartSidoValue?: (value: string) => void; - setRtnEnvPartSigunValue?: (value: { val: string, text: string }) => void; // setRtnEnvPartSigunValue prop 타입을 객체로 변경 - resetTrigger?: boolean; -} - - - -function GovSidoSigunguSearchForm({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSigunValue, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false }: SearchFormLeftProps) { - //환경청별 시도 시군구 - const [envPartValue, setEnvPartValue] = useState('0'); - const [envPartSidoValue, setEnvPartSidoValue] = useState('0'); // 환경청 선택 시 시/도 값 - const [envPartSigunValue, setEnvPartSigunValue] = useState('0'); // 환경청 선택 시 시/군/구 값 - - const [filteredEnvPartSigunDataSelected, setFilteredEnvPartSigunDataSelected] = useState([]); - - // API로부터 가져온 전체 데이터 목록 (필터링 전) - const [allEnvPartOptions, setAllEnvPartOptions] = useState([]); - const [allSidoOptions, setAllSidoOptions] = useState([]); - const [allSigunOptions, setAllSigunOptions] = useState([]); - - // 조회된 조건에 맞는 환경청 시도 시군 (SelectBox에 실제 사용될 데이터) - const [filteredEnvPartData, setFilteredEnvPartData] = useState([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }]); - const [filteredEnvPartSidoData, setFilteredEnvPartSidoData] = useState([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }]); - const [filteredEnvPartSigunData, setFilteredEnvPartSigunData] = useState([{ text: "전체", val: '0', sido: '0' }]); - - // 시군구 데이터 - const getSggCd = async (): Promise => { - try { - const response = await fetch('http://con.mind-one.co.kr:11082/api/v1/addr/sgg', { method: "GET", headers: { accept: "application/json" } }); - if (!response.ok) throw new Error(`시군구 데이터 오류: ${response.status}`); - const res = await response.json(); - const sgg = res.data; - const sggCd = sgg.map((item: any) => ({ - val: item.adstDstrCd, - sido: item.sido, - text: item.gugun, - })); - sggCd.unshift({ val: '0', text: '전체' }); // "전체" 옵션 추가 - return sggCd; - } catch (error) { - console.error('시군구 데이터를 가져오는데 실패했습니다:', error); - return [{ val: '0', text: '전체', sido: '0' }]; - } - }; - - // 시도 데이터 - const getCityCd = async (): Promise => { - try { - const response = await fetch('http://con.mind-one.co.kr:11082/api/v1/addr/city', { method: "GET", headers: { accept: "application/json" } }); - if (!response.ok) throw new Error(`시도 데이터 오류: ${response.status}`); - const res = await response.json(); - const city = res.data; - const cityCd = city.map((item: any) => ({ - val: item.adstDstrCd, - text: item.sido, - })); - cityCd.unshift({ val: '0', text: '전체' }); - return cityCd; - } catch (error) { - console.error('시도 데이터를 가져오는데 실패했습니다:', error); - return [{ val: '0', text: '전체', sido: '0' }]; - } - }; - - // 환경청 데이터 - const getEnvPartCd = async (): Promise => { - try { - const response = await fetch('http://con.mind-one.co.kr:11082/api/v1/addr/govArea', { method: "GET", headers: { accept: "application/json" } }); - if (!response.ok) throw new Error(`환경청 데이터 오류: ${response.status}`); - const res = await response.json(); - const lclgvrnmnt = res.data.filter((item: { lclgvrnmntNm: string; }) => { - // item.lclgvrnmntNm이 null이나 undefined가 아닌지 확인 후 endsWith 사용 - return item.lclgvrnmntNm && item.lclgvrnmntNm.endsWith("환경청"); - }); - const lclgvrnmntCd = lclgvrnmnt.map((item: any) => ({ - val: item.lclgvrnmntCd, - text: item.lclgvrnmntNm, - adstDliStrCd: item.adstDliStrCd || '' - })); - lclgvrnmntCd.unshift({ val: '0', text: '전체', adstDliStrCd: '' }); - return lclgvrnmntCd; - } catch (error) { - console.error('환경청 데이터를 가져오는데 실패했습니다:', error); - return [{ val: '0', text: '전체', adstDliStrCd: '', sido: '0' }]; - } - }; - - useEffect(() => { - const loadInitialData = async () => { - const [envParts, sidos, siguns] = await Promise.all([ - getEnvPartCd(), - getCityCd(), - getSggCd() - ]); - - setAllEnvPartOptions(envParts.filter(item => item.val !== '0')); - setFilteredEnvPartData(envParts); - - setAllSidoOptions(sidos.filter(item => item.val !== '0')); - // 초기 filteredEnvPartSidoData는 모든 시/도를 포함 (환경청 "전체" 선택 시) - setFilteredEnvPartSidoData(sidos.map(s => ({ ...s, adstDliStrCd: '' }))); - - - setAllSigunOptions(siguns.filter(item => item.val !== '0')); - // 초기 filteredEnvPartSigunData는 모든 시/군/구를 포함 (환경청 "전체", 시/도 "전체" 선택 시) - setFilteredEnvPartSigunData(siguns); - - // 만약 isShowPart가 true이고, partValue가 'G'(환경청별)로 기본 설정되어 있다면, - // 초기 envPartValue ('0')에 대한 시도/시군구 목록을 설정합니다. - initializeCityDataByEnvPartSido(envPartValue); // envPartValue 기본값 '0' - }; - - if (isShowPart) { - loadInitialData(); - } - }, [isShowPart]); // isShowPart가 변경될 때 데이터를 다시 로드 - - // 환경청 세팅 - 시도 - const initializeCityDataByEnvPartSido = (envPartCd: string) => { - const foundEnvPart = allEnvPartOptions.find((item: EnvPartSelectDataType) => item.val === envPartCd); - console.log('foundEnvPart:', foundEnvPart); - - let relevantSidos: SelectDataType[] = []; // 기본값을 빈 배열로 설정 - let relevantSigunsForSidoDerivation: SelectDataType[] = []; // 기본값을 빈 배열로 설정 - - if (envPartCd === '0') { // "전체" 환경청 선택 시 - relevantSidos = [...allSidoOptions]; - relevantSigunsForSidoDerivation = [...allSigunOptions]; - } else if (foundEnvPart && foundEnvPart.adstDliStrCd) { // 특정 환경청 선택 & 해당 환경청에 adstDliStrCd 정보가 있는 경우 - // adstDliStrCd가 시도 코드를 포함한다고 가정 (주석 참고) - const cityCodesInEnvPart = foundEnvPart.adstDliStrCd.toString().split('|').join('').split(',').filter(c => c); - console.log('cityCodesInEnvPart:', cityCodesInEnvPart); - // 1. allSigunOptions에서 val이 cityCodesInEnvPart에 포함된 데이터의 sido만 추출 - const relevantSigunSidos = Array.from( - new Set( - allSigunOptions - .filter(sigungu => cityCodesInEnvPart.includes(sigungu.val)) - .map(sigungu => sigungu.sido) - ) - ); - console.log('relevantSigunTexts:', relevantSigunSidos); - - // 2. relevantSigunsForSidoDerivation은 해당 sido만을 가진 배열로 반환 - relevantSidos = allSidoOptions.filter(sido => relevantSigunSidos.includes(sido.text)); - console.log('relevantSidos:', relevantSidos); - - // 해당 시도들에 속하는 시군구들 (가나다순 오름차순 정렬) - relevantSigunsForSidoDerivation = allSigunOptions - .filter(sigun => { - const sigunSidoPrefix = sigun.val.substring(0, 2); - return relevantSidos.some(s => s.val === sigunSidoPrefix); - }) - .sort((a, b) => a.text.localeCompare(b.text, 'ko')); - } - // 특정 환경청이 선택되었으나, foundEnvPart가 없거나 adstDliStrCd 정보가 없는 경우, - // relevantSidos와 relevantSigunsForSidoDerivation은 초기 빈 배열 상태를 유지합니다. - - setFilteredEnvPartSidoData([ - { text: "전체", val: '0', adstDliStrCd: '', sido: '0' }, - ...relevantSidos.map((item: SelectDataType) => ({ text: item.text, val: item.val, adstDliStrCd: '', sido: '0' })) - ]); - - setFilteredEnvPartSigunDataSelected(relevantSigunsForSidoDerivation); - - // 시/도 선택이 "전체"('0')로 초기화되므로, 해당 시/군/구 목록도 업데이트 - initializeCityDataByEnvPartSigun('0'); - } - - // 환경청 세팅 - 시군구 - const initializeCityDataByEnvPartSigun = (selectedSidoInEnvPart: string) => { - let sigunsToList: SelectDataType[]; - if (selectedSidoInEnvPart === '0') { // 선택된 시도가 "전체"이면 - // filteredEnvPartSigunDataSelected는 이미 해당 환경청에 맞는 시군구들로 필터링 되어있음 - sigunsToList = [...filteredEnvPartSigunDataSelected]; - } else { - sigunsToList = filteredEnvPartSigunDataSelected.filter((item: SelectDataType) => item.val.substring(0, 2) === selectedSidoInEnvPart); - } - setFilteredEnvPartSigunData([ - { text: "전체", val: '0' }, - ...sigunsToList - ]); - } - - const onSelectValue = (val: string, type: string) => { - switch (type) { - // 환경청 세팅 - case 'envPart': - setEnvPartValue(val); - setRtnEnvPartValue?.(val); - initializeCityDataByEnvPartSido(val); - setEnvPartSidoValue('0'); // 시도 "전체"로 초기화 - setEnvPartSigunValue('0'); // 시군구 "전체"로 초기화 - // 부모에게도 초기화된 값 전달 (text 포함) - if (setRtnEnvPartSidoValue) { - const sidoDefault = filteredEnvPartSidoData.find(item => item.val === '0') || { val: '0', text: '전체' }; - // setRtnEnvPartSidoValue(sidoDefault.val); // 이전 방식: 값만 전달 - // TODO: 만약 시도 값도 객체로 전달해야 한다면 아래와 같이 수정 - // setRtnEnvPartSidoValue({ val: sidoDefault.val, text: sidoDefault.text }); - } - if (setRtnEnvPartSigunValue) { - const sigunDefault = filteredEnvPartSigunData.find(item => item.val === '0') || { val: '0', text: '전체' }; - setRtnEnvPartSigunValue({ val: sigunDefault.val, text: sigunDefault.text }); - } - break; - case 'envPartSido': // 환경청별 - 시도 선택 - setEnvPartSidoValue(val); - setRtnEnvPartSidoValue?.(val); - initializeCityDataByEnvPartSigun(val); - setEnvPartSigunValue('0'); // 시군구 "전체"로 초기화 - if (setRtnEnvPartSigunValue) { - const sigunDefault = filteredEnvPartSigunData.find(item => item.val === '0') || { val: '0', text: '전체' }; - setRtnEnvPartSigunValue({ val: sigunDefault.val, text: sigunDefault.text }); - } - break; - case 'envPartSigun': // 환경청별 - 시군구 선택 - setEnvPartSigunValue(val); - const selectedSigun = filteredEnvPartSigunData.find(item => item.val === val); - if (setRtnEnvPartSigunValue && selectedSigun) { - setRtnEnvPartSigunValue({ val: selectedSigun.val, text: selectedSigun.text }); - } else if (setRtnEnvPartSigunValue && val === '0') { // "전체"를 명시적으로 처리 (find 실패 대비) - setRtnEnvPartSigunValue({ val: '0', text: '전체' }); - } - break; - } - } - - // 리셋 함수 - const resetAllValues = () => { - - setEnvPartValue('0'); - setEnvPartSidoValue('0'); - setEnvPartSigunValue('0'); - - // 필터링된 목록들도 초기 "전체" 또는 전체 목록으로 리셋 - setFilteredEnvPartSigunData([{ text: "전체", val: '0', sido: '0' }, ...allSigunOptions]); - - setFilteredEnvPartData([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }, ...allEnvPartOptions]); - setFilteredEnvPartSidoData([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }, ...allSidoOptions.map(s => ({ ...s, adstDliStrCd: '' }))]); - setFilteredEnvPartSigunData([{ text: "전체", val: '0', sido: '0' }, ...allSigunOptions]); - setFilteredEnvPartSigunDataSelected(allSigunOptions); - - - setRtnPartValue?.('A'); - setRtnSidoValue?.('0'); - setRtnSigunValue?.('0'); - setRtnRegionValue?.('0'); - setRtnUnitWaterShedValue?.('0'); - setRtnMiddleRegionValue?.('0'); - setRtnEnvPartValue?.('0'); - setRtnEnvPartSidoValue?.('0'); - setRtnEnvPartSigunValue?.({ val: '0', text: '전체' }); - }; - - // 리셋 트리거 변경 감지 - useEffect(() => { - if (resetTrigger) { - resetAllValues(); - // 리셋 후 트리거를 false로 설정하는 로직은 부모 컴포넌트에서 처리해야 할 수 있습니다. - // 여기서는 resetTrigger props가 true일 때 리셋을 수행합니다. - } - }, [resetTrigger]); - - return ( - isShowPart ? ( -
- <> - - onSelectValue(val.toString(), 'envPart')} - value={envPartValue} - selectClass={'rounded-lg bg-white truncate'} - /> - - onSelectValue(val.toString(), 'envPartSido')} - value={envPartSidoValue} - selectClass={'rounded-lg bg-white truncate'} - /> - - onSelectValue(val.toString(), 'envPartSigun')} - value={envPartSigunValue} - selectClass={'rounded-lg bg-white truncate'} - /> - - {children} -
- ) : ( -
- {children} -
- ) - ) -} - -export { GovSidoSigunguSearchForm }; \ No newline at end of file From b2a6285475fe06a9a75e4029f490f91d69f1c71a Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 4 Aug 2025 18:41:35 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20UiModal=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20headerTitleClass=20props=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/business_components/src/ui/dialog_ui/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/common/business_components/src/ui/dialog_ui/index.tsx b/packages/common/business_components/src/ui/dialog_ui/index.tsx index df7fffd3a..0858a66b8 100644 --- a/packages/common/business_components/src/ui/dialog_ui/index.tsx +++ b/packages/common/business_components/src/ui/dialog_ui/index.tsx @@ -9,6 +9,7 @@ type ModalType = { noHeaderYn?: 'N' | 'Y'; noFooterYn?: 'N' | 'Y'; headerTitle?: string; + headerTitleClass?: string; footerContent?: React.ReactNode; useDirectRender?: boolean; } @@ -21,6 +22,7 @@ export function UiModal({ noHeaderYn = 'N', noFooterYn = 'N', headerTitle, + headerTitleClass, footerContent, useDirectRender = false }: ModalType) { @@ -31,7 +33,7 @@ export function UiModal({ {noHeaderYn === 'N' && ( - + {headerTitle} From b494fdd1a6224c7118dc452dbf500c23c457ec2e Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 4 Aug 2025 18:44:48 +0900 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20=ED=95=98=EC=88=98=EB=8F=84?= =?UTF-8?q?=EC=A0=95=EB=B9=84=EA=B8=B0=EB=B3=B8=EA=B3=84=ED=9A=8D=20?= =?UTF-8?q?=EB=B3=B4=EA=B3=A0=EC=84=9C=20=EB=AA=A8=EB=8B=AC=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/popupComponent/SwtrmtFclt.tsx | 116 ++++------ .../map/searchModal/reportTab.tsx | 218 +++++++++--------- .../map/searchModal/searchModalReportApi.ts | 161 ++++--------- 3 files changed, 204 insertions(+), 291 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx b/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx index 6d182072b..366218ec1 100644 --- a/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx +++ b/apps/sn/app/(map-layout)/map/components/popupComponent/SwtrmtFclt.tsx @@ -1,88 +1,62 @@ 'use client' -import React, { useState,useEffect } from 'react' -import Modal from '@/components/ui/modal'; +import React, { useState } from 'react' import { VerticalTable } from '@/components/ui/verticalTable'; -import { UiTable } from '@common/business_components/ui'; +import { UiModal } from '@common/business_components/ui'; import { Button } from '@common/components/ui'; -import { useDcTree, flattenFolders, getFileDownload, addFileName, type FlattenedFolder} from '../../searchModal/searchModalReportApi'; +import ReportTab from '../../searchModal/reportTab'; // 하수처리시설 const headList = [ { id: "title", title: "하수도정비 기본계획보고서" }, ]; -const modalHeadList = [ - { id: "no", title: "번호" }, - { id: "year", title: "승인연도" }, - { id: "cad", title: '평면도' }, - { id: "fileNm", title: "파일명" }, - { id: "date", title: "입력시간" }, -] const SwtrmtFclt = ({ gisObject }: { gisObject: any }) => { const [isModalOpen, setIsModalOpen] = useState(false); - const [modalTitle, setModalTitle] = useState(''); - const [dcDataOne, setDcDataOne] = useState({}); - const openModal = (title: string) => { - setIsModalOpen(true); - setModalTitle(title) - }; - const closeModal = () => { - setIsModalOpen(false); - }; - const ctpvNm = gisObject?.ctpv_nm; - //const reportOriginData = useDcTree(ctpvNm); - const reportOriginData = useDcTree("강원특별자치도"); - useEffect(() => { - const fetch = async () => { - - if (!reportOriginData || reportOriginData.length === 0) { - return; - } - const reportData = flattenFolders(reportOriginData); - let filterReportData = [...reportData]; - if (gisObject && gisObject.sgg_nm !== undefined && gisObject.sgg_nm !== "") { // 시군구검색 - //const sggNm = gisObject?.sgg_nm; - const sggNm = "강릉시"; - filterReportData = filterReportData.filter(item => item.sigungu === sggNm); - } - addFileName(filterReportData).then(data => { - filterReportData = data.filter(item => item.filePath !== undefined && item.filePath !== ""); - if (filterReportData !== undefined && filterReportData.length > 0) { - setDcDataOne(filterReportData[0]); - } - }); - console.log("filterReportData", filterReportData); - }; - fetch(); - }, [reportOriginData]); - - // 하수도정비기본계획 filedownload 핸들러 - const handleFileDownloadClick = () => { - if (dcDataOne && dcDataOne !== undefined && dcDataOne.fileSn !== undefined && dcDataOne.fileSn !== "" - && dcDataOne.filePath !== undefined && dcDataOne.filePath !== "" && dcDataOne.fileName !== undefined && dcDataOne.fileName !== "" ) { - console.log("dcDataOne", dcDataOne); - getFileDownload(String(dcDataOne.fileSn), String(dcDataOne.fileName), String(dcDataOne.filePath)); - } - }; - //cad: , + const ctpvNm = gisObject?.ctpv_nm; + const sggNm = gisObject?.sgg_nm; + return (
- handleFileDownloadClick()}>다운로드 - }} headName='디지털 자료관 연계정보' tableData={[]} headlist={headList} pageSize={20} total={0} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void { - throw new Error("Function not implemented."); - }}> - {/* -
- -
-
*/} -
+ setIsModalOpen(true)}>조회 + }} + headName='디지털 자료관 연계정보' + tableData={[]} + headlist={headList} + pageSize={20} + total={0} + cellClick={undefined} + isEditable={true} + onPageChange={function (page: number): void { + throw new Error("Function not implemented."); + }} + /> + + } + useDirectRender={true} + /> + ) } diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx index 5ca3f7059..a4a50e89c 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx @@ -4,9 +4,9 @@ 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, useCityCd, useDcTree, useSggCd, reportGubuns } from './searchModalReportApi'; -import { type SelectDataType } from '../commonStore'; +import { addFileName, flattenFolders, getFileDownload, getDcTree, useDcTree, reportGubuns } from './searchModalReportApi'; +import useStore from '../../../store/useStore'; +import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox'; // 하수도정비 기본계획 보고서 @@ -15,18 +15,19 @@ export const reportTableHeadlist = [ { id: "sido", title: "시도" }, { id: "sigungu", title: "시군구" }, { id: "approvalYear", title: "승인년도" }, - { id: "plantZoneGugun", title: "구분" }, + { id: "plantZoneGubun", title: "구분" }, { id: "reportGubun", title: "보고서구분" }, { id: "fileName", title: "파일명" }, { id: "regDate", title: "등록일자" }, ]; - - -interface BasicTabProps { +interface ReportTabProps { open?: boolean; + isSearch?: boolean; + ctpvNm?: string; + sggNm?: string; } -const ReportTab: React.FC = ({ open }) => { +const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sggNm }) => { const [isLoading, setIsLoading] = useState(false); const [isResetPage, setIsResetPage] = useState(false); @@ -36,24 +37,20 @@ const ReportTab: React.FC = ({ open }) => { const [tableCount, setTableCount] = useState(0); const [curPage, setCurPage] = useState(1); const [yearFilter, setYearFilter] = useState({ start: "", end: "" }); - const [ctpvCd, setCtpvCd] = useState('0'); // 하수정비기본계획 report - const [sigungu, setSigungu] = useState('0'); // 하수정비기본계획 report - const [filteredSiguns, setFilteredSiguns] = useState([{ val: '0', text: '전체', sido: '0' }]); - const [gubun, setGubun] = useState(""); // 하수정비기본계획 report - const [reportGubun, setReportGubun] = useState("0"); // 하수정비기본계획 report - + const [ctpvCd, setCtpvCd] = useState(0); + const [sggCd, setSggCd] = useState(0); + const [reportGubun, setReportGubun] = useState("0"); + // const [gubun, setGubun] = useState(""); + + const { ctpv, sgg } = useStore(); // 하수정비기본계획 report - const sidos = useCityCd(); - const siguns = useSggCd(); const reportOriginData = useDcTree(""); //:Folder[] - const dynamicHeadlist: { id: string, title: string }[] = reportTableHeadlist; - useEffect(() => { // filterParams 초기화 - setCtpvCd('0'); - setSigungu('0'); + setCtpvCd(0); + setSggCd(0); setReportGubun('0'); setYearFilter({ start: "", end: "" }); setShowTable(false); @@ -64,27 +61,39 @@ const ReportTab: React.FC = ({ open }) => { setIsResetSelection(true); // 선택 초기화 }, [open]); + useEffect(() => { + let newCtpvCd = ctpv.find((n) => n.name === ctpvNm)?.id || 0; + let newSggCd = sgg.find((n) => n.name === sggNm)?.id || 0; + + setCtpvCd(newCtpvCd); + setSggCd(newSggCd); + }, [ctpvNm, sggNm]); + + // ctpvCd, sggCd가 업데이트된 후에 fetchFilteredData 실행 + useEffect(() => { + if (!isSearch && ctpvCd && sggCd) { + fetchFilteredData(); + } + }, [reportOriginData, ctpvCd, sggCd, isSearch]); + // 페이지 변경 시마다 데이터 fetch useEffect(() => { // showTable이 true일 때만 fetch (최초 렌더 방지) setIsResetPage(false); - setIsResetSelection(true); // 선택 초기화 - if (showTable) fetchFilteredData(); - }, [curPage]); - - const handleCtpvCdChange = (val: string | number) => { // 하수도정비기본계획.시도 - const sidoOption = sidos.find((n) => n.val === val); - setCtpvCd(String(val)); - if (val !== '0') { - const filterSiguns = siguns.filter(item => item.sido === sidoOption?.text); - filterSiguns.unshift({ val: '0', text: '전체', sido: '0' }); - setFilteredSiguns(filterSiguns); - } - setSigungu("0"); - setGubun(""); - setYearFilter({ start: "", end: "", }); - } + setIsResetSelection(true); + if (showTable) fetchFilteredData(); + + setIsResetSelection(false); + }, [curPage]); + + const handleCtpvCdChange = (sidoId: number) => { + setCtpvCd(sidoId); + setSggCd(0); + setReportGubun('0'); + setYearFilter({ start: "", end: "", }); + // setGubun(""); + } // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 const handleSearch = () => { @@ -96,7 +105,7 @@ const ReportTab: React.FC = ({ open }) => { }; // 하수도정비기본계획 테이블 cellClick 핸들러 - const handleReportCellClick = (index: number | undefined ) => { + const handleReportCellClick = (index: number | undefined) => { console.log(index); if (typeof index === "number" && tableData[index]) { const reportData = tableData[index]; @@ -107,23 +116,23 @@ const ReportTab: React.FC = ({ open }) => { } } }; - + // 검색 API 호출 함수 const fetchFilteredData = async () => { setIsLoading(true); // 로딩 시작 setIsResetSelection(true); // 선택 초기화 - + try { - //const reportOriginData = await getDcTree('경기도'); const reportData = flattenFolders(reportOriginData); let filterReportData = reportData; - if (ctpvCd !== "0") { // 시도검색 - const sidoOption = sidos.find((n) => n.val === ctpvCd); - filterReportData = filterReportData.filter(item => item.sido === sidoOption?.text); + + if (ctpvCd !== 0) { // 시도검색 + const sidoOption = ctpv.find((n) => n.id === ctpvCd); + filterReportData = filterReportData.filter(item => item.sido === sidoOption?.name); } - if (sigungu !== "0") { // 시군구검색 - const sigunguOption = filteredSiguns.find((n) => n.val === sigungu); - filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.text); + if (sggCd !== 0) { // 시군구검색 + const sigunguOption = sgg.find((n) => n.id === sggCd); + filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.name); } if (reportGubun !== "0") { // 보고서구분검색 const reportGubunOption = reportGubuns.find((n) => n.val === reportGubun); @@ -139,14 +148,16 @@ const ReportTab: React.FC = ({ open }) => { return (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" && item.approvalYear <= yearFilter.end + "년" : null; }); } - if (gubun !== "") { // 구분검색 - filterReportData = filterReportData.filter(item => (item.plantZoneGugun) ? String(item.plantZoneGugun).includes(String(gubun)) : null); - } + // if (gubun !== "") { // 구분검색 (추후 사용할 수도 있을 것 같아 남겨둠) + // filterReportData = filterReportData.filter(item => (item.plantZoneGubun) ? String(item.plantZoneGubun).includes(String(gubun)) : null); + // } console.log('report count:', filterReportData.length); + setTableCount(filterReportData.length || 0); + filterReportData = filterReportData.slice((curPage - 1) * 10, curPage * 10); // paging filterReportData = filterReportData?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 })); - //const fileList = await getDcFile("CLS20250530000041"); + addFileName(filterReportData).then(data => { filterReportData = data; setTableData(filterReportData || []); @@ -159,69 +170,70 @@ const ReportTab: React.FC = ({ open }) => { setTableData([]); } finally { setIsLoading(false); // 로딩 종료 + setIsResetSelection(false); } }; return ( <> -
-
- - - - setSigungu(String(val))} - selectContentClass="z-[101]" - /> -
-
- - setReportGubun(String(val))} - selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2' - /> - {/* 승인년도 필터 */} - setYearFilter({ ...yearFilter, start: range.start, end: range.end })} - startLabel="승인년도" endLabel="" - /> - {/* 조회 버튼 */} -
- +
+ {isSearch && ( + <> +
+ { handleCtpvCdChange(sidoId) }} + onSigunguChange={setSggCd} + setIsLoading={setIsLoading} + /> +
+ + )} +
+ + setReportGubun(String(val))} + selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2' + /> + {/* 승인년도 필터 */} + setYearFilter({ ...yearFilter, start: range.start, end: range.end })} + startLabel="승인년도" endLabel="" + /> + {/* 조회 버튼 */} +
+ +
-
-
- {/* 테이블2 영역 (가로/세로 스크롤 가능) */} -
- - { - return (<>조회 결과 ({tableCount.toLocaleString('ko-KR')})건); - })() - } - tableData={tableData} - headlist={dynamicHeadlist} - pageSize={10} - total={tableCount} - cellClick={handleReportCellClick} - onPageChange={setCurPage} - resetPageNo={isResetPage} - resetSelection={isResetSelection} - /> +
+ {/* 테이블2 영역 (가로/세로 스크롤 가능) */} +
+ + { + return (<>조회 결과 ({tableCount.toLocaleString('ko-KR')})건); + })() + } + tableData={tableData} + headlist={reportTableHeadlist} + pageSize={10} + total={tableCount} + cellClick={handleReportCellClick} + onPageChange={setCurPage} + resetPageNo={isResetPage} + resetSelection={isResetSelection} + /> +
-
); }; diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModalReportApi.ts b/apps/sn/app/(map-layout)/map/searchModal/searchModalReportApi.ts index 9610b4264..4a5df24a1 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModalReportApi.ts +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModalReportApi.ts @@ -1,39 +1,35 @@ -import {useState, useEffect} from 'react'; +import { useState, useEffect } from 'react'; import { getDcData } from '@/components/lib/api'; -const ADDR_URL = process.env.NEXT_PUBLIC_ADDR_URL; export type Folder = { - clsfSystmSn: string; - upClsfSystmSn: string; - clsfSystmNm: string; - children: Folder[]; - }; - + clsfSystmSn: string; + upClsfSystmSn: string; + clsfSystmNm: string; + children: Folder[]; +}; export type FlattenedFolder = { - [key: string]: string|number|undefined; -}; - -type SelectDataType = { - text: string; - val: string; - sido?: string; // 시도 코드 (선택 사항) + [key: string]: string | number | undefined; +}; +export type SelectDataType = { + text: string; + val: string; + sido?: string; // 시도 코드 (선택 사항) } - type ReportDataFile = { - no : number; - fileTitle : string; - regDt : string; - fileSn : string; - filePath : string; + no: number; + fileTitle: string; + regDt: string; + fileSn: string; + filePath: string; } const isYear = (value: string): boolean => { return /^[0-9]{4}년$/.test(value); }; +export const reportGubuns = [{ val: '0', text: '전체' }, { val: '1', text: '보고서' } + , { val: '2', text: '요약보고서' }, { val: '3', text: '부록' }, { val: '4', text: '기타' }]; -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. @@ -41,18 +37,20 @@ export const reportGubuns = [{val: '0', text: '전체'}, {val: '1', text: '보 */ 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:"plantZoneGugun", 5:"reportGubun", 7:"fileName", 8:"regDate" }; + 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 }, + path: { [key: string]: string | number }, level: number ) { if (level === 2 && isYear(node.clsfSystmNm)) { - level++; + level++; } if (level === 4 && addNodeNm.includes(node.clsfSystmNm)) { level++; // 레벨을 증가시켜 하위값에 추가되도록 함. @@ -79,7 +77,7 @@ export function flattenFolders(data: Folder[]): FlattenedFolder[] { } } - if (data && data.length > 0 && data[0].clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리 + if (data && data.length > 0 && data[0]?.clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리 data.forEach((root) => traverse(root, {}, 0)); } else { data.forEach((root) => traverse(root, {}, 1)); @@ -87,11 +85,11 @@ export function flattenFolders(data: Folder[]): FlattenedFolder[] { return result; } -export const getDcTree = async (clsfSystmNm:string) => { +export const getDcTree = async (clsfSystmNm: string) => { const result = await getDcData("/api/v1/dc/clsf?clsfSystmNm=" + clsfSystmNm); return result; }; -export const useDcTree = (clsfSystmNm:string) => { +export const useDcTree = (clsfSystmNm: string) => { const [data, setData] = useState([]); useEffect(() => { @@ -109,31 +107,31 @@ export const useDcTree = (clsfSystmNm:string) => { return data; }; -export const getDcFile = async (clsfSystmSn:string) => { - const queryString = 'clsfSystmSn='+clsfSystmSn; +export const getDcFile = async (clsfSystmSn: string) => { + const queryString = 'clsfSystmSn=' + clsfSystmSn; const result = await getDcData(`/api/v1/dc/file${queryString ? `?${queryString}` : ''}`); return result; }; -export const addFileName = async (data:FlattenedFolder[]) => { +export const addFileName = async (data: FlattenedFolder[]) => { const fileReportData = await Promise.all( data.map(async (item) => { - const filenameArr:ReportDataFile[] = await getDcFile(String(item.clsfSystmSn)); + const filenameArr: ReportDataFile[] = await getDcFile(String(item.clsfSystmSn)); const f = filenameArr[0]; - const result = {...item, fileName:f?.fileTitle, regDate:f?.regDt, fileSn:f?.fileSn, filePath:f?.filePath}; - + const result = { ...item, fileName: f?.fileTitle, regDate: f?.regDt, fileSn: f?.fileSn, filePath: f?.filePath }; + return result; }) ); return fileReportData; }; -export const getDcFileprcs = async (fileSn:string) => { - const queryString = 'fileSn='+fileSn; +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); +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', }); @@ -143,7 +141,7 @@ export const getDcFileDownload = async (filePath:string) => { const blob = await response.blob(); // PDF, image 등은 blob으로 받아야 함 return blob; }; -export const fileDownload = (data:Blob, fileName:string) => { // Blob데이터 임시link로 연결 +export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터 임시link로 연결 const url = window.URL.createObjectURL(data); const link = document.createElement('a'); link.href = url; @@ -153,80 +151,9 @@ export const fileDownload = (data:Blob, fileName:string) => { // Blob데이터 link.remove(); window.URL.revokeObjectURL(url); } -export const getFileDownload = async (fileSn:string, fileName:string, filePath:string) => { - if (filePath === "") alert("filePath 정보 없으므로 다운로드 불가:filesn"+ fileSn); +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); } -// 시도 데이터 -export const getCityCd = async (): Promise => { - try { - const response = await fetch(`${ADDR_URL}/api/v1/addr/city`, { method: "GET", headers: { accept: "application/json" } }); - if (!response.ok) throw new Error(`시도 데이터 오류: ${response.status}`); - const res = await response.json(); - const city = res.data; - let citys:SelectDataType[] = city.map((item: {adstDstrCd:string, sido:string}) => ({ - val: item.adstDstrCd, - text: item.sido, - })); - citys = citys.sort((a:SelectDataType, b:SelectDataType) => a.text.localeCompare(b.text)); - citys.unshift({ val: '0', text: '전체' }); - return citys; - } catch (error) { - console.error('시도 데이터를 가져오는데 실패했습니다:', error); - return [{ val: '0', text: '전체', sido: '0' }]; - } - }; - -export const useSggCd = () => { - const [data, setData] = useState([]); - - useEffect(() => { - const fetch = async () => { - const result = await getSggCd(); - if (result) { - setData(result); - } - console.log("useSggCd호출"); - }; - fetch(); - }, []); - return data; -}; - -// 시군구 데이터 -export const getSggCd = async (): Promise => { - try { - const response = await fetch(`${ADDR_URL}/api/v1/addr/sgg`, { method: "GET", headers: { accept: "application/json" } }); - if (!response.ok) throw new Error(`시군구 데이터 오류: ${response.status}`); - const res = await response.json(); - const sgg = res.data; - const sggCd = sgg.map((item: {adstDstrCd:string, sido:string, gugun:string}) => ({ - val: item.adstDstrCd, - sido: item.sido, - text: item.gugun, - })); - sggCd.unshift({ val: '0', text: '전체' }); // "전체" 옵션 추가 - return sggCd; - } catch (error) { - console.error('시군구 데이터를 가져오는데 실패했습니다:', error); - return [{ val: '0', text: '전체', sido: '0' }]; - } -}; - -export const useCityCd = () => { - const [data, setData] = useState([]); - - useEffect(() => { - const fetch = async () => { - const result = await getCityCd(); - if (result) { - setData(result); - } - console.log("useCityCd호출"); - }; - fetch(); - }, []); - return data; -}; From d979c01c441610e07901ddd7107ab7dd9d03292b Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 4 Aug 2025 19:50:04 +0900 Subject: [PATCH 08/20] =?UTF-8?q?fix:=20=ED=95=98=EC=88=98=EB=8F=84?= =?UTF-8?q?=EC=A0=95=EB=B9=84=EA=B8=B0=EB=B3=B8=EA=B3=84=ED=9A=8D=EB=B3=B4?= =?UTF-8?q?=EA=B3=A0=EC=84=9C=20=EC=8B=9C=EB=8F=84,=20=EC=8B=9C=EA=B5=B0?= =?UTF-8?q?=EA=B5=AC=20=EC=A1=B0=ED=9A=8C=20=EC=A1=B0=EA=B1=B4=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sn/app/(map-layout)/map/searchModal/reportTab.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx index a4a50e89c..f5b4c8f8e 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx @@ -90,9 +90,6 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg const handleCtpvCdChange = (sidoId: number) => { setCtpvCd(sidoId); setSggCd(0); - setReportGubun('0'); - setYearFilter({ start: "", end: "", }); - // setGubun(""); } // 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화 @@ -126,12 +123,12 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg const reportData = flattenFolders(reportOriginData); let filterReportData = reportData; - if (ctpvCd !== 0) { // 시도검색 - const sidoOption = ctpv.find((n) => n.id === ctpvCd); + if (ctpvCd) { // 시도검색 + const sidoOption = ctpv.find((n) => Number(n.id) === ctpvCd); filterReportData = filterReportData.filter(item => item.sido === sidoOption?.name); } - if (sggCd !== 0) { // 시군구검색 - const sigunguOption = sgg.find((n) => n.id === sggCd); + if (sggCd) { // 시군구검색 + const sigunguOption = sgg.find((n) => Number(n.id) === sggCd); filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.name); } if (reportGubun !== "0") { // 보고서구분검색 From 2a064ed68e451a9f6d0a55d4ef053964c4772a2d Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 5 Aug 2025 10:01:52 +0900 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20=EC=8B=9C=EB=8F=84,=20=EC=8B=9C?= =?UTF-8?q?=EA=B5=B0=EA=B5=AC=20=EC=A1=B0=EA=B1=B4=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sn/app/(map-layout)/map/searchModal/reportTab.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx index f5b4c8f8e..a0627d4cf 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx @@ -122,14 +122,21 @@ const ReportTab: React.FC = ({ open, isSearch = true, ctpvNm, sg try { const reportData = flattenFolders(reportOriginData); let filterReportData = reportData; + console.log("필터링 전 보고서 데이터:", filterReportData); + console.log('ctpv, sgg, ctpvCd, sggCd', ctpv, sgg, ctpvCd, sggCd); if (ctpvCd) { // 시도검색 - const sidoOption = ctpv.find((n) => Number(n.id) === ctpvCd); + console.log('시도 실행되나'); + const sidoOption = ctpv.find((n) => String(n.id) === String(ctpvCd)); + console.log('sidoOption', sidoOption) filterReportData = filterReportData.filter(item => item.sido === sidoOption?.name); + console.log('시도 필터링 후:', filterReportData); } if (sggCd) { // 시군구검색 - const sigunguOption = sgg.find((n) => Number(n.id) === sggCd); + console.log('시군구 실행되나'); + const sigunguOption = sgg.find((n) => String(n.id) === String(sggCd)); filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.name); + console.log('시군구 필터링 후:', filterReportData); } if (reportGubun !== "0") { // 보고서구분검색 const reportGubunOption = reportGubuns.find((n) => n.val === reportGubun); From ab63cbe992513b1a216c38ba9c8c2a464ed5ab89 Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 5 Aug 2025 10:55:47 +0900 Subject: [PATCH 10/20] =?UTF-8?q?feat:=20Gis.js=EC=97=90=20=EC=B9=A8?= =?UTF-8?q?=EC=88=98=EC=8B=AC=20=EB=B2=94=EB=A1=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/Gis.js | 802 +++++++----------- 1 file changed, 324 insertions(+), 478 deletions(-) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 229e3f629..48868313b 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -1,518 +1,364 @@ -import { Search } from 'lucide-react'; -import OlMap from 'ol/Map'; -import Overlay from 'ol/Overlay'; -import View from 'ol/View'; -import { defaults as defaultControls, OverviewMap } from 'ol/control'; -import { defaults } from 'ol/interaction'; -import * as olProj from 'ol/proj'; -import { register } from 'ol/proj/proj4'; -import proj4 from 'proj4'; -import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; -import { createRoot } from 'react-dom/client'; -import GisLonLat from './GisLonLat'; -import GisMapToggle from './GisMapToggle'; -import GisSelect from './GisSelect'; -import GisToolBar from './GisToolBar'; -import GisTree from './GisTree'; -import './lib/css/gis.common.css'; -import GisApp from './lib/gis.core'; -// import FacilityInfoPopup from './FacilityInfoPopup'; +import { Search } from "lucide-react"; +import OlMap from "ol/Map"; +import View from "ol/View"; +import { defaults as defaultControls, OverviewMap } from "ol/control"; +import { defaults } from "ol/interaction"; +import * as olProj from "ol/proj"; +import { register } from "ol/proj/proj4"; +import proj4 from "proj4"; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import GisMapToggle from "./GisMapToggle"; +import GisSelect from "./GisSelect"; +import GisToolBar from "./GisToolBar"; +import GisTree from "./GisTree"; +import "./lib/css/gis.common.css"; +import GisApp from "./lib/gis.core"; -export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); +export const GisContext = createContext({ + gisCore: null, + gisMap: null, + treeData: null, + checkedLayerIds: null, +}); export const useGisContext = () => useContext(GisContext); export const searchUpperPpls = async (fcltMngNo) => { - const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; + const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; + const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, { + method: "GET", + }); - const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, { - method: "GET", - }); + let data = null; + if (res.ok) { + try { + // 응답이 비어있는지 확인 + const contentLength = res.headers.get("content-length"); + if (contentLength === "0") + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - let data = null; - if (res.ok) { - try { - // 응답이 비어있는지 확인 - const contentLength = res.headers.get('content-length'); - if (contentLength === '0') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 + const text = await res.text(); + if (!text || text.trim() === "") + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - // 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인 - const text = await res.text(); - if (!text || text.trim() === '') alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + // JSON 파싱 시도 + data = JSON.parse(text); + } catch (error) { + console.error("JSON parsing error:", error); + alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); + } + } - // JSON 파싱 시도 - data = JSON.parse(text); - } catch (error) { - console.error('JSON parsing error:', error); - alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - } - } + const select = GisApp.Module.select; + const gisMap = GisApp.Module.core?.map; + let foundFeature = null; - const select = GisApp.Module.select; + data.forEach((item) => { + const pplMngNo = item.ppl_mng_no.trim(); - const gisMap = GisApp.Module.core?.map; + gisMap + .getLayers() + .getArray() + .filter((e) => e.get("lyrTyNm") == "tb_ms_swage_ppl") + .forEach((layer) => { + const source = layer.getSource?.(); + if (source?.getFeatures) { + const features = source.getFeatures(); + for (const f of features) { + if (f.values_.fclt_mng_no.trim() === pplMngNo) { + foundFeature = f; + break; + } + } + } + }); - let foundFeature = null; + if (foundFeature) select.selectMultiFeature(foundFeature, true); + }); - data.forEach(item => { - const pplMngNo = item.ppl_mng_no.trim(); + if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - gisMap.getLayers().getArray().filter(e => e.get("lyrTyNm") == 'tb_ms_swage_ppl').forEach(layer => { - const source = layer.getSource?.(); - if (source?.getFeatures) { - const features = source.getFeatures(); - for (const f of features) { - if (f.values_.fclt_mng_no.trim() === pplMngNo) { - foundFeature = f; - break; - } - } - } - }); + let geometry = foundFeature?.getGeometry(); + if (geometry) { + let extent = geometry.getExtent(); - if (foundFeature) select.selectMultiFeature(foundFeature, true); - }); - - if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`); - - let geometry = foundFeature?.getGeometry(); - if (geometry) { - let extent = geometry.getExtent(); - - gisMap.getView().fit(extent, { - duration: 300, - padding: [50, 50, 50, 50], - maxZoom: 14, - constrainResolution: true, - nearest: true, - }); - } + gisMap.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 14, + constrainResolution: true, + nearest: true, + }); + } }; - function Gis({ - isOpen, - gisMaps, - isSearchModalOpen, - treeData = [], - isSearch = false, - isMiniMap = false, - setIsOpen, - setFeatureData, - featureData, - imgVisble = false, - searchView: SearchView, - detailView: DetailView, - onFeatureSelect, - setIsSearchModalOpen, - onCheckedLayersChange + isOpen, //우측 상세 레이어 팝업 + gisMaps, + isSearchModalOpen, + treeData = [], + isSearch = false, + isMiniMap = false, + setIsOpen, + setFeatureData, + featureData, + imgVisble = false, + searchView: SearchView, + detailView: DetailView, + onFeatureSelect, + setIsSearchModalOpen, + onCheckedLayersChange, + searchBar: SearchBar, + waterDepth: WaterDepth, + height: height = "h-[calc(100vh-10rem)]", }) { - const mapRef = useRef(); - const [mapObject, setMapObject] = useState({}); - const [tabVisible, setTabVisible] = useState(true); - const [layerNm, setLayerNm] = useState('') - // const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 - // const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터 - // const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 - const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); - const mapInstanceRef = useRef(null); - const checkedLayerIds = useRef([]); - const layerNmRef = useRef('') + const mapRef = useRef(); + const [mapObject, setMapObject] = useState({}); + const [tabVisible, setTabVisible] = useState(true); + const [layerNm, setLayerNm] = useState(""); + // const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + // const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터 + // const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 + const gisCore = useMemo( + () => new GisApp.core({ targetId: mapRef.current }), + [], + ); + const mapInstanceRef = useRef(null); + const checkedLayerIds = useRef([]); + const layerNmRef = useRef(""); - // 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가 - // const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]); - // const facilityOverlaysRef = useRef([]); - // 체크된 레이어 상태 관리 - const [checkedLayers, setCheckedLayers] = useState([]); - const [checkedLayersData, setCheckedLayersData] = useState([]); + // 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가 + // const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]); + // const facilityOverlaysRef = useRef([]); + // 체크된 레이어 상태 관리 + const [checkedLayers, setCheckedLayers] = useState([]); + const [checkedLayersData, setCheckedLayersData] = useState([]); - let gisMap = null; - const closePopup = () => { - setIsOpen(false); // 팝업 닫기 - setFeatureData(null) - }; + let gisMap = null; + const closePopup = () => { + setIsOpen(false); // 팝업 닫기 + setFeatureData(null); + }; - // Feature Click Event - const handleFeatureSelect = useCallback((data) => { - gisMap = mapInstanceRef.current; + // Feature Click Event + const handleFeatureSelect = useCallback( + (data) => { + onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 + }, + [mapInstanceRef], + ); - if (!gisMap) { - console.warn('지도가 초기화되지 않았습니다.'); - return; - } + // 지도 생성 및 초기화 + useEffect(() => { + if (!mapRef.current) return; + if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 - // 항상 같은 select 객체 사용 - const select = GisApp.Module.select; - if (!select) return; + // EPSG:5179 등록 + proj4.defs([ + [ + "EPSG:5179", + "+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs", + ], + ]); + register(proj4); - // 1. 기존 선택 초기화 - select.resetSelectedFeature(); + // OpenLayers 지도 객체 생성 + const map = new OlMap({ + layers: [gisCore.baseLayers["base"], gisCore.baseLayers["satellite"]], + renderer: "webgl", + target: mapRef.current, + interactions: defaults({ doubleClickZoom: false }), + controls: defaultControls({ + zoom: false, + rotate: false, + attributionOptions: { + collapsible: false, + }, + }).extend([]), + view: new View({ + projection: GisApp.TO_PROJ, + zoom: gisCore.defaults.zoom, + center: olProj.transform( + [127.4875, 37.4916], + GisApp.FROM_PROJ, + GisApp.TO_PROJ, + ), + minZoom: gisCore.defaults.minZoom, + maxZoom: gisCore.defaults.maxZoom, + }), + }); - // 2. data가 OpenLayers Feature 인스턴스인지 확인 - let foundFeature = null; - if (data && typeof data.getGeometry === 'function' && typeof data.setStyle === 'function') { - // 이미 Feature 인스턴스라면 그대로 사용 - foundFeature = data; - } else { - // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 - let featureId = data.fid; - const layerName = layerNmRef.current - if (layerName === '하수재이용시설') { - featureId = data.searchPk; - } else if (layerName === '빗물이용시설' || layerName == '중수도시설') { - featureId = data.bdstInfoSn; - } else if (layerName === '하수처리시설') { - featureId = data.bscFctCd; - } else if (layerName === '온배수시설') { - featureId = data.bdstRuseFcltSn; - } else if(layerName == '수요처'){ - featureId = data.dmdInfoId.replaceAll('-','') - } - else { - console.warn('해당 레이어에 대한 featureId 매핑 없음', layerNm); - featureId = data.fid || data.id || data.ftrIdn; - } - // gisMap.getLayers().getArray().forEach(layer => { - // const source = layer.getSource?.(); - // if (source?.getFeatureById) { - // const feature = source.getFeatureById(String(featureId).trim()); - // if (feature) foundFeature = feature; - // } - // }); - // const [layerType, id] = String(featureId).split('.'); - gisMap.getLayers().getArray().forEach(layer => { - const source = layer.getSource?.(); - if (source?.getFeatures) { - const allFeatures = source.getFeatures(); - for (const f of allFeatures) { - const id = String(f.getId()); - // console.log('id', id, featureId) - if (id && id.split('.')[1] === String(featureId)) { - foundFeature = f; - break; - } - } - } - }); - } + gisMaps.current = map; - // 3. 피처 선택 - if (foundFeature) { - let geometry = foundFeature.getGeometry(); - if (geometry) { - let extent = geometry.getExtent(); + // 미니맵 추가 + if (isMiniMap) { + const overviewMap = new OverviewMap({ + layers: [GisApp.Module.core.baseMiniMapLayers["base"]], + view: new View({ + projection: GisApp.TO_PROJ, + minZoom: 9, + }), + collapsed: false, + collapseLabel: "\u00BB", + label: "\u00AB", + }); + map.addControl(overviewMap); + } - gisMap.getView().fit(extent, { - duration: 300, - padding: [50, 50, 50, 50], - maxZoom: 16, - constrainResolution: true, - nearest: true, - }); - } - select.selectFeature(foundFeature, true); - } else { - console.warn('OpenLayers Feature를 찾을 수 없습니다:', data); - } + // 스케일 컨트롤 및 베이스 레이어 설정 + gisCore.__proto__.map = map; + gisCore.scaleControl(); + gisCore.changeBaseLayer("base"); + setMapObject({ map: map }); + mapInstanceRef.current = map; // 생성 완료 플래그 설정 + }, []); - setFeatureData(data); - setIsOpen(true); - setIsSearchModalOpen(false); - onFeatureSelect?.(data); // 선택된 피쳐 데이터 콜백 호출 - }, [mapInstanceRef]); + // treeData 변경 시 레이어 코드 생성 + useEffect(() => { + if (!treeData || treeData.length === 0) return; - // 지도 생성 및 초기화 - useEffect(() => { - if (!mapRef.current) return; - if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 + // 기존 오버레이 및 레이어 모두 삭제 + gisCore.deleteAllOverlay(); + gisCore.deleteAllLayer(); - // EPSG:5179 등록 - proj4.defs([ - [ - 'EPSG:5179', - '+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', - ], - ]); - register(proj4); + // 트리 데이터를 레이어 코드 객체로 변환 + const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, "lyrSn"); + GisApp.layerCode = layerCode; // 전역 레이어 코드 설정 - // OpenLayers 지도 객체 생성 - const map = new OlMap({ - layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']], - renderer: 'webgl', - target: mapRef.current, - interactions: defaults({ doubleClickZoom: false }), - controls: defaultControls({ - zoom: false, - rotate: false, - attributionOptions: { - collapsible: false, - }, - }).extend([]), - view: new View({ - projection: GisApp.TO_PROJ, - zoom: gisCore.defaults.zoom, - center: olProj.transform( - [127.4875, 37.4916], - GisApp.FROM_PROJ, - GisApp.TO_PROJ - ), - minZoom: gisCore.defaults.minZoom, - maxZoom: gisCore.defaults.maxZoom, - }), - }); + //setTreeData(data); + // 편집 가능한 레이어 목록 생성 + //const layerItems = getLayerItems(layerCode); + //setEditLayerItems(prev => [...layerItems]) + }, [treeData]); - gisMaps.current = map + const getLayerItems = (layerCode) => { + const layerItems = Object.entries(layerCode) + .filter(([key, obj], idx) => obj?.updtPsblYn === "Y") + .map(([key, obj]) => ({ + ...obj, + name: obj.lyrNm, + label: obj.lyrNm, + value: obj.lyrSn + "", + })) + .sort((a, b) => { + if (a.sortOrdr > b.sortOrdr) return 1; + if (a.sortOrdr < b.sortOrdr) return -1; + return 0; + }); + return layerItems; + }; - // 미니맵 추가 - if (isMiniMap) { - const overviewMap = new OverviewMap({ - layers: [GisApp.Module.core.baseMiniMapLayers['base']], - view: new View({ - projection: GisApp.TO_PROJ, - minZoom: 9, - }), - collapsed: false, - collapseLabel: '\u00BB', - label: '\u00AB', - }); - map.addControl(overviewMap); - } + if (mapObject.map) { + try { + gisMap = mapObject.map; + } catch (e) { + console.error("오류 발생:", e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력 + } + } - // 스케일 컨트롤 및 베이스 레이어 설정 - gisCore.__proto__.map = map; - gisCore.scaleControl(); - gisCore.changeBaseLayer('base'); - setMapObject({ map: map }); - mapInstanceRef.current = map; // 생성 완료 플래그 설정 - }, []); + // 체크된 레이어 정보를 처리하는 함수 + const handleCheckedLayersChange = useCallback( + (checkedIds, checkedNodesData) => { + setTimeout(() => { + setCheckedLayers(checkedIds); + setCheckedLayersData(checkedNodesData); + onCheckedLayersChange?.(checkedIds, checkedNodesData); + }, 1000); + }, + [onCheckedLayersChange], + ); - // treeData 변경 시 레이어 코드 생성 - useEffect(() => { - if (!treeData || treeData.length === 0) return; + return ( + <> + +
+
+ { + layerNmRef.current = layerNm; + setLayerNm(layerNm); + setIsOpen(true); + const dummyFeature = { + getProperties: () => ({}), // 최소 구조 + }; + onFeatureSelect(dummyFeature); + }} + imageVisible={imgVisble} + /> - // 기존 오버레이 및 레이어 모두 삭제 - gisCore.deleteAllOverlay(); - gisCore.deleteAllLayer(); + {typeof WaterDepth === "function" && ( +
+ setIsOpen(false)} + /> +
+ )} - // 트리 데이터를 레이어 코드 객체로 변환 - const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn'); - GisApp.layerCode = layerCode; // 전역 레이어 코드 설정 + {/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} + {isOpen && !isSearchModalOpen && ( + ({}) }} + seletectedLayerNm={layerNm} + onClose={closePopup} // 닫기 콜백 전달 + onFeatureSelect={handleFeatureSelect} + /> + )} + {/* SearchView가 존재할 때만 렌더링 */} + {SearchView && ( + + )} + {/* 시도/시군구 조회 영역 */} + {SearchBar && ( +
+ +
+ )} + + + +
+
- //setTreeData(data); - // 편집 가능한 레이어 목록 생성 - const layerItems = getLayerItems(layerCode); - //setEditLayerItems(prev => [...layerItems]) - }, [treeData]); - - const getLayerItems = (layerCode) => { - const layerItems = Object.entries(layerCode) - .filter(([key, obj], idx) => obj?.updtPsblYn === 'Y') - .map(([key, obj]) => ({ - ...obj, - name: obj.lyrNm, - label: obj.lyrNm, - value: obj.lyrSn + '', - })) - .sort((a, b) => { - if (a.sortOrdr > b.sortOrdr) return 1; - if (a.sortOrdr < b.sortOrdr) return -1; - return 0; - }); - return layerItems; - }; - - if (mapObject.map) { - try { - gisMap = mapObject.map; - } catch (e) { - console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력 - } - } - - // // 데이터 로드 및 팝업 생성 로직 - // useEffect(() => { - // if (!mapInstanceRef.current) return; // 맵이 생성되지 않았으면 실행 안 함 - - // // 1. 팝업 데이터 가져오기 (feature.js의 getData와 유사) - // const fetchPopupData = async () => { - // // 실제 API 호출 로직으로 대체해야 합니다. - // // 예시: const response = await yourApiService.getFcltyWtCnt(); - // // setFacilityInfoPopupData(response.data); - - // // 임시 목업 데이터 - // const mockData = [ - // { ftrIdn: 1, ftrCde: 'SA206', bplcNm: '처리장A', fcltyUseYn: 'Y', x: 127.005, y: 37.282, aCnt: 10, cCnt: 5, eCnt: 3, mCnt: 8, iCnt: 2 }, - // { ftrIdn: 2, ftrCde: 'SA112', bplcNm: '처리장B', fcltyUseYn: 'N', x: 127.010, y: 37.285, aCnt: 8, cCnt: 4, eCnt: 2, mCnt: 6, iCnt: 1 } - // ]; - // setFacilityInfoPopupData(mockData); - // }; - - // fetchPopupData(); - - // }, [mapInstanceRef.current]); // 맵 인스턴스가 생성된 후 한 번만 실행 - - // // 데이터가 변경되면 오버레이를 생성/업데이트하는 로직 - // useEffect(() => { - // const map = mapInstanceRef.current; - // if (!map || facilityInfoPopupData.length === 0) return; - - // // 기존 오버레이 정리 - // facilityOverlaysRef.current.forEach(overlay => map.removeOverlay(overlay)); - // facilityOverlaysRef.current = []; - - // facilityInfoPopupData.forEach(data => { - // const popupId = `${data.ftrIdn}_${data.ftrCde}`; - - // // 팝업을 담을 div 엘리먼트 생성 - // const popupElement = document.createElement('div'); - // popupElement.id = popupId; - - // // OpenLayers Overlay 생성 - // const position = olProj.transform([Number(data.x), Number(data.y)], GisApp.FROM_PROJ, GisApp.TO_PROJ); - // const overlay = new Overlay({ - // element: popupElement, - // position: position, - // offset: [-75, -210], // 팝업 너비의 절반, 높이만큼 오프셋 조정 - // positioning: 'bottom-center', - // className: 'facility-popup-overlay' // 필요시 공통 클래스 추가 - // }); - - // map.addOverlay(overlay); - // facilityOverlaysRef.current.push(overlay); - - // // React 컴포넌트를 popupElement에 렌더링 - // const root = createRoot(popupElement); - // root.render(); - // }); - - // // 초기 가시성 업데이트 - // updatePopupVisibility(); - - // }, [facilityInfoPopupData]); // 팝업 데이터가 변경될 때마다 실행 - - // // 레이어 가시성에 따라 팝업 가시성을 제어하는 함수 - // const updatePopupVisibility = useCallback(() => { - // const map = mapInstanceRef.current; - // if (!map) return; - - // const visibleLayers = new Set(); - // map.getLayers().forEach(layer => { - // if (layer.getVisible()) { - // // feature.js의 로직처럼 typeName이나 ftrCde를 사용해 필터링 - // const typeName = layer.get('typeName'); - // if (typeName === 'VW_WTL_FCLTY_AS') { // 예시 레이어 이름 - // visibleLayers.add(layer.get('ftrCde')); - // } - // } - // }); - - // facilityOverlaysRef.current.forEach(overlay => { - // const element = overlay.getElement(); - // const ftrCde = element.querySelector('table')?.classList[0]; // FacilityPopup의 클래스에서 ftrCde 가져오기 - - // if (visibleLayers.has(ftrCde)) { - // element.style.display = ''; - // } else { - // element.style.display = 'none'; - // } - // }); - // }, []); - - // GisTree에서 체크 상태가 변경될 때 이 함수를 호출하도록 연결해야 합니다. - // 예시: GisTree 컴포넌트에 onCheck 콜백을 추가하고, 그 콜백에서 이 함수를 호출 - // useEffect(() => { - // // 레이어 가시성이 변경될 때마다 팝업 가시성 업데이트 - // const map = mapInstanceRef.current; - // if (!map) return; - - // const handleLayerVisibilityChange = () => { - // updatePopupVisibility(); - // }; - - // map.getLayers().on('change:length', handleLayerVisibilityChange); - // map.getLayers().forEach(layer => { - // layer.on('change:visible', handleLayerVisibilityChange); - // }); - - // return () => { - // // 클린업 - // map.getLayers().un('change:length', handleLayerVisibilityChange); - // map.getLayers().forEach(layer => { - // layer.un('change:visible', handleLayerVisibilityChange); - // }); - // }; - // }, [updatePopupVisibility]); - - // 체크된 레이어 정보를 처리하는 함수 - const handleCheckedLayersChange = useCallback((checkedIds, checkedNodesData) => { - setTimeout(() => { - setCheckedLayers(checkedIds); - setCheckedLayersData(checkedNodesData); - onCheckedLayersChange?.(checkedIds, checkedNodesData); - }, 0); - }, [onCheckedLayersChange]); - - return ( - <> - -
-
- { - layerNmRef.current = nodeNm - setLayerNm(nodeNm); setIsOpen(true); const dummyFeature = { - getProperties: () => ({}), // 최소 구조 - }; - onFeatureSelect(dummyFeature); - }} - imageVisible={imgVisble} - /> - - {/* */} - - {/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} - {isOpen && !isSearchModalOpen && ( - ({}) }} - seletectedLayerNm={layerNm} - onClose={closePopup} // 닫기 콜백 전달 - onFeatureSelect={handleFeatureSelect} - /> - )} - {/* SearchView가 존재할 때만 렌더링 */} - {SearchView && ( - - )} - {/* */} - - - -
- - {isSearch && ( -
- -
- )} - - - ); + {isSearch && ( +
+ +
+ )} + + + ); } export default React.memo(Gis); From 115fbbfc995bd1b9d69e6f1af86ba020fcd4a7fe Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 5 Aug 2025 11:22:22 +0900 Subject: [PATCH 11/20] =?UTF-8?q?feat:=20=EC=B9=A8=EC=88=98=EC=8B=AC=20?= =?UTF-8?q?=EB=B2=94=EB=A1=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/(map-layout)/map/page.tsx | 27 +++++++++++++++++-- .../map/waterDepth/waterDepth.tsx | 18 ++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index 6105bc818..f7ceeb205 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -40,6 +40,7 @@ const Map = () => { const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부 useEffect(() => { @@ -118,6 +119,28 @@ const Map = () => { setIsSearchModalOpen(false); }; + // WaterDepth 표시 여부를 결정하는 함수 + const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + // 침수 관련 레이어 ID들 (실제 데이터에 맞게 수정 필요) + const waterDepthLayerTypes = [ + 'tb_ms_flood_dngr_map', // 침수위험지도 + 'tb_ms_sbmrg_dngr_map' // 도시침수지도 + ]; + + // 체크된 레이어 중에 침수 관련 레이어가 있는지 확인 + const hasWaterDepthLayer = checkedNodesData.some(node => + waterDepthLayerTypes.includes(node.data?.lyrTyNm) + ); + + setShowWaterDepth(hasWaterDepthLayer); + }, []); + + // 체크된 레이어 변경 처리 함수 + const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => { + // WaterDepth 표시 여부 결정 + handleWaterDepthVisibility(checkedIds, checkedNodesData); + }, [handleWaterDepthVisibility]); + return (
@@ -135,9 +158,9 @@ const Map = () => { setIsSearchModalOpen={setIsSearchModalOpen} isOpen={isOpen} isSearchModalOpen={isSearchModalOpen} - onCheckedLayersChange={undefined} + onCheckedLayersChange={handleCheckedLayersChange} searchBar={SearchBar} - // waterDepth={WaterDepth} + waterDepth={showWaterDepth ? WaterDepth : undefined} />
); diff --git a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx index 39e375a4b..a69587aa2 100644 --- a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx +++ b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx @@ -3,18 +3,28 @@ import React, { useRef, useEffect, useState } from "react"; import { Label } from "@common/components/ui/label"; -export function WaterDepth({ onClose }: { onClose?: () => void }) { +interface WaterDepthProps { + onClose?: () => void; +} + +export function WaterDepth({ + onClose, +}: WaterDepthProps) { const [isPlaying, setIsPlaying] = useState(false); + const handlePlayToggle = () => { + setIsPlaying((prev) => !prev); + }; + return ( <> -
+
-
+
From d019ed61828f9fa679e91e461db3940b45222f1b Mon Sep 17 00:00:00 2001 From: RSONE001 Date: Tue, 5 Aug 2025 14:12:26 +0900 Subject: [PATCH 12/20] =?UTF-8?q?=EC=97=B0=EA=B3=84=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=86=8C=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/entities/link/model/link-interface.ts | 22 +++++++++---------- .../cs/src/pages/link/model/table-columns.tsx | 4 ++-- apps/cs/src/pages/link/ui/link-form.tsx | 8 +++---- apps/cs/src/pages/link/ui/modify-page.tsx | 6 ++--- apps/cs/src/pages/link/ui/regist-page.tsx | 6 ++--- .../linkParams/ui/link-params-dialog.tsx | 2 +- .../pages/linkParams/ui/link-params-page.tsx | 8 +++---- .../pages/linkTable/ui/link-table-dialog.tsx | 4 ++-- .../pages/linkTable/ui/link-table-page.tsx | 12 +++++----- 9 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/cs/src/entities/link/model/link-interface.ts b/apps/cs/src/entities/link/model/link-interface.ts index c4493a6c0..57d1b4582 100644 --- a/apps/cs/src/entities/link/model/link-interface.ts +++ b/apps/cs/src/entities/link/model/link-interface.ts @@ -18,8 +18,8 @@ export interface Link { next_link_excn_dt: string; last_link_err_cd: string; mdfcn_dt: string; - mdfr: string; - link_batch_time: string; + mdfr_id_id: string; + link_cycle_mi: string; } export interface LinkLog { @@ -48,8 +48,8 @@ export interface LinkInfo extends LinkFormData { next_link_excn_dt: string; last_link_err_cd: string; mdfcn_dt: string; - mdfr: string; - link_batch_time: number; + mdfr_id: string; + link_cycle_mi: number; } export interface LinkStats { @@ -65,7 +65,7 @@ export interface LinkVrbl { link_vrbl_cd: string; link_vrbl_expln: string; mdfcn_dt: string; - mdfr: string; + mdfr_id: string; } export interface LinkTable { @@ -94,7 +94,7 @@ export interface LinkTableColumns { pk_yn: string; col_seq: number; mdfcn_dt: string; - mdfr: string; + mdfr_id: string; } export const linkFormSchema = z.object({ @@ -118,8 +118,8 @@ export const linkFormSchema = z.object({ next_link_excn_dt: z.string().max(40).nullable().optional(), last_link_err_cd: z.string().max(10).nullable().optional(), mdfcn_dt: z.string().nullable().optional(), - mdfr: z.string().nullable().optional(), - link_batch_time: z.coerce.number().nullable().optional().refine((val) => { + mdfr_id: z.string().nullable().optional(), + link_cycle_mi: z.coerce.number().nullable().optional().refine((val) => { if (val == null) return true; return val >= 5; }, { @@ -135,7 +135,7 @@ export const linkVrblFormSchema = z.object({ link_vrbl_cd: z.string().max(10, { message: '변수코드는 10자 이하로 입력해 주세요.' }).nullable().optional(), link_vrbl_vl: z.string().max(1000, { message: '변수값은 1000자 이하로 입력해 주세요.' }).nonempty({ message: '변수값은 필수입니다.' }), mdfcn_dt: z.string().nullable().optional(), - mdfr: z.string().nullable().optional(), + mdfr_id: z.string().nullable().optional(), }); export const LinkTableColumnsSchema = z.object({ @@ -168,7 +168,7 @@ export const LinkTableColumnsSchema = z.object({ message: "컬럼 매칭 순서는 0 이상이어야 합니다." }), mdfcn_dt: z.string().nullable().optional(), - mdfr: z.string().nullable().optional(), + mdfr_id: z.string().nullable().optional(), }); export const LinkTableSchema = z.object({ @@ -182,7 +182,7 @@ export const LinkTableSchema = z.object({ trsf_tbl_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환테이블 한글명은 필수입니다.' }), trsf_tbl_expln: z.string().max(2000, { message: '설명은 2000자 이하로 입력해 주세요.' }).nullable().optional(), mdfcn_dt: z.string().nullable().optional(), - mdfr: z.string().nullable().optional(), + mdfr_id: z.string().nullable().optional(), }); interface LinkStatsListObject { diff --git a/apps/cs/src/pages/link/model/table-columns.tsx b/apps/cs/src/pages/link/model/table-columns.tsx index 5b1710fec..81b86b570 100644 --- a/apps/cs/src/pages/link/model/table-columns.tsx +++ b/apps/cs/src/pages/link/model/table-columns.tsx @@ -53,8 +53,8 @@ const columns = [ header: '연계서버 IP', cell: (info) => (info.getValue() ? info.getValue() : '-'), }), - columnHelper.accessor('link_batch_time', { - id: 'link_batch_time', + columnHelper.accessor('link_cycle_mi', { + id: 'link_cycle_mi', header: '연계 주기', cell: (info) => convertMinutesToPeriod(Number(info.getValue())), }), diff --git a/apps/cs/src/pages/link/ui/link-form.tsx b/apps/cs/src/pages/link/ui/link-form.tsx index 172215662..41f125335 100644 --- a/apps/cs/src/pages/link/ui/link-form.tsx +++ b/apps/cs/src/pages/link/ui/link-form.tsx @@ -336,7 +336,7 @@ const LinkForm = ({ form, linkSn, updateLinkInfo, handleInsertLink, deleteLink, { const timeParts = convertMinutesToPeriod(Number(field.value)); return ( @@ -434,9 +434,9 @@ const LinkForm = ({ form, linkSn, updateLinkInfo, handleInsertLink, deleteLink,
- {form.formState.errors.link_batch_time && ( + {form.formState.errors.link_cycle_mi && (

- {form.formState.errors.link_batch_time.message} + {form.formState.errors.link_cycle_mi.message}

)} @@ -520,7 +520,7 @@ const LinkForm = ({ form, linkSn, updateLinkInfo, handleInsertLink, deleteLink, ( diff --git a/apps/cs/src/pages/link/ui/modify-page.tsx b/apps/cs/src/pages/link/ui/modify-page.tsx index 594cb4c49..c690b8aa7 100644 --- a/apps/cs/src/pages/link/ui/modify-page.tsx +++ b/apps/cs/src/pages/link/ui/modify-page.tsx @@ -35,7 +35,7 @@ function ModifyLinkPage({ linkSn }: { linkSn: string }) { mutationFn: (data: LinkFormData) => LinkApi.updateLinkInfo({ ...data, link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null, - mdfr: userUniqId + mdfr_id: userUniqId }), onSuccess: () => { alert('수정되었습니다.'); @@ -73,11 +73,11 @@ function ModifyLinkPage({ linkSn }: { linkSn: string }) { if (linkInfo) { let newData = { ...linkInfo }; - if (linkInfo.mdfr) { + if (linkInfo.mdfr_id) { const userName = await fetchUserInfo(); newData = { ...linkInfo, - mdfr: userName, + mdfr_id: userName, }; } diff --git a/apps/cs/src/pages/link/ui/regist-page.tsx b/apps/cs/src/pages/link/ui/regist-page.tsx index 403aefa24..76912ab9f 100644 --- a/apps/cs/src/pages/link/ui/regist-page.tsx +++ b/apps/cs/src/pages/link/ui/regist-page.tsx @@ -27,7 +27,7 @@ function RegistLinkPage() { link_id: '', link_pswd: '', srvc_usg: '', - link_batch_time: 5, + link_cycle_mi: 5, link_clsf_cd: 'none', }, }); @@ -44,8 +44,8 @@ function RegistLinkPage() { const insertData = { ...data, link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null, - next_link_excn_dt: dayjs().add(Number(data.link_batch_time), 'minute').format('YYYY-MM-DD HH:mm:ss'), - mdfr: userUniqId, + next_link_excn_dt: dayjs().add(Number(data.link_cycle_mi), 'minute').format('YYYY-MM-DD HH:mm:ss'), + mdfr_id: userUniqId, }; insertLinkInfo(insertData); diff --git a/apps/cs/src/pages/linkParams/ui/link-params-dialog.tsx b/apps/cs/src/pages/linkParams/ui/link-params-dialog.tsx index cff72a030..9a3e2b87e 100644 --- a/apps/cs/src/pages/linkParams/ui/link-params-dialog.tsx +++ b/apps/cs/src/pages/linkParams/ui/link-params-dialog.tsx @@ -292,7 +292,7 @@ function LinkParamsDialog({ selectedLink, form, insertLinkVrbl, dialogOpen, onOp ( diff --git a/apps/cs/src/pages/linkParams/ui/link-params-page.tsx b/apps/cs/src/pages/linkParams/ui/link-params-page.tsx index 4c811ce13..319df80b2 100644 --- a/apps/cs/src/pages/linkParams/ui/link-params-page.tsx +++ b/apps/cs/src/pages/linkParams/ui/link-params-page.tsx @@ -112,7 +112,7 @@ function LinkParamsPage() { await LinkApi.insertLinkVrbl({ ...data, link_sn: selectedLink.linkSn, - mdfr: userUniqId, + mdfr_id: userUniqId, }), onSuccess: async () => { // 쿼리 무효화를 통한 최신 데이터 가져오기 @@ -128,7 +128,7 @@ function LinkParamsPage() { mutationFn: async (data: LinkVrblFormData) => await LinkApi.updateLinkVrblInfo({ ...data, link_sn: selectedLink.linkSn, - mdfr: userUniqId, + mdfr_id: userUniqId, }), onSuccess: async () => { await queryClient.invalidateQueries({ @@ -229,11 +229,11 @@ function LinkParamsPage() { if (linkVrblInfo) { let newData = { ...linkVrblInfo }; - if (linkVrblInfo.mdfr) { + if (linkVrblInfo.mdfr_id) { const userName = await fetchUserInfo(); newData = { ...linkVrblInfo, - mdfr: userName, + mdfr_id: userName, }; } diff --git a/apps/cs/src/pages/linkTable/ui/link-table-dialog.tsx b/apps/cs/src/pages/linkTable/ui/link-table-dialog.tsx index a566a7d95..be835872d 100644 --- a/apps/cs/src/pages/linkTable/ui/link-table-dialog.tsx +++ b/apps/cs/src/pages/linkTable/ui/link-table-dialog.tsx @@ -101,7 +101,7 @@ function LinkTableDialog({ dialogOpen, onOpenChange, form, tableInfo, insertLink col_seq: null, pk_yn: 'N', mdfcn_dt: '', - mdfr: '', + mdfr_id: '', }); } @@ -444,7 +444,7 @@ function LinkTableDialog({ dialogOpen, onOpenChange, form, tableInfo, insertLink ( diff --git a/apps/cs/src/pages/linkTable/ui/link-table-page.tsx b/apps/cs/src/pages/linkTable/ui/link-table-page.tsx index cbdf4627a..0394e7bf6 100644 --- a/apps/cs/src/pages/linkTable/ui/link-table-page.tsx +++ b/apps/cs/src/pages/linkTable/ui/link-table-page.tsx @@ -78,7 +78,7 @@ function LinkTablePage() { const { mutateAsync: insertLinkTableCol } = useMutation({ mutationFn: async (data: LinkTableColData) => await LinkApi.insertLinkTableCol({ ...data, - mdfr: userUniqId, + mdfr_id: userUniqId, }), onSuccess: async () => { if (selectedTableInfo) { @@ -104,7 +104,7 @@ function LinkTablePage() { const { mutateAsync: updateLinkTableCol } = useMutation({ mutationFn: async (data: LinkTableColData) => await LinkApi.updateLinkTableCol({ ...data, - mdfr: userUniqId, + mdfr_id: userUniqId, }), onSuccess: async () => { if (selectedTableInfo) { @@ -280,11 +280,11 @@ function LinkTablePage() { if (colInfo) { let newData = { ...colInfo }; - if (colInfo.mdfr) { + if (colInfo.mdfr_id) { const userName = await fetchUserInfo(); newData = { ...colInfo, - mdfr: userName, + mdfr_id: userName, }; } @@ -319,7 +319,7 @@ function LinkTablePage() { if (await confirm(confirmMsg)) { insertLinkTableInfo({ ...data, - mdfr: userUniqId, + mdfr_id: userUniqId, tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "", }); } @@ -330,7 +330,7 @@ function LinkTablePage() { updateLinkTableInfo({ ...data, tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "", - mdfr: userUniqId, + mdfr_id: userUniqId, }); } }); From 71a7335de4bcbe9f867e9284e0c65a989c51295b Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 5 Aug 2025 16:18:40 +0900 Subject: [PATCH 13/20] =?UTF-8?q?feat:=20=EC=B9=A8=EC=88=98=EC=8B=AC=20?= =?UTF-8?q?=EB=B2=94=EB=A1=80=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/(map-layout)/map/page.tsx | 21 +++++---- .../map/waterDepth/waterDepth.tsx | 44 ++++++++++++------- 2 files changed, 37 insertions(+), 28 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index f7ceeb205..fe4ff9665 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -7,7 +7,7 @@ import GisApp from "@common/business_components/ui/map/lib/gis.core"; import SearchModal from "./searchModal/searchModal"; import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup"; import { SearchBar } from "./searchBar/searchBar"; -import { WaterDepth } from "./waterDepth/waterDepth"; +import { WaterDepth, WATER_DEPTH_LAYER_TYPES, ORDERED_LAYERS } from "./waterDepth/waterDepth"; // 타입 정의 추가 export type LyrStngProps = { @@ -41,6 +41,7 @@ const Map = () => { const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부 + const [isPlaying, setIsPlaying] = useState(false); useEffect(() => { @@ -121,23 +122,16 @@ const Map = () => { // WaterDepth 표시 여부를 결정하는 함수 const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => { - // 침수 관련 레이어 ID들 (실제 데이터에 맞게 수정 필요) - const waterDepthLayerTypes = [ - 'tb_ms_flood_dngr_map', // 침수위험지도 - 'tb_ms_sbmrg_dngr_map' // 도시침수지도 - ]; - - // 체크된 레이어 중에 침수 관련 레이어가 있는지 확인 - const hasWaterDepthLayer = checkedNodesData.some(node => - waterDepthLayerTypes.includes(node.data?.lyrTyNm) + const waterDepthLayers = checkedNodesData.filter(node => + WATER_DEPTH_LAYER_TYPES.includes(node.data?.lyrTyNm) ); + const hasWaterDepthLayer = waterDepthLayers.length > 0; setShowWaterDepth(hasWaterDepthLayer); }, []); // 체크된 레이어 변경 처리 함수 const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => { - // WaterDepth 표시 여부 결정 handleWaterDepthVisibility(checkedIds, checkedNodesData); }, [handleWaterDepthVisibility]); @@ -161,6 +155,11 @@ const Map = () => { onCheckedLayersChange={handleCheckedLayersChange} searchBar={SearchBar} waterDepth={showWaterDepth ? WaterDepth : undefined} + isPlaying={isPlaying} + setIsPlaying={setIsPlaying} + targetLayerTypes={WATER_DEPTH_LAYER_TYPES} + orderedLayers={ORDERED_LAYERS} + checkInterval={1000} />
); diff --git a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx index a69587aa2..652ac81fd 100644 --- a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx +++ b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx @@ -3,17 +3,39 @@ import React, { useRef, useEffect, useState } from "react"; import { Label } from "@common/components/ui/label"; +export const WATER_DEPTH_LAYER_TYPES = [ + 'tb_ms_flood_dngr_map', + 'tb_ms_sbmrg_dngr_map' +]; +// 지정된 순서에 따른 lyr_sn 배열 +export const ORDERED_LAYERS = [ + '27', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 30년 빈도 + '28', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 50년 빈도 + '3', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 80년 빈도 + '4', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 100년 빈도 + '59', // 도시침수지도(tb_ms_flood_dngr_map) - 30년 빈도 + '60', // 도시침수지도(tb_ms_flood_dngr_map) - 50년 빈도 + '58' // 도시침수지도(tb_ms_flood_dngr_map) - 80년 빈도 +]; + interface WaterDepthProps { onClose?: () => void; + isPlaying: boolean; + setIsPlaying: (isPlaying: boolean) => void; } export function WaterDepth({ onClose, + isPlaying, + setIsPlaying, }: WaterDepthProps) { - const [isPlaying, setIsPlaying] = useState(false); + + useEffect(() => { + if (setIsPlaying) setIsPlaying(isPlaying); + }, [isPlaying, setIsPlaying]); const handlePlayToggle = () => { - setIsPlaying((prev) => !prev); + setIsPlaying(!isPlaying); }; return ( @@ -21,23 +43,11 @@ export function WaterDepth({
-
From a83ea455fb2ede235c3dc655022ca62ec1beb455 Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 5 Aug 2025 16:35:03 +0900 Subject: [PATCH 14/20] =?UTF-8?q?feat:=20GisTree=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20Check=20=ED=95=A8=EC=88=98=20=EA=B5=AC=ED=98=84=20(?= =?UTF-8?q?=EC=B9=A8=EC=88=98=EC=8B=AC=20=EB=B2=94=EB=A1=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/Gis.js | 12 ++ .../business_components/src/ui/map/GisTree.js | 184 +++++++++++++++++- 2 files changed, 190 insertions(+), 6 deletions(-) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 48868313b..046619de1 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -117,6 +117,11 @@ function Gis({ onCheckedLayersChange, searchBar: SearchBar, waterDepth: WaterDepth, + isPlaying = false, + setIsPlaying = () => {}, + targetLayerTypes = [], + orderedLayers = [], + checkInterval = 3000, height: height = "h-[calc(100vh-10rem)]", }) { const mapRef = useRef(); @@ -305,12 +310,19 @@ function Gis({ onFeatureSelect(dummyFeature); }} imageVisible={imgVisble} + isPlaying={isPlaying} + setIsPlaying={setIsPlaying} + targetLayerTypes={targetLayerTypes} + orderedLayers={orderedLayers} + checkInterval={checkInterval} /> {typeof WaterDepth === "function" && (
setIsOpen(false)} + isPlaying={isPlaying} + setIsPlaying={setIsPlaying} />
)} diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index 8a2db3ae2..1880eb225 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -4,19 +4,36 @@ import { getBackendOptions } from "@minoru/react-dnd-treeview"; import { ChevronLeft, ChevronRight } from 'lucide-react'; -import React, { useEffect, useState } from 'react'; -import { DndProvider } from "react-dnd"; +import React, { useEffect, useState, useRef, useCallback } from 'react'; import { useGisContext } from './Gis'; import GisApp from './lib/gis.core'; -export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange, onLayerIconClick,imageVisible=false }) { +export default React.memo(function GisTree({ + checkedNodes = [], + setCheckedNodes = () => { }, + visible, + setVisible, + onCheckedChange, + onLayerIconClick, + imageVisible = false, + isPlaying = false, + setIsPlaying = () => {}, + targetLayerTypes = [], + orderedLayers = [], + checkInterval = 3000, +}) { const { gisCore, treeData } = useGisContext(); const [checked, setChecked] = useState([]); const [expanded, setExpanded] = useState([]); const [nodes, setNodes] = useState([]); const [activeTab, setActiveTab] = useState('layers'); + const currentAnimationIndexRef = useRef(0); + const targetNodesRef = useRef([]); // 대상 노드들을 저장할 ref + const isPausedRef = useRef(false); // 일시정지 상태를 저장할 ref + const timeoutIdRef = useRef(null); // setTimeout ID를 저장할 ref + useEffect(() => { if (treeData && treeData.length > 0) { const nodeData = setNodeValues(treeData); @@ -188,12 +205,167 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes image.src = canvas.toDataURL('image/png'); return image; }; - const handleLayerImgClick = (nodeNm, nodeId) => { - if(!checked.includes(nodeId)) alert('시설을 선택해주세요') + const handleLayerImgClick = (nodeNm, nodeId, layerNm) => { + if (!checked.includes(nodeId)) alert('시설을 선택해주세요') else onLayerIconClick(nodeNm) } + + // 특정 노드를 체크/언체크하는 함수 + const checkSpecificNode = useCallback((nodeId, shouldCheck = true) => { + setChecked((prevChecked) => { + let newChecked = [...prevChecked]; + const isCurrentlyChecked = prevChecked.includes(nodeId); + + if (shouldCheck && !isCurrentlyChecked) { + // 체크하기 + const descendants = getDescendants(nodeId, nodes); + newChecked = Array.from(new Set([...newChecked, nodeId, ...descendants])); + const ancestors = getAncestors(nodeId, nodes); + ancestors.forEach((p) => { + if (isEveryChildChecked(p, nodes, newChecked)) { + newChecked.push(p); + } + }); + } else if (!shouldCheck && isCurrentlyChecked) { + // 언체크하기 + const descendants = getDescendants(nodeId, nodes); + newChecked = newChecked.filter((cid) => cid !== nodeId && !descendants.includes(cid)); + const ancestors = getAncestors(nodeId, nodes); + ancestors.forEach((p) => { + if (newChecked.includes(p)) { + newChecked = newChecked.filter((cid) => cid !== p); + } + }); + } + + // 지도에 레이어 표시/숨김 적용 + const allIds = nodes.map((n) => n.id); + const unchecked = allIds.filter((nid) => !newChecked.includes(nid)); + gisCore.setLayerOnMap(newChecked, true); + gisCore.setLayerOnMap(unchecked, false); + + // 상위로 변경 사항 전달 + const checkedNodesData = nodes.filter(node => newChecked.includes(node.id)); + onCheckedChange?.(newChecked, checkedNodesData); + + return newChecked; + }); + }, [nodes]); + + // 침수위험지도 실행 함수 + const handleAutoCheckAnimation = useCallback((isEnabled, layerTypes = [], interval = 3000) => { + // 기존 타임아웃 정리 + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + timeoutIdRef.current = null; + } + + if (!isEnabled || layerTypes.length === 0) { + // 애니메이션 비활성화 시 상태 초기화 + isPausedRef.current = false; + targetNodesRef.current = []; + currentAnimationIndexRef.current = 0; + return; + } + + // 대상 노드들을 찾아서 특정 순서로 정렬 후 배열로 반환 + const getTargetNodesInOrder = () => { + const orderedNodes = []; + orderedLayers.forEach(lyrSn => { + const node = nodes.find(n => n.id === lyrSn && layerTypes.includes(n.data?.lyrTyNm)); + if (node) { + orderedNodes.push(node); + } + }); + + return orderedNodes; + }; + + const targetNodes = getTargetNodesInOrder(); + + if (targetNodes.length === 0) { + console.warn('대상 레이어를 찾을 수 없습니다:', layerTypes); + return; + } + + // 처음 시작하는 경우에만 초기화 + if (targetNodesRef.current.length === 0) { + targetNodesRef.current = targetNodes; + currentAnimationIndexRef.current = 0; + + console.log('자동 체크 대상 노드들 (순서대로):', targetNodes.map(n => ({ id: n.id, name: n.text, type: n.data?.lyrTyNm }))); + + // 모든 대상 레이어를 먼저 언체크 + targetNodes.forEach(node => { + checkSpecificNode(node.id, false); + }); + } + + // 애니메이션 재개/시작 + isPausedRef.current = false; + + const processNextLayer = () => { + // 일시정지 상태이거나 애니메이션이 비활성화되면 중단 + if (isPausedRef.current || !isPlaying) { + return; + } + + const currentTargetNodes = targetNodesRef.current; + const currentIndex = currentAnimationIndexRef.current; + + if (currentIndex < currentTargetNodes.length) { + checkSpecificNode(currentTargetNodes[currentIndex].id, true); + if (currentIndex > 0) { + checkSpecificNode(currentTargetNodes[currentIndex - 1].id, false); + } + + currentAnimationIndexRef.current++; + + // 다음 레이어 처리를 위한 타이머 설정 + timeoutIdRef.current = setTimeout(processNextLayer, interval); + } else { + // 애니메이션 완료 - 상태 초기화 및 완료 신호 전송 + console.log('애니메이션 완료'); + isPausedRef.current = false; + targetNodesRef.current = []; + currentAnimationIndexRef.current = 0; + timeoutIdRef.current = null; + setIsPlaying?.(false); + } + }; + + processNextLayer(); + + }, [nodes, checkSpecificNode, setIsPlaying, isPlaying]); + + // 애니메이션 일시정지 함수 + const pauseAnimation = useCallback(() => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + timeoutIdRef.current = null; + } + isPausedRef.current = true; + }, []); + + // 외부에서 전달받은 props로 자동 체크 제어 + useEffect(() => { + if (isPlaying) handleAutoCheckAnimation(true, targetLayerTypes, checkInterval); // 재생 시작/재개 + else pauseAnimation(); // 일시정지 + }, [isPlaying, handleAutoCheckAnimation, targetLayerTypes, checkInterval, pauseAnimation]); + + // 컴포넌트 언마운트 시 정리 + useEffect(() => { + return () => { + if (timeoutIdRef.current) { + clearTimeout(timeoutIdRef.current); + } + }; + }, []); + + if (!treeData || treeData.length === 0) return null; + return ( <>
handleCheck(node.id)} className="cursor-pointer" /> {node.icon && {node.icon}} {node.text} - {imageVisible ? depth > 0 && {handleLayerImgClick(node.text, node.id)}} alt={node.text}> : null} + {imageVisible ? depth > 0 && { handleLayerImgClick(node.text, node.id, node.data.lyrTyNm) }} alt={node.text}> : null}
)} sort={(a, b) => { From cb153f1c4a7405f00205e82cf1ee909947413167 Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 5 Aug 2025 17:29:01 +0900 Subject: [PATCH 15/20] =?UTF-8?q?refactor:=20-=20=EC=B9=A8=EC=88=98?= =?UTF-8?q?=EC=8B=AC=20=EB=B2=94=EB=A1=80=20interval=201=EC=B4=88=20->=202?= =?UTF-8?q?.5=EC=B4=88=20=EC=88=98=EC=A0=95=20(=EB=8F=84=EC=8B=9C=EC=B9=A8?= =?UTF-8?q?=EC=88=98=EC=A7=80=EB=8F=84=20=ED=91=9C=EC=B6=9C=20=EB=95=8C?= =?UTF-8?q?=EB=AC=B8)=20-=20page.tsx=20=ED=83=80=EC=9E=85=20=EB=8B=A8?= =?UTF-8?q?=EC=96=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/(map-layout)/map/page.tsx | 8 ++++---- apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/page.tsx b/apps/sn/app/(map-layout)/map/page.tsx index fe4ff9665..3728332f0 100644 --- a/apps/sn/app/(map-layout)/map/page.tsx +++ b/apps/sn/app/(map-layout)/map/page.tsx @@ -156,10 +156,10 @@ const Map = () => { searchBar={SearchBar} waterDepth={showWaterDepth ? WaterDepth : undefined} isPlaying={isPlaying} - setIsPlaying={setIsPlaying} - targetLayerTypes={WATER_DEPTH_LAYER_TYPES} - orderedLayers={ORDERED_LAYERS} - checkInterval={1000} + setIsPlaying={setIsPlaying as any} + targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any} + orderedLayers={ORDERED_LAYERS as any} + checkInterval={2500} />
); diff --git a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx index 652ac81fd..91ec47009 100644 --- a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx +++ b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx @@ -3,12 +3,12 @@ import React, { useRef, useEffect, useState } from "react"; import { Label } from "@common/components/ui/label"; -export const WATER_DEPTH_LAYER_TYPES = [ +export const WATER_DEPTH_LAYER_TYPES: string[] = [ 'tb_ms_flood_dngr_map', 'tb_ms_sbmrg_dngr_map' ]; // 지정된 순서에 따른 lyr_sn 배열 -export const ORDERED_LAYERS = [ +export const ORDERED_LAYERS: string[] = [ '27', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 30년 빈도 '28', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 50년 빈도 '3', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 80년 빈도 From a6b34780ae7c9a7ca905ed6191ec785aa88f94be Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 5 Aug 2025 18:11:09 +0900 Subject: [PATCH 16/20] =?UTF-8?q?feat:=20=EC=B9=A8=EC=88=98=EC=8B=AC=20?= =?UTF-8?q?=EB=B2=94=EB=A1=80=EC=97=90=20=EC=A7=84=ED=96=89=EB=B0=94(Progr?= =?UTF-8?q?ess=20Bar)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/map/Gis.js | 13 ++++++++- .../business_components/src/ui/map/GisTree.js | 27 +++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 046619de1..68c9ee956 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -118,7 +118,7 @@ function Gis({ searchBar: SearchBar, waterDepth: WaterDepth, isPlaying = false, - setIsPlaying = () => {}, + setIsPlaying = () => { }, targetLayerTypes = [], orderedLayers = [], checkInterval = 3000, @@ -128,6 +128,8 @@ function Gis({ const [mapObject, setMapObject] = useState({}); const [tabVisible, setTabVisible] = useState(true); const [layerNm, setLayerNm] = useState(""); + const [totalSteps, setTotalSteps] = useState(0); + const [currentStep, setCurrentStep] = useState(0); // const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 // const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터 // const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 @@ -280,6 +282,12 @@ function Gis({ [onCheckedLayersChange], ); + // 진행률 변경 핸들러 + const handleProgressChange = useCallback((total, current) => { + setTotalSteps(total); + setCurrentStep(current); + }, []); + return ( <> {typeof WaterDepth === "function" && ( @@ -323,6 +332,8 @@ function Gis({ onClose={() => setIsOpen(false)} isPlaying={isPlaying} setIsPlaying={setIsPlaying} + totalSteps={totalSteps} + currentStep={currentStep} />
)} diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index 1880eb225..ed6bb00fc 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -18,10 +18,11 @@ export default React.memo(function GisTree({ onLayerIconClick, imageVisible = false, isPlaying = false, - setIsPlaying = () => {}, - targetLayerTypes = [], - orderedLayers = [], - checkInterval = 3000, + setIsPlaying = () => { }, + targetLayerTypes = [], + orderedLayers = [], + checkInterval = 3000, + onProgressChange = () => { }, }) { const { gisCore, treeData } = useGisContext(); const [checked, setChecked] = useState([]); @@ -265,6 +266,7 @@ export default React.memo(function GisTree({ isPausedRef.current = false; targetNodesRef.current = []; currentAnimationIndexRef.current = 0; + onProgressChange(0, 0); return; } @@ -295,6 +297,9 @@ export default React.memo(function GisTree({ console.log('자동 체크 대상 노드들 (순서대로):', targetNodes.map(n => ({ id: n.id, name: n.text, type: n.data?.lyrTyNm }))); + // 진행률 초기화 (전체 단계 수 설정) + onProgressChange(targetNodes.length, 0); + // 모든 대상 레이어를 먼저 언체크 targetNodes.forEach(node => { checkSpecificNode(node.id, false); @@ -321,6 +326,9 @@ export default React.memo(function GisTree({ currentAnimationIndexRef.current++; + // 진행률 업데이트 + onProgressChange(currentTargetNodes.length, currentAnimationIndexRef.current); + // 다음 레이어 처리를 위한 타이머 설정 timeoutIdRef.current = setTimeout(processNextLayer, interval); } else { @@ -330,13 +338,22 @@ export default React.memo(function GisTree({ targetNodesRef.current = []; currentAnimationIndexRef.current = 0; timeoutIdRef.current = null; + + // 진행률 100% 완료 + onProgressChange(currentTargetNodes.length, currentTargetNodes.length); + + // 잠시 후 진행률 초기화 + setTimeout(() => { + onProgressChange(0, 0); + }, 1000); + setIsPlaying?.(false); } }; processNextLayer(); - }, [nodes, checkSpecificNode, setIsPlaying, isPlaying]); + }, [nodes, checkSpecificNode, setIsPlaying, isPlaying, onProgressChange]); // 애니메이션 일시정지 함수 const pauseAnimation = useCallback(() => { From 66d02cda0a2254c7e90ba443ceaac16a1e0adfd7 Mon Sep 17 00:00:00 2001 From: swcho Date: Tue, 5 Aug 2025 18:17:46 +0900 Subject: [PATCH 17/20] =?UTF-8?q?feat:=20=EC=B9=A8=EC=88=98=EC=8B=AC=20?= =?UTF-8?q?=EB=B2=94=EB=A1=80=EC=97=90=20=EC=A7=84=ED=96=89=EB=B0=94(Progr?= =?UTF-8?q?ess=20Bar)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../map/waterDepth/waterDepth.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx index 91ec47009..f039d7ad5 100644 --- a/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx +++ b/apps/sn/app/(map-layout)/map/waterDepth/waterDepth.tsx @@ -22,12 +22,16 @@ interface WaterDepthProps { onClose?: () => void; isPlaying: boolean; setIsPlaying: (isPlaying: boolean) => void; + totalSteps?: number; + currentStep?: number; } export function WaterDepth({ onClose, isPlaying, setIsPlaying, + totalSteps = 0, + currentStep = 0, }: WaterDepthProps) { useEffect(() => { @@ -38,10 +42,14 @@ export function WaterDepth({ setIsPlaying(!isPlaying); }; + // 진행률 계산 (0-100%) + const progressPercentage = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 100; + + return ( <>
-
+
-
+
+
+
+
+
침 수 심
From 6c0d65a69ae37e063d9001a41bfecdc3de96be5d Mon Sep 17 00:00:00 2001 From: swcho Date: Wed, 6 Aug 2025 09:50:50 +0900 Subject: [PATCH 18/20] =?UTF-8?q?feat:=20Gis=20Search=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20hover=EC=8B=9C=EC=97=90=20=ED=91=9C=EC=8B=9C=EB=90=98?= =?UTF-8?q?=EB=8A=94=20title=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/business_components/src/ui/map/Gis.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 68c9ee956..7c92c9f59 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -373,6 +373,7 @@ function Gis({