Gis Search 모달 작업(추가 작업 필요)
iipbs_sn / build (push) Waiting to run

This commit is contained in:
swcho
2025-05-02 17:32:58 +09:00
parent 24d4f2d326
commit 2aa31be1dc
4 changed files with 418 additions and 7 deletions
@@ -31,6 +31,7 @@
"@radix-ui/react-scroll-area": "^1.2.3", "@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6", "@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"axios": "1.8.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
@@ -2,10 +2,13 @@ import React, { useState } from 'react';
import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs'; import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs';
import GisTree from './GisTree'; import GisTree from './GisTree';
import GisSearch from './GisSearch'; import GisSearch from './GisSearch';
import { ChevronLeft, ChevronRight } from 'lucide-react'; import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
import SearchModal from './SearchModal';
function GisTab({ visible, setVisible, isSearch }) { function GisTab({ visible, setVisible, isSearch }) {
const [activeTab, setActiveTab] = useState('layers'); const [activeTab, setActiveTab] = useState('layers');
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
return ( return (
<> <>
@@ -17,21 +20,29 @@ function GisTab({ visible, setVisible, isSearch }) {
<TabsList className="flex-none mt-4 border-none rounded-none"> <TabsList className="flex-none mt-4 border-none rounded-none">
<TabsTrigger value="layers" className="flex-1 h-10">레이어</TabsTrigger> <TabsTrigger value="layers" className="flex-1 h-10">레이어</TabsTrigger>
{isSearch && ( {isSearch && (
<TabsTrigger value="search" className="flex-1 h-10">검색</TabsTrigger> <button
type="button"
onClick={() => setIsSearchModalOpen(true)}
className="h-10 w-10 flex items-center justify-center px-0"
>
<Search size={18} className="text-gray-700" />
</button>
// <TabsTrigger value="search" className="flex-1 h-10 flex items-center justify-center">
// <Search size={18} />
// </TabsTrigger>
)} )}
</TabsList> </TabsList>
{/* GisTree는 항상 마운트되며, 탭 상태에 따라 보여주기만 함 */} {/* GisTree는 항상 마운트되며, 탭 상태에 따라 보여주기만 함 */}
<div <div
className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${ className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${activeTab === 'layers' ? 'block' : 'hidden'
activeTab === 'layers' ? 'block' : 'hidden' }`}
}`}
> >
<GisTree /> <GisTree />
</div> </div>
{/* GisSearch도 동일하게 처리 */} {/* GisSearch도 동일하게 처리 */}
{isSearch && ( {/* {isSearch && (
<div <div
className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${ className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${
activeTab === 'search' ? 'block' : 'hidden' activeTab === 'search' ? 'block' : 'hidden'
@@ -39,7 +50,11 @@ function GisTab({ visible, setVisible, isSearch }) {
> >
<GisSearch /> <GisSearch />
</div> </div>
)} )} */}
{/* 🔍 검색 모달 */}
<SearchModal open={isSearchModalOpen} onOpenChange={setIsSearchModalOpen} />
</Tabs> </Tabs>
{/* 접기 버튼 */} {/* 접기 버튼 */}
@@ -0,0 +1,377 @@
import React from 'react';
import axios from 'axios';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogClose,
} from '@common/components/ui/dialog';
import { Button } from '@common/components';
import { UiTable } from '../../ui';
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}`;
const TABS = [
{ key: 'basic', label: '기본정보' },
{ key: 'operation', label: '운영정보' },
{ key: 'report', label: '하수도정비기본계획보고서' },
];
const WORK_TYPES = [
{ key: 'maintenance', label: '보수 및 갱신' },
{ key: 'cleaning', label: '청소 및 준설' },
{ key: 'inspection', label: '점검 및 진단' },
];
const YEARS = Array.from({ length: 150 }, (_, i) => `${2025 - i}`);
const tableHeadlist = [
{ id: 'no', title: 'NO' },
{ id: 'fileNm', title: '파일명' },
{ id: 'manageYear', title: '년도' },
{ id: 'bungi', title: '분기' },
{ id: 'registDt', title: '등록 일자' },
{ id: 'registUsr', title: '등록자' }
]
const tableData = [
{ 'no': '1', 'fileNm': '파일명', 'manageYear': '2023', 'registDt': '2025-04-16', 'registUsr': 'me', 'bungi': '1분기' },
{ 'no': '2', 'fileNm': '파일명', 'manageYear': '2024', 'registDt': '2025-04-16', 'registUsr': 'me2', 'bungi': '1분기' },
{ 'no': '3', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '4', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '5', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '6', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '7', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '8', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '9', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' },
{ 'no': '10', 'fileNm': '파일명', 'manageYear': '2025', 'registDt': '2025-04-16', 'registUsr': 'me3', 'bungi': '1분기' }
]
const BASIC_TYPES = [
{ key: 'SEWER_PIPE', label: '하수관로' },
{ key: 'TREATMENT_FACILITY', label: '하수처리시설' },
{ key: 'TREATMENT_AREA', 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 = ['선택', '서울', '경기', '부산'];
const sigunguList = ['선택', '강남구', '서초구', '수원시'];
const 처리구역List = ['선택', 'A', 'B'];
const 하수관용도List = ['선택', '우수', '오수'];
const 관재질List = ['선택', 'PVC', 'PE'];
const 설치년도List = ['선택', ...YEARS];
const 경과년도List = ['선택', '1', '2', '3'];
// tableHeadlist, tableData 타입 예시 (실제 타입에 맞게 수정)
type TableHead = {
key: string;
label: string;
};
type TableRow = Record<string, any>;
interface SearchModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
const SearchModal: React.FC<SearchModalProps> = ({ open, onOpenChange }) => {
const [activeTab, setActiveTab] = React.useState('operation');
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('');
const [처리구역, set처리구역] = React.useState('');
const [하수관용도, set하수관용도] = React.useState('');
const [관재질, set관재질] = React.useState('');
const [설치년도1, set설치년도1] = React.useState('');
const [설치년도2, set설치년도2] = React.useState('');
const [경과년도, set경과년도] = React.useState('');
const [연장1, set연장1] = React.useState('');
const [연장2, set연장2] = React.useState('');
// cellClick 함수 타입 지정
const handleCellClick = (row: TableRow, col: TableHead) => {
// 원하는 동작 구현
console.log(row, col);
};
const modalMinWidth = 'min-w-[900px]';
const modalMaxWidth = 'max-w-6xl';
// 모달 높이 동적 계산
const contentRef = React.useRef<HTMLDivElement>(null);
const [dynamicHeight, setDynamicHeight] = React.useState<number | undefined>(undefined);
// API 호출 테스트 (axios 사용)
React.useEffect(() => {
// API_URL root 경로 확인 테스트
console.log('API_URL:', API_URL);
// lyrTypeNm과 cql_filter에 따른 API 호출 테스트
axios.get(`${API_URL}/v1/map/features?lyrTypeNm=tb_ms_swage_prcs_zone&cql_filter=ctpv_nm='전라북도'`)
.then(res => {
console.log('API DATA:', res.data); // 실제 JSON 데이터 출력
})
.catch(err => {
console.error('API ERROR:', err);
});
}, []);
React.useEffect(() => {
if (contentRef.current) {
setDynamicHeight(contentRef.current.scrollHeight + 60); // 여유 padding
}
}, [activeTab, showTable, curPage, tableData.length]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={`
${modalMaxWidth} ${modalMinWidth}
transition-all duration-300
w-full overflow-hidden
`}
style={{ minHeight: showTable ? 600 : 220 }}
>
<DialogHeader>
<DialogTitle>검색</DialogTitle>
</DialogHeader>
{/* 탭 영역 */}
<div className="flex gap-2 mb-4">
{TABS.map(tab => (
<button
key={tab.key}
className={`px-4 py-2 rounded-t border border-b-0 text-base font-semibold
${activeTab === tab.key
? 'bg-[#bfcfe6] text-black'
: 'bg-[#e6e6e6] text-gray-700'
}`}
style={{
borderColor: '#888',
borderBottom: activeTab === tab.key ? '2px solid white' : undefined,
minWidth: 140,
fontSize: '1.1rem'
}}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</button>
))}
</div>
{/* 기본정보 탭 */}
{activeTab === 'basic' && (
<div className="border rounded p-6 mb-4">
{/* 구분 */}
<div className="flex items-center mb-3">
<label className="font-semibold mr-2">구분</label>
<select className="border rounded px-2 py-1" value={basicType} onChange={e => setBasicType(e.target.value)}>
{BASIC_TYPES.map(opt => (
<option key={opt.key} value={opt.key}>{opt.label}</option>
))}
</select>
</div>
{/* 관리번호, 시도, 시군구 한 줄 */}
<div className="flex items-center mb-3">
<label className="font-semibold mr-2">관리번호</label>
<input className="border rounded px-2 py-1 mr-4" value={관리번호} onChange={e => set관리번호(e.target.value)} />
<label className="font-semibold mr-2">시도</label>
<select className="border rounded px-2 py-1 mr-4" value={시도} onChange={e => set시도(e.target.value)}>
{sidoList.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<label className="font-semibold mr-2">시군구</label>
<select className="border rounded px-2 py-1" value={시군구} onChange={e => set시군구(e.target.value)}>
{sigunguList.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
{/* 처리구역, 하수관용도, 관재질 한 줄 */}
<div className="flex items-center mb-3">
<label className="font-semibold mr-2">처리구역</label>
<select className="border rounded px-2 py-1 mr-4" value={처리구역} onChange={e => set처리구역(e.target.value)}>
{처리구역List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<label className="font-semibold mr-2">하수관용도</label>
<select className="border rounded px-2 py-1 mr-4" value={하수관용도} onChange={e => set하수관용도(e.target.value)}>
{하수관용도List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<label className="font-semibold mr-2">관재질</label>
<select className="border rounded px-2 py-1" value={관재질} onChange={e => set관재질(e.target.value)}>
{관재질List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
{/* 설치년도, 경과년도 한 줄 */}
<div className="flex items-center mb-3">
<label className="font-semibold mr-2">설치년도</label>
<select className="border rounded px-2 py-1 mr-2" value={설치년도1} onChange={e => set설치년도1(e.target.value)}>
{설치년도List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<span className="mx-1">~</span>
<select className="border rounded px-2 py-1 mr-6" value={설치년도2} onChange={e => set설치년도2(e.target.value)}>
{설치년도List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<label className="font-semibold mr-2">경과년도</label>
<select className="border rounded px-2 py-1" value={경과년도} onChange={e => set경과년도(e.target.value)}>
{경과년도List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
{/* 연장 한 줄 */}
<div className="flex items-center mb-4">
<label className="font-semibold mr-2">연장(m)</label>
<input className="border rounded px-2 py-1 mr-2" value={연장1} onChange={e => set연장1(e.target.value)} />
<span className="mx-1">~</span>
<input className="border rounded px-2 py-1" value={연장2} onChange={e => set연장2(e.target.value)} />
</div>
{/* 조회 버튼 중앙 정렬 */}
<div className="flex justify-center">
<Button className="w-40 text-base py-2">조회</Button>
</div>
</div>
)}
{/* 운영정보 탭 */}
{activeTab === 'operation' && (
<div className="border rounded p-6 mb-4">
<div className="flex items-center gap-6 mb-6">
<label className="w-24 font-semibold text-base">업무구분</label>
<select
className="border rounded px-3 py-2 text-base"
value={workType}
onChange={e => setWorkType(e.target.value)}
style={{ minWidth: 140 }}
>
{WORK_TYPES.map(w => (
<option key={w.key} value={w.key}>{w.label}</option>
))}
</select>
<label className="w-16 text-center font-semibold text-base">년도</label>
<select
className="border rounded px-3 py-2 text-base"
value={startYear}
onChange={e => setStartYear(e.target.value)}
style={{ minWidth: 100 }}
>
<option value="">선택</option>
{YEARS.map(y => (
<option key={y} value={y}>{y}</option>
))}
</select>
<span className="mx-2 text-lg">~</span>
<select
className="border rounded px-3 py-2 text-base"
value={endYear}
onChange={e => setEndYear(e.target.value)}
style={{ minWidth: 100 }}
>
<option value="">선택</option>
{YEARS.map(y => (
<option key={y} value={y}>{y}</option>
))}
</select>
</div>
<div className="flex justify-center">
<Button className="w-40 text-base py-2" onClick={() => setShowTable(true)}>
조회
</Button>
</div>
</div>
)}
{/* 하수도정비기본계획보고서서 탭 */}
{activeTab === 'report' && (
<div className="border rounded p-6 mb-4">
{/* 시도, 시군구, 하수처리구역 한 줄 */}
<div className="flex items-center mb-3">
<label className="font-semibold mr-2">시도</label>
<select className="border rounded px-2 py-1 mr-4" value={시도} onChange={e => set시도(e.target.value)}>
{sidoList.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<label className="font-semibold mr-2">시군구</label>
<select className="border rounded px-2 py-1 mr-4" value={시군구} onChange={e => set시군구(e.target.value)}>
{sigunguList.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<label className="font-semibold mr-2">하수처리구역</label>
<select className="border rounded px-2 py-1 mr-2" value={처리구역} onChange={e => set처리구역(e.target.value)}>
{처리구역List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<input className="border rounded px-2 py-1 w-24" placeholder="0000" />
</div>
{/* 승인년도 한 줄 */}
<div className="flex items-center mb-4">
<label className="font-semibold mr-2">승인년도</label>
<select className="border rounded px-2 py-1 mr-2" value={설치년도1} onChange={e => set설치년도1(e.target.value)}>
{설치년도List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<span className="mx-1">~</span>
<select className="border rounded px-2 py-1" value={설치년도2} onChange={e => set설치년도2(e.target.value)}>
{설치년도List.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
</div>
{/* 조회 버튼 중앙 정렬 */}
<div className="flex justify-center">
<Button className="w-40 text-base py-2">조회</Button>
</div>
</div>
)}
{/* 📋 결과 리스트 */}
<div className="border-t pt-6 space-y-2">
{/* 선택된 탭의 label */}
<div className="text-2xl font-bold mb-2">
{TABS.find(tab => tab.key === activeTab)?.label}
</div>
{/* "▶" + 선택된 탭 label + " 조회 결과" */}
<div className="text-lg font-semibold mb-6">
▶ {TABS.find(tab => tab.key === activeTab)?.label} 조회 결과
</div>
<UiTable headName='' tableData={tableData} headlist={tableHeadlist}
pageSize={10} total={20} children={undefined}
cellClick={() => { }} onPageChange={function (page: number): void {
throw new Error('Function not implemented.')
}}>
</UiTable>
</div>
</DialogContent>
</Dialog>
);
}
export default React.memo(SearchModal);
@@ -0,0 +1,18 @@
export default function SearchIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6 text-gray-700"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-4.35-4.35M17 11A6 6 0 105 11a6 6 0 0012 0z"
/>
</svg>
);
}