Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs-sn-map
iipbs_sn / build (push) Has been cancelled

This commit is contained in:
yschoi
2025-05-09 17:29:01 +09:00
16 changed files with 1328 additions and 563 deletions
+31
View File
@@ -0,0 +1,31 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, DialogOverlay } from '@common/components/ui'
import React from 'react'
interface ModalProps {
isModalOpen: boolean;
setIsModalOpen: (open: boolean) => void;
title?: string;
children: React.ReactNode;
closeModal: () => void;
}
const Modal: React.FC<ModalProps> = ({ isModalOpen, setIsModalOpen, title, children, closeModal }) => {
return (
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogOverlay className='z-[200]'/>
<DialogContent className="w-auto max-w-[1200px] h-auto max-h-[450px] overflow-auto z-[1000]">
{title ? <DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader> : null}
{children}
<DialogFooter>
<DialogClose>
<button className="px-4 py-2 bg-gray-600 text-white rounded" onClick={closeModal}>닫기</button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export default Modal
+796
View File
@@ -0,0 +1,796 @@
import React 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 WORK_TYPES = [
{ key: 'maintenance', label: '보수 및 갱신' },
{ key: 'cleaning', label: '청소 및 준설' },
{ key: 'inspection', label: '점검 및 진단' },
];
const YEARS = Array.from({ length: 100 }, (_, i) => `${2025 - i}`);
// 하수처리구역 테이블 헤더 리스트
const swageZoneTableHeadlist = [
// { id: 'ctpvCd', title: '시도코드' },
{ id: 'ctpvNm', title: '시도명' },
// { id: 'sggCd', title: '시군구코드' },
{ id: 'sggNm', title: '시군구명' },
// { id: 'swtrmtZoneSn', title: '하수처리구역번호' },
{ id: 'swtrmtZoneNm', title: '하수처리구역명' },
{ id: 'swtrmtFcltCd', title: '하수처리시설번호' },
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
]
// 하수처리시설 테이블 헤더 리스트
const swageFcltTableHeadlist = [
{ id: 'swtrmtFcltCd', title: '하수처리시설코드' },
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
{ id: 'fcltCpct', title: '시설용량' },
{ id: 'rgnExpln', title: '지역설명' },
{ id: 'cmcnYmd', title: '준공일자' },
{ id: 'oprtngStrtYmd', title: '가동개시일자' },
{ id: 'clsgYmd', title: '폐쇄일자' },
{ id: 'laeNm', title: '지자체명' },
{ id: 'ctpvNm', title: '시도명' },
{ id: 'sggNm', title: '시군구명' },
{ id: 'sareaNm', title: '권역명' },
{ id: 'lrgeSareaNm', title: '대권역명' },
{ id: 'rvrNm', title: '하천명' },
{ id: 'addr', title: '주소' },
{ id: 'daddr', title: '상세주소' },
{ id: 'rdnmAddr', title: '도로명주소' },
{ id: 'dtlRdnmAddr', title: '상세도로명주소' },
{ id: 'dschgWtstNm', title: '방류수계명' },
{ id: 'pllnArea', title: '부지면적' },
{ id: 'mngRprtvBzentyNm', title: '관리대행업제명' },
{ id: 'swtrmtPpltnCnt', title: '하수처리인구수' },
{ id: 'prcsZoneAreaSum', title: '처리구역면적합계' },
{ id: 'geom', title: 'geometry' },
{ id: 'xcrd', title: 'X좌표' },
{ id: 'ycrd', title: 'Y좌표' },
{ id: 'rprsMsrcCn', title: '대표공법내용' },
{ id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량' },
{ id: 'sttyCrtrToc', title: '법정기준TOC' },
{ id: 'sttyCrtrSs', title: '법정기준SS' },
{ id: 'sttyCrtrTn', title: '법정기준총질소' },
{ id: 'sttyCrtrTph', title: '법정기준총인' },
{ id: 'sttyCrtrClbclCnt', title: '법정기준대장균수' },
{ 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: '부산광역시' },
{ id: 28, name: '인천광역시' },
{ id: 30, name: '대전광역시' },
{ id: 41, name: '경기도' },
{ id: 42, name: '강원도' },
{ id: 44, name: '충청남도' },
{ id: 45, name: '전라북도' },
{ id: 46, name: '전라남도' },
{ id: 47, name: '경상북도' },
{ id: 48, name: '경상남도' },
];
// 시도별 시군구 리스트 매핑
const sigunguMap: Record<number, { id: number; name: string }[]> = {
26: [
{ id: 0, name: '선택' },
// ...부산산광역시의 시군구 추가
],
28: [
{ id: 0, name: '선택' },
// ...인천광역시의 시군구 추가
],
30: [
{ id: 0, name: '선택' },
// ...대전광역시의 시군구 추가
],
41: [
{ id: 0, name: '선택' },
{ id: 41820, name: '가평군' },
{ id: 41550, name: '안성시' },
{ id: 41830, name: '양평군' },
{ id: 41670, name: '여주시' },
{ id: 41500, name: '이천시' },
{ id: 41430, name: '의왕시' },
{ id: 41150, name: '의정부시' },
{ id: 41480, name: '파주시' },
{ id: 41650, name: '포천시' },
// ...경기도의 시군구 추가
],
42: [
{ id: 0, name: '선택' },
{ id: 42170, name: '동해시' },
{ id: 42110, name: '춘천시' },
{ id: 42720, name: '홍천군' },
// ...강원도의 시군구 추가
],
44: [
{ id: 0, name: '선택' },
{ id: 44770, name: '서천군' },
// ...충청남도의 시군구 추가
],
45: [
{ id: 0, name: '선택' },
{ id: 45130, name: '군산시' },
{ id: 45190, name: '남원시' },
{ id: 45730, name: '무주군' },
{ id: 45800, name: '부안군' },
{ id: 45770, name: '순창군' },
{ id: 45140, name: '익산시' },
{ id: 45750, name: '임실군' },
{ id: 45710, name: '완주군' },
{ id: 45110, name: '전주시' },
// ...전라북도의 시군구 추가
],
46: [
{ id: 0, name: '선택' },
{ id: 46810, name: "강진군" },
{ id: 46770, name: "고흥군" },
{ id: 46720, name: "곡성군" },
{ id: 46730, name: "구례군" },
{ id: 46230, name: "광양시" },
{ id: 46170, name: "나주시" },
{ id: 46110, name: "목포시" },
{ id: 46840, name: "무안군" },
{ id: 46150, name: "순천시" },
{ id: 46910, name: "신안군" },
{ id: 46130, name: "여수시" },
{ id: 46890, name: "완도군" },
// ...전라남도의 시군구 추가
],
47: [
{ id: 0, name: '선택' },
{ id: 47130, name: "경주시" },
{ id: 47830, name: "고령군" },
{ id: 47190, name: "구미시" },
{ id: 47720, name: "군위군" },
{ id: 47150, name: "김천시" },
{ id: 47280, name: "문경시" },
{ id: 47920, name: "봉화군" },
{ id: 47250, name: "상주시" },
{ id: 47770, name: "영덕군" },
{ id: 47760, name: "영양군" },
{ id: 47210, name: "영주시" },
{ id: 47900, name: "예천군" },
{ id: 47930, name: "울진군" },
{ id: 47730, name: "의성군" },
{ id: 47750, name: "청송군" },
{ id: 47850, name: "칠곡군" },
{ id: 47110, name: "포항시" },
// ...경상북도의 시군구 추가
],
48: [
{ id: 0, name: '선택' },
{ id: 48310, name: "거제시" },
{ id: 48250, name: "김해시" },
{ id: 48270, name: "밀양시" },
{ id: 48220, name: "통영시" },
// ...경상남도의 시군구 추가
],
// ...나머지 시도별 시군구 추가
};
const 처리구역List = ['선택', 'A', 'B'];
const 설치년도List = ['선택', ...YEARS];
// swageZoneTableHeadlist, tableData 타입 예시 (실제 타입에 맞게 수정)
type TableHead = {
key: string;
label: string;
};
type TableRow = Record<string, any>;
interface SearchModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
// filterParams 타입 정의 추가
type FilterParams = {
swtrmt_zone_nm?: string;
ctpv_cd?: number;
sgg_cd?: number;
swtrmt_fclt_nm?: string;
// 추가 필터 속성들...
};
// 해당 년/월의 마지막 일 구하는 함수
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;
const y = year || defaultDate.slice(0, 4);
const m = month || defaultDate.slice(4, 6);
const d = day || defaultDate.slice(6, 8);
return `${y}${m}${d}`;
};
const SearchModal: React.FC<SearchModalProps> = ({ 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<any[]>([]);
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가동개시_종료_월] = React.useState('');
const [가동개시_종료_일, set가동개시_종료_일] = React.useState('');
// 오늘 날짜 구하기
const today = new Date();
const todayYear = today.getFullYear().toString();
const todayMonth = (today.getMonth() + 1).toString().padStart(2, '0');
const todayDate = today.getDate().toString().padStart(2, '0');
// UiTable2에 넘길 headlist를 BASIC_TYPES 값에 따라 분기 처리
let dynamicHeadlist = swageZoneTableHeadlist;
if (basicType === 'tb_ms_swage_prcs_zone') {
dynamicHeadlist = swageZoneTableHeadlist; // 하수처리구역
} else if (basicType === 'tb_il_swage_prcs_fclt') {
dynamicHeadlist = swageFcltTableHeadlist; // 하수처리시설
}
// 선택된 시도에 따라 시군구 리스트 가져오기
const selectedSigunguList = sigunguMap[시도] || [{ id: 0, name: '선택' }];
// 조회 버튼 클릭 시 1페이지로 이동하도록 수정
const handleSearch = () => {
setCurPage(1); // 페이지를 1로 초기화
fetchFilteredData();
setShowTable(true);
};
// 선택된 필터 값을 저장할 상태 추가
const [filterParams, setFilterParams] = React.useState<FilterParams>({});
// 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);
// 구분(basicType)이 바뀔 때마다 queryParams, filterParams 초기화
React.useEffect(() => {
// filterParams 초기화
setFilterParams({});
set시설용량최소('');
set시설용량최대('');
set가동개시_시작_년('');
set가동개시_시작_월('');
set가동개시_시작_일('');
set가동개시_종료_년('');
set가동개시_종료_월('');
set가동개시_종료_일('');
set하수처리구역명('');
set하수처리시설명('');
set시도(0);
set시군구(0);
set처리구역('');
set설치년도1('');
set설치년도2('');
// queryParams는 fetchFilteredData 내에서 매번 새로 생성되므로 별도 상태 초기화는 필요 없음
}, [basicType]);
// 필터 값이 변경될 때 쿼리 파라미터 업데이트
React.useEffect(() => {
const params: FilterParams = {};
if (하수처리구역명) {
params.swtrmt_zone_nm = 하수처리구역명;
}
// 시도 선택 시 (0이 아닌 경우)
if (시도 !== 0) {
const selectedSido = sidoList.find(item => item.id === 시도);
if (selectedSido) {
params.ctpv_cd = selectedSido.id;
}
}
// 시군구 선택 시 (0이 아닌 경우)
if (시군구 !== 0) {
const selectedSigungu = (sigunguMap[시도] || []).find((item: { id: number; name: string }) => item.id === 시군구);
if (selectedSigungu) {
params.sgg_cd = selectedSigungu.id;
}
}
// 하수처리시설명이 있으면 추가
if (하수처리시설명) {
params.swtrmt_fclt_nm = 하수처리시설명;
}
setFilterParams(params);
}, [하수처리구역명, 시도, 시군구, 하수처리시설명]);
React.useEffect(() => {
if (contentRef.current) {
setDynamicHeight(contentRef.current.scrollHeight + 60); // 여유 padding
}
}, [activeTab, showTable, curPage, tableData.length]);
// 검색 API 호출 함수
const fetchFilteredData = () => {
const queryParams = new URLSearchParams({
lyrTypeNm: basicType
});
const cqlFilters: string[] = [];
// 구분별 분기 처리
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 '));
}
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 구조에 따라 조정 필요
setShowTable(true);
})
.catch(err => {
console.error('API ERROR:', err);
setTableData([]); // 에러 시 빈 배열로 초기화
});
};
const renderBody = () => (
<>
{/* 탭 영역 */}
<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',
height: '2.4rem', // 글자 크기에서 약간의 여백만 있도록 높이 조정
}}
onClick={() => setActiveTab(tab.key)}
>
{tab.label}
</button>
))}
</div>
{/* 기본정보 탭 */}
{activeTab === 'basic' && (
<div className="border rounded p-6 mb-4">
{/* 구분 */}
<div className="flex justify-between items-center mb-3 gap-4">
<div className="flex items-center">
<label className="font-semibold text-xl mr-2">구분</label>
<select
className="border rounded text-xl 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>
{/* 분기처리: 하수처리구역 */}
{basicType === 'tb_ms_swage_prcs_zone' && (
<div className="mb-3">
<div className="flex items-center mb-2">
<label className="font-semibold text-xl mr-2">시도</label>
<select className="border rounded text-xl px-2 py-1 mr-4" value={시도} onChange={e => set시도(parseInt(e.target.value))}>
{sidoList.map(opt => (
<option key={opt.id} value={opt.id}>{opt.name}</option>
))}
</select>
<label className="font-semibold text-xl mr-2">시군구</label>
<select className="border rounded text-xl px-2 py-1 mr-4" value={시군구} onChange={e => set시군구(parseInt(e.target.value))}>
{selectedSigunguList.map(opt => (
<option key={opt.id} value={opt.id}>{opt.name}</option>
))}
</select>
</div>
<div className="flex items-center">
<label className="font-semibold text-xl mr-2">하수처리구역명</label>
<input className="border rounded text-xl px-2 py-1 mr-4" value={하수처리구역명} onChange={e => set하수처리구역명(e.target.value)} />
</div>
<div className="flex items-center">
<label className="font-semibold text-xl mr-2">하수처리시설명</label>
<input className="border rounded text-xl px-2 py-1 mr-4" value={하수처리시설명} onChange={e => set하수처리시설명(e.target.value)} />
</div>
</div>
)}
{/* 분기처리: 하수처리시설 */}
{basicType === 'tb_il_swage_prcs_fclt' && (
<div className="mb-3">
{/* 가동개시일자 */}
<div className="flex items-center mb-2">
<label className="font-semibold text-xl mr-2">가동개시일자</label>
{/* 시작일자 */}
<select
className="border rounded text-xl px-2 py-1 mr-1"
value={가동개시_시작_년}
onChange={e => set가동개시_시작_년(e.target.value)}
>
<option value="">년</option>
{Array.from({ length: today.getFullYear() - 1900 + 1 }, (_, i) => {
const y = (today.getFullYear() - i).toString();
return <option key={y} value={y}>{y}</option>;
})}
</select>
{/* <select
className="border rounded text-xl px-2 py-1 mr-1"
value={가동개시_시작_월}
onChange={e => set가동개시_시작_월(e.target.value)}
>
<option value="">월</option>
{[...Array(12)].map((_, i) => {
const m = (i + 1).toString().padStart(2, '0');
return <option key={m} value={m}>{m}</option>;
})}
</select>
<select
className="border rounded text-xl px-2 py-1 mr-4"
value={가동개시_시작_일}
onChange={e => set가동개시_시작_일(e.target.value)}
>
<option value="">일</option>
{[...Array(31)].map((_, i) => {
const d = (i + 1).toString().padStart(2, '0');
return <option key={d} value={d}>{d}</option>;
})}
</select> */}
<span className="mx-1">~</span>
{/* 종료일자 */}
<select
className="border rounded text-xl px-2 py-1 mr-1"
value={가동개시_종료_년}
onChange={e => set가동개시_종료_년(e.target.value)}
>
<option value="">년</option>
{Array.from({ length: today.getFullYear() - 1900 + 1 }, (_, i) => {
const y = (today.getFullYear() - i).toString();
// 시작년이 선택된 경우, 종료년은 시작년 이상만 표시
if (가동개시_시작_년 && parseInt(y) < parseInt(가동개시_시작_년)) return null;
return <option key={y} value={y}>{y}</option>;
})}
</select>
{/* <select
className="border rounded text-xl px-2 py-1 mr-1"
value={가동개시_종료_월}
onChange={e => set가동개시_종료_월(e.target.value)}
>
<option value="">월</option>
{[...Array(12)].map((_, i) => {
const m = (i + 1).toString().padStart(2, '0');
// 시작년/월이 선택된 경우, 종료년이 시작년과 같으면 시작월 이상만 표시
if (
가동개시_시작_년 &&
가동개시_종료_년 === 가동개시_시작_년 &&
가동개시_시작_월 &&
parseInt(m) < parseInt(가동개시_시작_월)
) return null;
return <option key={m} value={m}>{m}</option>;
})}
</select>
<select
className="border rounded text-xl px-2 py-1 mr-4"
value={가동개시_종료_일}
onChange={e => set가동개시_종료_일(e.target.value)}
>
<option value="">일</option>
{[...Array(31)].map((_, i) => {
const d = (i + 1).toString().padStart(2, '0');
// 시작년/월/일이 선택된 경우, 종료년/월이 시작년/월과 같으면 시작일 이상만 표시
if (
가동개시_시작_년 &&
가동개시_종료_년 === 가동개시_시작_년 &&
가동개시_시작_월 &&
가동개시_종료_월 === 가동개시_시작_월 &&
가동개시_시작_일 &&
parseInt(d) < parseInt(가동개시_시작_일)
) return null;
// 시작년/월이 선택된 경우, 종료년/월이 시작년/월과 같고 시작일이 선택되었으면 시작일 이상만 표시
if (
가동개시_시작_년 &&
가동개시_종료_년 === 가동개시_시작_년 &&
가동개시_시작_월 &&
가동개시_종료_월 === 가동개시_시작_월 &&
가동개시_시작_일 &&
parseInt(d) < parseInt(가동개시_시작_일)
) return null;
// 시작년/월만 선택된 경우, 종료년/월이 시작년/월과 같으면 모든 일 표시(위 조건에 포함)
// 시작년만 선택된 경우, 종료년이 시작년과 같으면 모든 월/일 표시(위 조건에 포함)
return <option key={d} value={d}>{d}</option>;
})}
</select> */}
</div>
{/* 시설용량 */}
<div className="flex items-center mb-2">
<label className="font-semibold text-xl mr-2">시설용량</label>
<input
type="text"
className="border rounded text-xl px-2 py-1 mr-2 w-40 placeholder:text-center"
placeholder="최소"
value={시설용량최소}
onChange={e => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, '');
set시설용량최소(val);
}}
/>
<span className="text-xl mr-2">~</span>
<input
type="text"
className="border rounded text-xl px-2 py-1 mr-4 w-40 placeholder:text-center"
placeholder="최대"
value={시설용량최대}
onChange={e => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, '');
set시설용량최대(val);
}}
/>
</div>
{/* 하수처리시설명 */}
<div className="flex items-center">
<label className="font-semibold text-xl mr-2">하수처리시설명</label>
<input className="border rounded text-xl px-2 py-1 mr-4" value={하수처리시설명} onChange={e => set하수처리시설명(e.target.value)} />
</div>
</div>
)}
{/* 조회 버튼 중앙 정렬 */}
<div className="flex justify-center">
<Button className="w-40 text-base py-2" onClick={handleSearch}>조회</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.id} value={opt.name}>{opt.id}</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)}>
{/* {sigunguMap.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" style={{ maxWidth: '100vw', maxHeight: '60vh', overflowX: 'auto'}}>
{/* 선택된 탭의 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>
<UiTable2 headName='' tableData={tableData} headlist={dynamicHeadlist}
pageSize={tableData.length < 10 ? tableData.length : 10}
total={tableData.length} children={undefined}
cellClick={() => { }} onPageChange={setCurPage}
resetPageNo={false}
cur={curPage}
/>
</div>
</>
);
{/* </DialogContent>
</Dialog>
); */}
return (
<UiModal
open={open}
onOpenChange={onOpenChange}
bodyClass={`
${modalMaxWidth} ${modalMinWidth}
transition-all duration-300
w-full overflow-hidden
`}
body={renderBody()}
headerTitle="검색"
// 필요시 footerContent 등 추가
/>
);
}
export default React.memo(SearchModal);
@@ -0,0 +1,50 @@
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@common/components/ui";
import React from "react";
interface InputSectionProps {
title: string;
data: { year: string; type: string }[];
sectionKey: string;
onInputChange: (section: string, index: number, field: string, value: string) => void;
}
const InputSection: React.FC<InputSectionProps> = ({ title, data, sectionKey, onInputChange }) => {
return (
<div>
{/* 섹션 제목 */}
<div className="card-header">
<h3 className="text-2xl mx-3 font-medium text-label">{title}</h3>
</div>
{/* 테이블 */}
<div className="card !m-[1.2rem] !p-0r">
<Table variant="secondary" className="w-full">
<TableHeader>
<TableRow>
<TableHead className="border border-gray-300 px-2 py-2 text-sm">년도</TableHead>
<TableHead className="border border-gray-300 px-2 py-2 text-sm">구분</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, index) => (
<TableRow key={index}>
<TableCell className="border border-gray-300 px-2 py-2 text-sm">
<input
type="text"
value={row.year}
onChange={(e) => onInputChange(sectionKey, index, "year", e.target.value)}
className="w-full text-xl border-radius-0 px-3 py-2 text-base text-center leading-normal"
/>
</TableCell>
<TableCell className="border border-gray-300 px-2 py-2 text-sm text-center leading-normal">
{row.type}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
};
export default InputSection;
@@ -0,0 +1,46 @@
import smh from "../popupComponent/smh";
import Ssc from "../popupComponent/Ssc";
import SwtrmtFclt from "../popupComponent/SwtrmtFclt";
// 레이어 이름과 컴포넌트를 매핑
const layerContentMap: { [key: string]: { component?: React.FC<any> ; props?: any; } } = {
// TODO: gis데이터 추가되면 추가 작업 필요
"하수처리시설": {
component: SwtrmtFclt,
},
"하수처리구역": { },
"중점우수토실":{component: Ssc,},
"하수관로":{component: SwtrmtFclt,},
"하수맨홀":{component: smh,},
};
// 동적 컴포넌트 반환 함수
export const getLayerContentComponent = (layerNm: string): React.FC => {
const component = layerContentMap[layerNm];
if (!component || !component.component) {
// component가 없으면 null 반환
return () => null;
}
return component.component;
};
// 사용자 입력 필드 (보수 및 갱신, 청소 및 준설, 점검 및 진단)
export const handleInputChange = (section: string,
index: number,
field: string,
value: string,
dataMap: {
[key: string]: {
data: { year: string; type: string }[];
setData: React.Dispatch<React.SetStateAction<{ year: string; type: string }[]>>;
};
}
) => {
const target = dataMap[section];
if (!target) return;
const updated = [...target.data];
if (updated[index]) {
updated[index][field as keyof typeof updated[number]] = value;
}
target.setData(updated);
};
@@ -0,0 +1,51 @@
// 하수처리시설
export const SwtrmtFcltCol = [
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
{ id: 'fcltCpct', title: '시설용량' },
{ id: 'rgnExpln', title: '지역설명' },
{ id: 'cmcnYmd', title: '준공일자' },
{ id: 'oprtngStrtYmd', title: '가동개시일자' },
{ id: 'clsgYmd', title: '폐쇄일자' },
{ id: 'prcsMsrcCd', title: '처리공법코드' },
{ id: 'laeCd', title: '지자체코드' },
{ id: 'laeNm', title: '지자체명' },
{ id: 'ctpvNm', title: '시도명' },
{ id: 'sggNm', title: '시군구명' },
{ id: 'sareaCd', title: '권역코드' },
{ id: 'sareaNm', title: '권역명' },
{ id: 'lrgeSareaNm', title: '대권역명' },
{ id: 'rvrNm', title: '하천명' },
{ id: 'addr', title: '주소' },
{ id: 'daddr', title: '상세주소' },
{ id: 'rdnmAddr', title: '도로명주소' },
{ id: 'dtlRdnmAddr', title: '상세도로명주소' },
{ id: 'dschgWtstNm', title: '방류수계명' },
{ id: 'pllnArea', title: '부지면적' },
{ id: 'mngRprtvBzentyNm', title: '관리대행업제명' },
{ id: 'swtrmtPpltnCnt', title: '하수처리인구수' },
{ id: 'prcsZoneAreaSum', title: '처리구역면적합계' },
{ id: 'xcrd', title: 'X좌표' },
{ id: 'ycrd', title: 'Y좌표' },
{ id: 'oprtngSttsCd', title: '가동상태코드' },
{ id: 'rprsMsrcCn', title: '대표공법내용' },
{ id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량' },
{ id: 'sttyCrtrToc', title: '법정기준TOC' },
{ id: 'sttyCrtrSs', title: '법정기준SS' },
{ id: 'sttyCrtrTn', title: '법정기준총질소' },
{ id: 'sttyCrtrTph', title: '법정기준총인' },
{ id: 'sttyCrtrClbclCnt', title: '법정기준대장균수' },
{ id: 'bsnNm', title: '유역명' },
{ id: 'obsrvnSptCd', title: '관측지점코드' },
];
// 하수처리구역
export const SwtrmtPrcsZoneCol = [
{ id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' },
{ id: 'swtrmtZoneNm', title: '하수처리구역명' },
{ id: 'swtrmtFcltCd', title: '하수처리시설코드' },
{ id: 'ctpvCd', title: '시도코드' },
{ id: 'ctpvNm', title: '시도명' },
{ id: 'sggCd', title: '시군구코드' },
{ id: 'sggNm', title: '시군구명' },
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
]
@@ -0,0 +1,66 @@
// 우수토실
import Modal from '@/components/ui/modal';
import { VerticalTable } from '@/components/ui/verticalTable';
import { UiTable } from '@common/business_components/ui';
import { Button, TableHead, TableRow } from '@common/components/ui';
import React from 'react'
const inputColumn = [
{ id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] },
{ id: "mainSscYn", title: "중점우수토실 대상여부", type: "select", options: ['Y', 'N'] },
];
const headlist = [
{ id: 'startrain', title: '강우시작일시' }, { id: 'endrain', title: '강우종료일시' }, { id: '선행건기', title: '선행건기(h)' }, { id: '강우정보', title: '강우정보' }, { id: '누적강우량', title: '누적강우량(mm)' }, { id: '지속시간', title: '지속시간(h)' }, { id: '강우강도', title: '강우강도(mm/h)' },
]
const headlist3 = [{ id: 'nm', title: '회차' }, { id: 'date', title: '측정일자' }, { id: 'measure', title: '수량(m)' }, { id: 'bod', title: 'BOD(mg/L)' }, { id: 'toc', title: 'TOC(mg/L)' }, { id: '오염부하량', title: '오염부하량(kg)' }]
const Ssc = () => {
const [isModalOpen, setIsModalOpen] = React.useState(false);
const openModal = () => {
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
};
return (
<div><VerticalTable headName='자체 입력정보' tableData={[]} headlist={inputColumn} pageSize={0} total={0} children={undefined} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}} />
<div>
<div className="card-header">
<h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3>
</div>
<Button className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600' onClick={openModal}>상세보기</Button>
<Modal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} closeModal={closeModal}>
<div className='grid grid-col-3'><UiTable headName='강우정보' tableData={[]} headlist={headlist} pageSize={0} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}}></UiTable>
<UiTable headName='측정정보' tableData={[]} headlist={[]} pageSize={0} total={0} cellClick={undefined} onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}}> {/* 커스텀 헤더 */}
<TableRow>
<TableHead colSpan={5} className="text-center bg-blue-100 font-bold">
수량
</TableHead>
<TableHead colSpan={3} className="text-center bg-blue-100 font-bold">
수질
</TableHead>
</TableRow>
<TableRow>
<TableHead className="text-center">측정시작일시</TableHead>
<TableHead className="text-center">측정종료일시</TableHead>
<TableHead className="text-center">측정간격(분)</TableHead>
<TableHead className="text-center">측정기기설치위치</TableHead>
<TableHead className="text-center">유량계 종류 및 모델명</TableHead>
<TableHead className="text-center">채수시작일시</TableHead>
<TableHead className="text-center">채수종료일시</TableHead>
<TableHead className="text-center">채수횟수</TableHead>
</TableRow></UiTable>
<UiTable headName='수량/수질측정 정보' tableData={[]} headlist={headlist3} pageSize={0} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}}></UiTable></div>
</Modal>
</div>
</div>
)
}
export default Ssc
@@ -0,0 +1,45 @@
import { VerticalTable } from '@/components/ui/verticalTable';
import React, { useState } from 'react'
import InputSection from '../InputSection';
import { handleInputChange } from '../common';
// 하수처리시설
// TODO: 컬럼에 보고서 엉 ? 이거 버튼누르면 팝업으로 상세화면 띄워주는 건지 확인 필요
const inputColumn = [
{ id: "title", title: "설치주제", type: "select", options: ["BTL", "재정사업"] },
{ id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] },
{ id: "basu", title: "배수구역", type: "input" },
{ id: "bungu", title: "배수분구", type: "input" },
{ id: "wltjs", title: "지선/간선", type: "select", options: ["지선", "간선"] },
{ id: "flow", title: "흐름방식", type: "select", options: ["자연유하", "압송"] },
{ id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] },
];
const SwtrmtFclt = () => {
const [repairData, setRepairData] = useState([
{ year: "2025", type: "개대체" },
{ year: "2025", type: "보수" },
]);
const [cleaningData, setCleaningData] = useState([
{ year: "2025", type: "청소" },
{ year: "2025", type: "준설" },
]);
const [inspectionData, setInspectionData] = useState([
{ year: "2025", type: "관로기술진단" },
{ year: "2025", type: "관로기술진단" },
]);
const dataMap = {
repair: { data: repairData, setData: setRepairData },
cleaning: { data: cleaningData, setData: setCleaningData },
inspection: { data: inspectionData, setData: setInspectionData },
};
return (
<div>
<VerticalTable headName='자체 입력정보' tableData={[]} headlist={inputColumn} pageSize={20} total={0} children={undefined} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}></VerticalTable>
<InputSection title="보수 및 갱신" data={repairData} sectionKey="repair" onInputChange={(section, index, field, value) => handleInputChange(section, index, field, value, dataMap)} />
<InputSection title="청소 및 준설" data={cleaningData} sectionKey="cleaning" onInputChange={(section, index, field, value) => handleInputChange(section, index, field, value, dataMap)} />
<InputSection title="점검 및 진단" data={inspectionData} sectionKey="inspection" onInputChange={(section, index, field, value) => handleInputChange(section, index, field, value, dataMap)} /></div>
)
}
export default SwtrmtFclt
@@ -0,0 +1,21 @@
// 하수맨홀
import { VerticalTable } from '@/components/ui/verticalTable';
import React from 'react'
const inputColumn = [
{ id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] },
{ id: "basu", title: "배수구역", type: "input" },
{ id: "bungu", title: "배수분구", type: "input" },
{ id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] },
{id:'spec', title:'규격', type:'input'},
];
const Smh = () => {
return (
<div>
<VerticalTable headName='자체 입력정보' tableData={[]} headlist={inputColumn} pageSize={20} total={0} children={undefined} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}></VerticalTable>
</div>
)
}
export default Smh;
+50 -103
View File
@@ -1,24 +1,19 @@
'use client'
import { VerticalTable } from "@/components/verticalTable";
import { VerticalTable } from "@/components/ui/verticalTable";
import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components/ui";
import { use, useEffect, useState } from "react";
import { SwtrmtFcltCol } from "./table";
import { useEffect, useState } from "react";
import { SwtrmtFcltCol, SwtrmtPrcsZoneCol } from "./components/common/table";
import { useGisContext } from "@common/business_components/ui/map/Gis";
import { getData } from "@/components/lib/api";
const inputColumn = [
{ id: "title", title: "설치주제", type: "select", options: ["BTL", "재정사업"] },
{ id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] },
{ id: "basu", title: "배수구역", type: "input" },
{ id: "bungu", title: "배수분구", type: "input" },
{ id: "wltjs", title: "지선/간선", type: "select", options: ["지선", "간선"] },
{ id: "flow", title: "흐름방식", type: "select", options: ["자연유하", "압송"] },
{ id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] },
];
import { getLayerContentComponent } from "./components/common";
// TODO:
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; };
const featureId = featureData.getId(); // 피처의 속성 가져오기
const featureId = featureData.getId(); // 피처의 id 가져오기
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
const [tableData, setTableData] = useState<any>(null); // 피처 데이터 상태
const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태
let [buttonChk,setButtonChk] = useState(true)
// 피쳐데이터의 현재 레이어 이름 가져오는 함수
const getSelectedLayers = () => {
const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기
@@ -31,88 +26,45 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
});
return featureLayer.get('lyrNm'); // 레이어 이름 출력
};
const [repairData, setRepairData] = useState([
{ year: "2025", type: "개대체" },
{ year: "2025", type: "보수" },
]);
useEffect(() => {
const fetchData = async () => {
if (featureId) {
const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기
setLayerNm(selectedLayerName); // 상태 업데이트
const [cleaningData, setCleaningData] = useState([
{ year: "2025", type: "청소" },
{ year: "2025", type: "준설" },
]);
const [inspectionData, setInspectionData] = useState([
{ year: "2025", type: "관로기술진단" },
{ year: "2025", type: "관로기술진단" },
]);
const handleInputChange = (section: string, index: number, field: string, value: string) => {
const updateData = (data: { year: string; type: string }[], setData: React.Dispatch<React.SetStateAction<{ year: string; type: string }[]>>) => {
const updated = [...data];
if (updated[index]) {
updated[index][field as keyof typeof updated[number]] = value;
}
setData(updated);
};
if (section === "repair") {
updateData(repairData, setRepairData);
} else if (section === "cleaning") {
updateData(cleaningData, setCleaningData);
} else if (section === "inspection") {
updateData(inspectionData, setInspectionData);
const data = await getData(`v1/map/features/${featureId}`);
switch (selectedLayerName) {
case '하수처리시설':
setHeadlist(SwtrmtFcltCol);
setButtonChk(false)
break;
case '하수처리구역':
setHeadlist(SwtrmtPrcsZoneCol)
setButtonChk(false)
break;
case '중점우수토실':
setButtonChk(false)
break;
case '우수토실':
setButtonChk(true)
break;
case '하수관로':
setButtonChk(true)
break;
case '하수맨홀':
setButtonChk(true)
break;
default:
break;
}
setTableData(data);
};
}
};
async function transformFeatureDataToArray(featureId: any, keys: string[]) {
const data = await getData(`v1/map/features/${featureId}`);
// 필요한 키만 추출하여 새로운 객체 생성
console.log("data", data);
const transformedData = keys.reduce((acc, key) => {
acc[key] = data[key] !== undefined ? data[key] : "N/A"; // 값이 없으면 "N/A"로 처리
return acc;
}, {} as Record<string, any>);
// 배열로 감싸서 반환
return [transformedData];
}
const keys = SwtrmtFcltCol.map((column) => column.id);
const transformedFeatureData = transformFeatureDataToArray(featureId, keys);
useEffect(() => { setLayerNm(getSelectedLayers()) }, [featureId]);
const renderSection = (title: string, data: { year: string; type: string }[], sectionKey: string) => (
<div>
{/* 섹션 제목 */}
<div className="card-header">
<h3 className="text-2xl mx-3 font-medium text-label">{title}</h3>
</div>
{/* 테이블 */}
<div className="card !m-[1.2rem] !p-0r">
<Table variant="secondary" className="w-full">
<TableHeader>
<TableRow>
<TableHead className="border border-gray-300 px-2 py-2 text-sm">년도</TableHead>
<TableHead className="border border-gray-300 px-2 py-2 text-sm">구분</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((row, index) => (
<TableRow key={index}>
<TableCell className="border border-gray-300 px-2 py-2 text-sm">
<input
type="text"
value={row.year}
onChange={(e) => handleInputChange(sectionKey, index, "year", e.target.value)}
className="w-full text-xl border-radius-0 px-3 py-2 text-base text-center leading-normal"
/>
</TableCell>
<TableCell className="border border-gray-300 px-2 py-2 text-sm text-center leading-normal">{row.type}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
fetchData();
}, [featureId]);
const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null;
return (
<div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[1000]">
<div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[200]">
<div className="card-header bg-primary flex items-center justify-between p-4">
<div className="flex mx-3 flex-col">
<h3 className="text-2xl font-medium text-white text-label">{layerNm} 정보</h3>
@@ -127,20 +79,15 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
</button>
</div>
<div className="overflow-y-auto flex-1">
<VerticalTable headName='LX 연계정보' tableData={[]} headlist={SwtrmtFcltCol} pageSize={20} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void {
<VerticalTable headName='LX 연계정보' tableData={[tableData]} headlist={headlist} pageSize={20} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}></VerticalTable>
<VerticalTable headName='자체 입력정보' tableData={[]} headlist={inputColumn} pageSize={20} total={0} children={undefined} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}></VerticalTable>
{renderSection("보수 및 갱신", repairData, "repair")}
{renderSection("청소 및 준설", cleaningData, "cleaning")}
{renderSection("점검 및 진단", inspectionData, "inspection")}
</div>
<div className="w-full mx-5 flex items-center justify-end space-x-2">
<Button className="px-4 mr-5 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700">
<div className="w-full mx-5 flex items-center justify-end space-x-2">
{buttonChk ? <Button className="px-4 mr-5 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700">
저장
</Button>
</Button> : null}
</div>
{ContentComponent && <ContentComponent />}
</div>
</div>
)
-39
View File
@@ -1,39 +0,0 @@
export const SwtrmtFcltCol = [
{ id: 'swtrmt_fclt_nm', title: '하수처리시설명' },
{ id: 'fclt_cpct', title: '시설용량' },
{ id: 'rgn_expln', title: '지역설명' },
{ id: 'cmcn_ymd', title: '준공일자' },
{ id: 'oprtng_strt_ymd', title: '가동개시일자' },
{ id: 'clsg_ymd', title: '폐쇄일자' },
{ id: 'prcs_msrc_cd', title: '처리공법코드' },
{ id: 'lae_cd', title: '지자체코드' },
{ id: 'lae_nm', title: '지자체명' },
{ id: 'ctpv_nm', title: '시도명' },
{ id: 'sgg_nm', title: '시군구명' },
{ id: 'sarea_cd', title: '권역코드' },
{ id: 'sarea_nm', title: '권역명' },
{ id: 'lrge_sarea_nm', title: '대권역명' },
{ id: 'rvr_nm', title: '하천명' },
{ id: 'addr', title: '주소' },
{ id: 'daddr', title: '상세주소' },
{ id: 'rdnm_addr', title: '도로명주소' },
{ id: 'dtl_rdnm_addr', title: '상세도로명주소' },
{ id: 'dschg_wtst_nm', title: '방류수계명' },
{ id: 'plln_area', title: '부지면적' },
{ id: 'mng_rprtv_bzenty_nm', title: '관리대행업제명' },
{ id: 'swtrmt_ppltn_cnt', title: '하수처리인구수' },
{ id: 'prcs_zone_area_sum', title: '처리구역면적합계' },
{ id: 'geom', title: 'geometry' },
{ id: 'xcrd', title: 'X좌표' },
{ id: 'ycrd', title: 'Y좌표' },
{ id: 'oprtng_stts_cd', title: '가동상태코드' },
{ id: 'rprs_msrc_cn', title: '대표공법내용' },
{ id: 'stty_crtr_bod', title: '법정기준생물화학적산소요구량' },
{ id: 'stty_crtr_toc', title: '법정기준TOC' },
{ id: 'stty_crtr_ss', title: '법정기준SS' },
{ id: 'stty_crtr_tn', title: '법정기준총질소' },
{ id: 'stty_crtr_tph', title: '법정기준총인' },
{ id: 'stty_crtr_clbcl_cnt', title: '법정기준대장균수' },
{ id: 'bsn_nm', title: '유역명' },
{ id: 'obsrvn_spt_cd', title: '관측지점코드' },
];
@@ -10,31 +10,44 @@ import proj4 from 'proj4';
import { register } from 'ol/proj/proj4';
import GisApp from './lib/gis.core';
import GisContent from './GisContent';
import GisLonLat from './GisLonLat';
import GisToolBar from './GisToolBar';
import GisTab from './GisTab';
import MapToggleBtn from './MapToggleBtn';
import FeatureClickEventPopup from '@/map/featureClickEventPopup';
export const GisContext = createContext({gisCore: null, gisMap: null, treeData:null});
export const GisContext = createContext({gisCore: null, gisMap: null, treeData:null, checkedLayerIds:null});
export const useGisContext = () => useContext(GisContext);
function Gis({ treeData = [], isSearch = false }) {
const mapRef = useRef();
const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []);
const [gisMap, setGisMap] = useState(null);
const [mapObject, setMapObject] = useState({});
const [tabVisible, setTabVisible] = useState(true);
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 [treeData, setTreeData] = useState([]);
const checkedLayerIds = useRef([]);
let gisMap = null;
const closePopup = () => {
setIsOpen(false); // 팝업 닫기
setIsOpen(false); // 팝업 닫기
setFeatureData(null)
};
};
const handleFeatureSelect = (data) => {
console.log('check',data)
setFeatureData(data); // 선택된 피처 데이터 저장
setIsOpen(true); // 팝업 열기
};
setFeatureData(data); // 선택된 피처 데이터 저장
setIsOpen(true); // 팝업 열기
};
// SearchModal이 열릴 때 featureClickEventPopup 닫기
useEffect(() => {
if (isSearchModalOpen && isOpen) {
setIsOpen(false);
setFeatureData(null);
}
}, [isSearchModalOpen]);
//맵 생성 및 컨트롤 추가
useEffect(() => {
if (!mapRef.current) return;
@@ -46,8 +59,8 @@ function Gis({ treeData = [], isSearch = false }) {
],
]);
register(proj4);
//맵 생성
//맵 생성성
const map = new OlMap({
layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']],
renderer: 'webgl',
@@ -72,8 +85,8 @@ function Gis({ treeData = [], isSearch = false }) {
maxZoom: gisCore.defaults.maxZoom,
}),
});
//미니맵 생성
//미니맵
const overviewMap = new OverviewMap({
layers: [GisApp.Module.core.baseMiniMapLayers['base']],
view: new View({
@@ -84,33 +97,33 @@ function Gis({ treeData = [], isSearch = false }) {
collapseLabel: '\u00BB',
label: '\u00AB',
});
map.addControl(overviewMap);
gisCore.__proto__.map = map;
//스케일 생성
gisCore.scaleControl();
//배경지도 설정
gisCore.changeBaseLayer('base');
setGisMap(map);
setMapObject({ map: map });
}, []);
// 레이어 정보 설정
useEffect(() => {
//if (!locgovCd) return null;
if (!treeData || treeData.length === 0) return;
gisCore.deleteAllOverlay();
gisCore.deleteAllLayer();
const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn');
GisApp.layerCode = layerCode;
//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,
@@ -125,28 +138,33 @@ function Gis({ treeData = [], isSearch = false }) {
return layerItems;
};
//피쳐 클릭 이벤트 핸들러
const handleFeatureClick = useCallback((feature) => {
console.log('Gis.js: 피처 클릭됨:', feature.getId());
console.log('Gis.js: 피처 클릭됨:', feature.getProperties());
//우측 피쳐 상세 팝업 호출
}, []);
if (mapObject.map) {
try {
gisMap = mapObject.map;
} catch (e) {
console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
}
}
return (
<>
<GisContext.Provider value={{gisCore, gisMap, treeData}}>
<div className="h-[calc(100vh-10rem)] flex relative">
{isOpen && featureData && (
<FeatureClickEventPopup
featureData={featureData}
onClose={closePopup} // 닫기 콜백 전달
/>
)}
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
<GisContent onFeatureSelect={handleFeatureSelect} />
<GisTab visible={tabVisible} setVisible={setTabVisible} isSearch={isSearch} />
</div>
</GisContext.Provider>
<GisContext.Provider value={{gisCore, gisMap, treeData, checkedLayerIds}}>
<div className="h-[calc(100vh-10rem)] flex relative">
<GisTab visible={tabVisible} setVisible={setTabVisible} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} />
<GisContent onFeatureSelect={handleFeatureSelect}/>
{isOpen && featureData && (
<FeatureClickEventPopup
featureData={featureData}
onClose={closePopup} // 닫기 콜백 전달
/>
)}
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
{/* <GisLonLat/>
<GisToolBar/>
<MapToggleBtn/> */}
</div>
</GisContext.Provider>
</>
);
}
@@ -3,12 +3,11 @@ import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs';
import GisTree from './GisTree';
import GisSearch from './GisSearch';
import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
import SearchModal from './SearchModal';
import SearchModal from '@/map/SearchModal';
function GisTab({ visible, setVisible, isSearch }) {
function GisTab({ visible, setVisible, isSearch, isSearchModalOpen, setIsSearchModalOpen }) {
const [activeTab, setActiveTab] = useState('layers');
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
return (
<>
@@ -1,377 +0,0 @@
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,111 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import { useEffect, useState } from "react";
import UiPageNations from "./uiPageNations";
type UiTableProps = {
headName: React.ReactNode | undefined;
tableData: { [key: string]: string | number | boolean | object }[];
headlist: UiHeadProps[];
pageSize: number;
total: number;
children: React.ReactNode | undefined;
cellClick: ((index: number | undefined, headId: string) => void) | undefined;
onPageChange: (page: number) => void;
className?: string;
resetPageNo?: boolean;
cur?: number;
}
type UiHeadProps = {
id: string;
title: string;
align?: string;
}
export function UiTable2({ tableData, headlist, pageSize, total, children, headName, cellClick, onPageChange, className = '', resetPageNo = false, cur }: UiTableProps) {
const [idx, setIdx] = useState<number>(-1);
const [curState, setCurState] = useState<number>(cur ?? 1);
useEffect(() => {
setCurState(cur ?? 1);
}, [cur]);
useEffect(() => {
if (resetPageNo) {
setCurState(1);
onPageChange(1);
}
}, [onPageChange, resetPageNo]);
const handlePageChange = (page: number) => {
setCurState(page);
onPageChange(page);
}
const handleCellClick = (index: number, headId: string) => {
if (cellClick) {
cellClick(index, headId);
setIdx(index);
}
};
const startIdx = (curState - 1) * pageSize;
const endIdx = startIdx + pageSize;
const pageData = tableData.slice(startIdx, endIdx);
return (
<>
{headName ? (
<div className="card-header">
<h3 className="text-2xl font-medium text-label">{headName}</h3>
</div>
) : null}
<div className={`card !m-[1.2rem] !p-0r w-full relative ${className}`}>
<div>
<Table variant="secondary" className="w-full">
{/* 스크롤 관련 div 제거 */}
<TableHeader className="bg-white">
{children ? (
<>{children}</>
) : (
<TableRow>
{headlist?.map((head) => (
<TableHead key={head.id}
className="min-w-[120px] max-w-[180px] w-[150px]
text-center whitespace-nowrap overflow-hidden
text-ellipsis bg-white">
{head.title}
</TableHead>
))}
</TableRow>
)}
</TableHeader>
<TableBody className='text-center'>
{pageData.map((el: { [key: string]: string | number | boolean | object }, index) => (
<TableRow key={index + "row"}>
{headlist?.map((head) => (
<TableCell
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id] === 'number' ? 'text-right' : 'text-center'} ${cellClick && idx === index ? '!font-[1000] cursor-pointer' :
cellClick ? 'cursor-pointer' : ''
}`}
onClick={() => handleCellClick(startIdx + index, head.id)}
key={head.id + index}
>
{typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* 페이지네이션을 sticky 하단에 고정 */}
{total > 0 && (
<div className="sticky left-0 right-0 bottom-0 bg-white z-20 border-t border-gray-200">
<UiPageNations total={total} cur={curState} pageSize={pageSize} onPageChange={handlePageChange} />
</div>
)}
</div>
</>
);
}
@@ -30,5 +30,5 @@
"**/*.mjs",
"**/*.jsx",
"**/*.json"
]
, "../../../apps/sn/app/map/SearchModal.tsx" ]
}