From cec2ebb8c086f76a787aeb294df728e93b89d3d5 Mon Sep 17 00:00:00 2001 From: swcho Date: Mon, 12 May 2025 14:04:24 +0900 Subject: [PATCH] =?UTF-8?q?feat,=20fix=20:=20SearchModal=20=EB=B0=8F=20Gis?= =?UTF-8?q?Tab=EC=97=90=EC=84=9C=20onFeatureSelect=20prop=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/SearchModal.tsx | 472 ++++++++---------- .../business_components/src/ui/map/Gis.js | 16 +- .../business_components/src/ui/map/GisTab.js | 8 +- 3 files changed, 218 insertions(+), 278 deletions(-) diff --git a/apps/sn/app/map/SearchModal.tsx b/apps/sn/app/map/SearchModal.tsx index 84624e45b..ba8b34df0 100644 --- a/apps/sn/app/map/SearchModal.tsx +++ b/apps/sn/app/map/SearchModal.tsx @@ -1,19 +1,32 @@ -import React from 'react'; +import FeatureClickEventPopup from './featureClickEventPopup'; +import React, { useState } from 'react'; import axios from 'axios'; import { UiModal } from '@common/business_components/ui/dialog_ui'; import { Button } from '@common/components'; import { UiTable2 } from '@common/business_components/ui/table/index-2'; -import { CommonTableColumn } from "./table"; const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`; +// 탭 const TABS = [ { key: 'basic', label: '기본정보' }, { key: 'operation', label: '운영정보' }, { key: 'report', label: '하수도정비기본계획보고서' }, ]; - +// 기본정보 '구분' 선택 목록 +const BASIC_TYPES = [ + { key: 'SEWER_PIPE', label: '하수관로' }, + { key: 'tb_il_swage_prcs_fclt', label: '하수처리시설' }, + { key: 'tb_ms_swage_prcs_zone', label: '하수처리구역' }, + { key: 'MANHOLE', label: '하수맨홀' }, + { key: 'WASTEWATER_PUMP', label: '오수펌프' }, + { key: 'RAINWATER_PUMP', label: '빗물펌프' }, + { key: 'OUTFALL', label: '우수토실' }, + { key: 'RETENTION_FACILITY', label: '하수저류시설' }, + { key: 'PRIORITY_OUTFALL', label: '중점우수토실' }, +]; +// 운영정보 '구분' 선택 목록 const WORK_TYPES = [ { key: 'maintenance', label: '보수 및 갱신' }, { key: 'cleaning', label: '청소 및 준설' }, @@ -70,19 +83,6 @@ const swageFcltTableHeadlist = [ { id: 'bsnNm', title: '유역명' }, ]; -// '구분' 선택 목록 -const BASIC_TYPES = [ - { key: 'SEWER_PIPE', label: '하수관로' }, - { key: 'tb_il_swage_prcs_fclt', label: '하수처리시설' }, - { key: 'tb_ms_swage_prcs_zone', label: '하수처리구역' }, - { key: 'MANHOLE', label: '하수맨홀' }, - { key: 'WASTEWATER_PUMP', label: '오수펌프' }, - { key: 'RAINWATER_PUMP', label: '빗물펌프' }, - { key: 'OUTFALL', label: '우수토실' }, - { key: 'RETENTION_FACILITY', label: '하수저류시설' }, - { key: 'PRIORITY_OUTFALL', label: '중점우수토실' }, -]; - const sidoList = [ { id: 0, name: '선택' }, { id: 26, name: '부산광역시' }, @@ -96,8 +96,6 @@ const sidoList = [ { id: 47, name: '경상북도' }, { id: 48, name: '경상남도' }, ]; - -// 시도별 시군구 리스트 매핑 const sigunguMap: Record = { 26: [ { id: 0, name: '선택' }, @@ -200,16 +198,10 @@ const sigunguMap: Record = { const 처리구역List = ['선택', 'A', 'B']; const 설치년도List = ['선택', ...YEARS]; -// swageZoneTableHeadlist, tableData 타입 예시 (실제 타입에 맞게 수정) -type TableHead = { - key: string; - label: string; -}; -type TableRow = Record; - interface SearchModalProps { open: boolean; onOpenChange: (open: boolean) => void; + onFeatureSelect: (row: any) => void; } // filterParams 타입 정의 추가 @@ -221,14 +213,6 @@ type FilterParams = { // 추가 필터 속성들... }; -// 해당 년/월의 마지막 일 구하는 함수 -function getLastDayOfMonth(year: string, month: string) { - if (!year || !month) return 31; - const y = parseInt(year, 10); - const m = parseInt(month, 10); - if (isNaN(y) || isNaN(m)) return 31; - return new Date(y, m, 0).getDate(); -} // 날짜 조합 함수 const getDateString = (year: string, month: string, day: string, defaultDate: string) => { if (!year && !month && !day) return defaultDate; @@ -238,31 +222,34 @@ const getDateString = (year: string, month: string, day: string, defaultDate: st return `${y}${m}${d}`; }; -const SearchModal: React.FC = ({ open, onOpenChange }) => { - const [activeTab, setActiveTab] = React.useState('basic'); - const [workType, setWorkType] = React.useState(WORK_TYPES[0]?.key ?? ''); - const [startYear, setStartYear] = React.useState(''); - const [endYear, setEndYear] = React.useState(''); - const [showTable, setShowTable] = React.useState(false); - const [curPage, setCurPage] = React.useState(1); - const [basicType, setBasicType] = React.useState(BASIC_TYPES[0]?.key ?? ''); - const [하수처리구역명, set하수처리구역명] = React.useState(''); - const [하수처리시설명, set하수처리시설명] = React.useState(''); - const [시도, set시도] = React.useState(0); - const [시군구, set시군구] = React.useState(0); - const [처리구역, set처리구역] = React.useState(''); - const [설치년도1, set설치년도1] = React.useState(''); - const [설치년도2, set설치년도2] = React.useState(''); - const [tableData, setTableData] = React.useState([]); - const [시설용량최소, set시설용량최소] = React.useState(''); - const [시설용량최대, set시설용량최대] = React.useState(''); +const SearchModal: React.FC = ({ open, onOpenChange, onFeatureSelect }) => { + const [activeTab, setActiveTab] = useState('basic'); + const [workType, setWorkType] = useState(WORK_TYPES[0]?.key ?? ''); + const [startYear, setStartYear] = useState(''); + const [endYear, setEndYear] = useState(''); + const [showTable, setShowTable] = useState(false); + const [curPage, setCurPage] = useState(1); + const [basicType, setBasicType] = useState(BASIC_TYPES[0]?.key ?? ''); + const [하수처리구역명, set하수처리구역명] = useState(''); + const [하수처리시설명, set하수처리시설명] = useState(''); + const [시도, set시도] = useState(0); + const [시군구, set시군구] = useState(0); + const [처리구역, set처리구역] = useState(''); + const [설치년도1, set설치년도1] = useState(''); + const [설치년도2, set설치년도2] = useState(''); + const [tableData, setTableData] = useState([]); + const [시설용량최소, set시설용량최소] = useState(''); + const [시설용량최대, set시설용량최대] = useState(''); // 날짜 상태 선언 - const [가동개시_시작_년, set가동개시_시작_년] = React.useState(''); - const [가동개시_시작_월, set가동개시_시작_월] = React.useState(''); - const [가동개시_시작_일, set가동개시_시작_일] = React.useState(''); - const [가동개시_종료_년, set가동개시_종료_년] = React.useState(''); - const [가동개시_종료_월, set가동개시_종료_월] = React.useState(''); - const [가동개시_종료_일, set가동개시_종료_일] = React.useState(''); + const [가동개시_시작_년, set가동개시_시작_년] = useState(''); + const [가동개시_시작_월, set가동개시_시작_월] = useState(''); + const [가동개시_시작_일, set가동개시_시작_일] = useState(''); + const [가동개시_종료_년, set가동개시_종료_년] = useState(''); + const [가동개시_종료_월, set가동개시_종료_월] = useState(''); + const [가동개시_종료_일, set가동개시_종료_일] = useState(''); + + // 상세 정보 조회를 위한 상태 추가 + const [popupFeature, setPopupFeature] = useState(null); // 오늘 날짜 구하기 const today = new Date(); @@ -278,9 +265,6 @@ const SearchModal: React.FC = ({ open, onOpenChange }) => { dynamicHeadlist = swageFcltTableHeadlist; // 하수처리시설 } - - - // 선택된 시도에 따라 시군구 리스트 가져오기 const selectedSigunguList = sigunguMap[시도] || [{ id: 0, name: '선택' }]; @@ -292,12 +276,17 @@ const SearchModal: React.FC = ({ open, onOpenChange }) => { }; // 선택된 필터 값을 저장할 상태 추가 - const [filterParams, setFilterParams] = React.useState({}); + const [filterParams, setFilterParams] = useState({}); // cellClick 함수 타입 지정 - const handleCellClick = (row: TableRow, col: TableHead) => { + const handleCellClick = (index: number | undefined, headId: string) => { // 원하는 동작 구현 - console.log(row, col); + console.log('Cell clicked:', index, headId); + if (typeof index === "number") { + console.log('Selected row data:', tableData[index]); + onFeatureSelect(tableData[index]); + onOpenChange(false); // SearchModal 닫기 + } }; const modalMinWidth = 'min-w-[900px]'; @@ -305,7 +294,7 @@ const SearchModal: React.FC = ({ open, onOpenChange }) => { // 모달 높이 동적 계산 const contentRef = React.useRef(null); - const [dynamicHeight, setDynamicHeight] = React.useState(undefined); + const [dynamicHeight, setDynamicHeight] = useState(undefined); // 구분(basicType)이 바뀔 때마다 queryParams, filterParams 초기화 React.useEffect(() => { @@ -369,73 +358,62 @@ const SearchModal: React.FC = ({ open, onOpenChange }) => { }, [activeTab, showTable, curPage, tableData.length]); + // 하수처리구역용 CQL 필터 생성 + const buildZoneCqlFilters = (params: FilterParams) => { + const cqlFilters: string[] = []; + if (params.ctpv_cd) cqlFilters.push(`ctpv_cd='${params.ctpv_cd}'`); + if (params.sgg_cd) cqlFilters.push(`sgg_cd='${params.sgg_cd}'`); + if (params.swtrmt_zone_nm) cqlFilters.push(`swtrmt_zone_nm ILIKE '%${params.swtrmt_zone_nm}%'`); + if (params.swtrmt_fclt_nm) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${params.swtrmt_fclt_nm}%'`); + return cqlFilters; + }; + // 하수처리시설용 CQL 필터 생성 + const buildFacilityCqlFilters = (params: FilterParams) => { + const cqlFilters: string[] = []; + // 가동개시일자 범위 + const startDate = getDateString(가동개시_시작_년, 가동개시_시작_월, 가동개시_시작_일, '19000101'); + const endDate = getDateString(가동개시_종료_년, 가동개시_종료_월, 가동개시_종료_일, `${todayYear}${todayMonth}${todayDate}`); + cqlFilters.push(`oprtng_strt_ymd >= '${startDate}' and oprtng_strt_ymd <= '${endDate}'`); + // 시설용량 범위 + if (시설용량최소 && 시설용량최대) cqlFilters.push(`fclt_cpct BETWEEN ${시설용량최소} AND ${시설용량최대}`); + else if (시설용량최소) cqlFilters.push(`fclt_cpct >= ${시설용량최소}`); + else if (시설용량최대) cqlFilters.push(`fclt_cpct <= ${시설용량최대}`); + // 하수처리시설명 + if (params.swtrmt_fclt_nm) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${params.swtrmt_fclt_nm}%'`); + return cqlFilters; + }; + // 기타 구분용 CQL 필터 생성 + const buildDefaultCqlFilters = (params: FilterParams) => { + const cqlFilters: string[] = []; + if (params.swtrmt_fclt_nm) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${params.swtrmt_fclt_nm}%'`); + return cqlFilters; + }; + // 검색 API 호출 함수 const fetchFilteredData = () => { const queryParams = new URLSearchParams({ lyrTypeNm: basicType }); - const cqlFilters: string[] = []; + let cqlFilters: string[] = []; + if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = buildZoneCqlFilters(filterParams); + else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = buildFacilityCqlFilters(filterParams); + else cqlFilters = buildDefaultCqlFilters(filterParams); + // 탭 + 구분 별로 cqlFilter 추가 예정 - // 구분별 분기 처리 - if (basicType === 'tb_ms_swage_prcs_zone') { - // 하수처리구역 - // 시도 - if (filterParams.ctpv_cd) { - cqlFilters.push(`ctpv_cd='${filterParams.ctpv_cd}'`); - } - // 시군구 - if (filterParams.sgg_cd) { - cqlFilters.push(`sgg_cd='${filterParams.sgg_cd}'`); - } - // 하수처리구역명 - if (filterParams.swtrmt_zone_nm) { - cqlFilters.push(`swtrmt_zone_nm ILIKE '%${filterParams.swtrmt_zone_nm}%'`); - } - // 하수처리시설명 - if (filterParams.swtrmt_fclt_nm) { - cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${filterParams.swtrmt_fclt_nm}%'`); - } - } else if (basicType === 'tb_il_swage_prcs_fclt') { - // 하수처리시설 - // 가동개시일자 범위 필터 추가 - // 기본값: 19000101 ~ 오늘 - const startDate = getDateString(가동개시_시작_년, 가동개시_시작_월, 가동개시_시작_일, '19000101'); - const endDate = getDateString(가동개시_종료_년, 가동개시_종료_월, 가동개시_종료_일, `${todayYear}${todayMonth}${todayDate}`); - cqlFilters.push(`oprtng_strt_ymd >= '${startDate}' and oprtng_strt_ymd <= '${endDate}'`); - // 시설용량 범위(fclt_cpct) 필터 추가 - if (시설용량최소 && 시설용량최대) cqlFilters.push(`fclt_cpct BETWEEN ${시설용량최소} AND ${시설용량최대}`); - else if (시설용량최소) cqlFilters.push(`fclt_cpct >= ${시설용량최소}`); - else if (시설용량최대) cqlFilters.push(`fclt_cpct <= ${시설용량최대}`); - // 하수처리시설명 - if (filterParams.swtrmt_fclt_nm) { - cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${filterParams.swtrmt_fclt_nm}%'`); - } - } else { - // 기타 구분(예: 하수관로, 맨홀 등) - 필요시 추가 - // 예시: 이름으로 검색 - if (filterParams.swtrmt_fclt_nm) { - cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${filterParams.swtrmt_fclt_nm}%'`); - } - } - - // 파라미터 이름 오타 주의! (Swagger와 동일하게) - if (cqlFilters.length > 0) { - queryParams.append('cqlFiter', cqlFilters.join(' and ')); - } + if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and ')); console.log('API URL:', `${API_URL}/v1/map/features?${queryParams.toString()}`); axios.get(`${API_URL}/v1/map/features?${queryParams.toString()}`) .then(res => { - // 결과값을 표에 사용할 수 있도록 상태에 저장 console.log('필터링된 API 결과:', res.data.length); console.log('필터링된 API 결과:', res.data); - setTableData(res.data || []); // res.data 구조에 따라 조정 필요 + setTableData(res.data || []); setShowTable(true); }) .catch(err => { console.error('API ERROR:', err); - setTableData([]); // 에러 시 빈 배열로 초기화 + setTableData([]); }); }; @@ -484,36 +462,9 @@ const SearchModal: React.FC = ({ open, onOpenChange }) => { - {/* 분기처리: 하수처리구역 */} - {basicType === 'tb_ms_swage_prcs_zone' && ( -
-
- - - - -
-
- - set하수처리구역명(e.target.value)} /> -
-
- - set하수처리시설명(e.target.value)} /> -
-
- )} {/* 분기처리: 하수처리시설 */} {basicType === 'tb_il_swage_prcs_fclt' && ( -
+
{/* 가동개시일자 */}
@@ -529,32 +480,10 @@ const SearchModal: React.FC = ({ open, onOpenChange }) => { return ; })} - {/* - */} - ~ + ~ {/* 종료일자 */} - {/* - */} -
- {/* 시설용량 */} -
+ {/* 시설용량 */} = ({ open, onOpenChange }) => { ~ { @@ -642,14 +520,36 @@ const SearchModal: React.FC = ({ open, onOpenChange }) => { set시설용량최대(val); }} /> -
- {/* 하수처리시설명 */} -
+ {/* 하수처리시설명 */} set하수처리시설명(e.target.value)} />
)} + {/* 분기처리: 하수처리구역 */} + {basicType === 'tb_ms_swage_prcs_zone' && ( +
+
+ + + + + + set하수처리구역명(e.target.value)} /> + + set하수처리시설명(e.target.value)} /> +
+
+ )} + {/* 조회 버튼 중앙 정렬 */}
@@ -660,10 +560,10 @@ const SearchModal: React.FC = ({ open, onOpenChange }) => { {/* 운영정보 탭 */} {activeTab === 'operation' && (
-
- +
+ - + - ~ + ~
+ {/* 조회 버튼 중앙 정렬 */}
- +
)} - {/* 하수도정비기본계획보고서서 탭 */} + {/* 하수도정비기본계획보고서 탭 */} {activeTab === 'report' && (
{/* 시도, 시군구, 하수처리구역 한 줄 */} -
- - set시도(e.target.value)}> {sidoList.map(opt => ( ))} - - set시군구(e.target.value)}> {/* {sigunguMap.map(opt => ( ))} */} - - set처리구역(e.target.value)}> {처리구역List.map(opt => ( ))} - -
- {/* 승인년도 한 줄 */} -
- - + + ~ - set설치년도2(e.target.value)}> {설치년도List.map(opt => ( ))} @@ -748,48 +644,92 @@ const SearchModal: React.FC = ({ open, onOpenChange }) => {
{/* 조회 버튼 중앙 정렬 */}
- +
)} {/* 📋 결과 리스트 */} -
+
{ + // Shift+Wheel은 브라우저가 기본적으로 가로 스크롤을 처리하지만, + // 세로 스크롤이 최상단/최하단에 도달했을 때 가로 스크롤이 막히는 현상을 해결 + const container = e.currentTarget; + const isShift = e.shiftKey; + if (!isShift) return; + + // 세로 스크롤 위치 + const atTop = container.scrollTop === 0; + const atBottom = Math.ceil(container.scrollTop + container.clientHeight) >= container.scrollHeight; + + // 세로 스크롤이 최상단/최하단에 도달한 경우에만 가로 스크롤 수동 처리 + if ((atTop && e.deltaY < 0) || (atBottom && e.deltaY > 0)) { + container.scrollLeft += e.deltaY; + e.preventDefault(); + } + }} + > {/* 선택된 탭의 label */} -
+
{TABS.find(tab => tab.key === activeTab)?.label}
- {/* "▶" + 선택된 탭 label + " 조회 결과" */} -
- ▶ {TABS.find(tab => tab.key === activeTab)?.label} 조회 결과 -
+ {/* 조회 결과 라벨 및 건수 표시 */} + { + (() => { + // 조회 건수 변수 선언 + const resultCount = tableData.length; + // 탭별 라벨 + const label = + activeTab === 'basic' + ? BASIC_TYPES.find(type => type.key === basicType)?.label + : activeTab === 'operation' + ? WORK_TYPES.find(type => type.key === workType)?.label + : activeTab === 'report' + ? '하수도정비기본계획보고서' + : TABS.find(tab => tab.key === activeTab)?.label; + return ( +
+ ▶ {label} 조회 결과 + ({resultCount.toLocaleString()}건) +
+ ); + })() + } { }} onPageChange={setCurPage} + cellClick={handleCellClick} onPageChange={setCurPage} resetPageNo={false} cur={curPage} /> + {/* FeatureClickEventPopup 표시 */} + { + popupFeature && ( + setPopupFeature(null)} + /> + ) + }
); - {/* - - ); */} return ( - + body={renderBody()} + headerTitle="검색" + // 필요시 footerContent 등 추가 + /> ); } diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index 97918d5b7..ce416bfd2 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -34,20 +34,15 @@ function Gis({ treeData = [], isSearch = false }) { setIsOpen(false); // 팝업 닫기 setFeatureData(null) }; + + // SearchModal, GisContent 등에서 호출 const handleFeatureSelect = (data) => { console.log('check',data) setFeatureData(data); // 선택된 피처 데이터 저장 setIsOpen(true); // 팝업 열기 + setIsSearchModalOpen(false); // SearchModal 닫기 }; - // SearchModal이 열릴 때 featureClickEventPopup 닫기 - useEffect(() => { - if (isSearchModalOpen && isOpen) { - setIsOpen(false); - setFeatureData(null); - } - }, [isSearchModalOpen]); - useEffect(() => { if (!mapRef.current) return; @@ -151,9 +146,10 @@ function Gis({ treeData = [], isSearch = false }) { <>
- + - {isOpen && featureData && ( + {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} + {isOpen && featureData && !isSearchModalOpen && ( + {/* 접기 버튼 */}