From 1d711704785f5d95faf125b472249d4d09284afa Mon Sep 17 00:00:00 2001 From: swcho Date: Fri, 30 May 2025 18:06:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20-=20=ED=99=98=EA=B2=BD=EC=B2=AD,=20?= =?UTF-8?q?=EC=8B=9C=EB=8F=84/=EC=8B=9C=EA=B5=B0=EA=B5=AC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(govSidoSigunguSearchForm.tsx)=20-=20API=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B2=98=EB=A6=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/components/lib/api/index.ts | 14 +- .../components/ui/SidoSigunguSelectBox.tsx | 4 +- .../ui/govSidoSigunguSearchForm.tsx | 331 ++++++++++++++++++ apps/sn/app/ii/BaseLayout.tsx | 45 ++- apps/sn/app/ii/irsetting/page.tsx | 33 +- apps/sn/app/ii/rdii/page.tsx | 7 +- 6 files changed, 392 insertions(+), 42 deletions(-) create mode 100644 apps/sn/app/components/ui/govSidoSigunguSearchForm.tsx diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index 0045047e1..649223de3 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -10,11 +10,13 @@ export const postData = async (url: string, data: object): Promise => { }; const res = await fetch(`${BASE_URL}/${url}`, options); if (res.ok) { - const text = await res.text(); - if (text) { - return JSON.parse(text); // JSON 데이터가 있으면 파싱하여 반환 - } - return true; // 응답 본문이 비어 있으면 true 반환 + const resData = await res.json(); + return resData; + // const text = await res.text(); + // if (text) { + // return JSON.parse(text); // JSON 데이터가 있으면 파싱하여 반환 + // } + // return true; // 응답 본문이 비어 있으면 true 반환 } }; @@ -61,6 +63,6 @@ export const deleteData = async (url: string, data?: object): Promise => { body: data ? JSON.stringify(data) : undefined, }; const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, options); - if(res.status === 204) return true + if (res.status === 204) return true return res; }; \ No newline at end of file diff --git a/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx b/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx index 18bc22337..d7fe3d793 100644 --- a/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx +++ b/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx @@ -137,7 +137,7 @@ export function SidoSigunguSelectBox({ const sigunguList = useMemo(() => sigunguMap[sido] || [{ id: 0, name: '전체' }], [sido]); return ( -
+
onSidoChange(Number(val))} /> ({ val: opt.id, text: opt.name }))} diff --git a/apps/sn/app/components/ui/govSidoSigunguSearchForm.tsx b/apps/sn/app/components/ui/govSidoSigunguSearchForm.tsx new file mode 100644 index 000000000..3cfb8725c --- /dev/null +++ b/apps/sn/app/components/ui/govSidoSigunguSearchForm.tsx @@ -0,0 +1,331 @@ +import React, { useEffect, useState } from 'react'; +import { SelectBox } from '@common/business_components/ui/select'; + +export type comCodeState = { + code: { + isInit: boolean; + chBuCd: SelectDataType[] + rainFcltCd: SelectDataType[]; + bdstUsgCd: SelectDataType[]; + cityCd: SelectDataType[]; + sggCd: SelectDataType[]; + rvbsnCd: SelectDataType[]; + mdmbsnCd: SelectDataType[]; + lgbsnCd: SelectDataType[]; + lclgvrnmntCd: EnvPartSelectDataType[]; + orgwtKndCd: SelectDataType[]; + yn: SelectDataType[]; + buildingType: { + RST: string, + RNB: string, + HTB: string + } + }; +}; + +type SelectDataType = { + text: string; + val: string; + sido?: string; // 시도 코드 (선택 사항) +} + +type EnvPartSelectDataType = SelectDataType & { + adstDliStrCd: string, +} + +type SearchFormLeftProps = { + isShowPart?: boolean | undefined; + children: React.ReactNode; + selectSidoData?: SelectDataType[] | []; + selectSigunData?: SelectDataType[] | []; + selectRegionData?: SelectDataType[] | []; + selectUnitWaterShedData?: SelectDataType[] | []; + selectMiddleRegionData?: SelectDataType[] | []; + selectEnvPartData?: EnvPartSelectDataType[] | []; + selectEnvPartSidoData?: SelectDataType[] | []; + selectEnvPartSigunData?: SelectDataType[] | []; + + setRtnPartValue?: (value: string) => void; + setRtnSidoValue?: (value: string) => void; + setRtnSigunValue?: (value: string) => void; + setRtnRegionValue?: (value: string) => void; + setRtnUnitWaterShedValue?: (value: string) => void; + setRtnMiddleRegionValue?: (value: string) => void; + setRtnEnvPartValue?: (value: string) => void; + setRtnEnvPartSidoValue?: (value: string) => void; + setRtnEnvPartSigunValue?: (value: string) => void; + resetTrigger?: boolean; +} + + + +function GovSidoSigunguSearchForm({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSigunValue, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false }: SearchFormLeftProps) { + //환경청별 시도 시군구 + const [envPartValue, setEnvPartValue] = useState('0'); + const [envPartSidoValue, setEnvPartSidoValue] = useState('0'); // 환경청 선택 시 시/도 값 + const [envPartSigunValue, setEnvPartSigunValue] = useState('0'); // 환경청 선택 시 시/군/구 값 + + const [filteredEnvPartSigunDataSelected, setFilteredEnvPartSigunDataSelected] = useState([]); + + // API로부터 가져온 전체 데이터 목록 (필터링 전) + const [allEnvPartOptions, setAllEnvPartOptions] = useState([]); + const [allSidoOptions, setAllSidoOptions] = useState([]); + const [allSigunOptions, setAllSigunOptions] = useState([]); + + // 조회된 조건에 맞는 환경청 시도 시군 (SelectBox에 실제 사용될 데이터) + const [filteredEnvPartData, setFilteredEnvPartData] = useState([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }]); + const [filteredEnvPartSidoData, setFilteredEnvPartSidoData] = useState([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }]); + const [filteredEnvPartSigunData, setFilteredEnvPartSigunData] = useState([{ text: "전체", val: '0', sido: '0' }]); + + // 시군구 데이터 + const getSggCd = async (): Promise => { + try { + const response = await fetch('http://con.mind-one.co.kr:11082/api/v1/addr/sgg', { method: "GET", headers: { accept: "application/json" } }); + if (!response.ok) throw new Error(`시군구 데이터 오류: ${response.status}`); + const res = await response.json(); + const sgg = res.data; + const sggCd = sgg.map((item: any) => ({ + val: item.adstDstrCd, + sido: item.sido, + text: item.gugun, + })); + sggCd.unshift({ val: '0', text: '전체' }); // "전체" 옵션 추가 + return sggCd; + } catch (error) { + console.error('시군구 데이터를 가져오는데 실패했습니다:', error); + return [{ val: '0', text: '전체', sido: '0' }]; + } + }; + + // 시도 데이터 + const getCityCd = async (): Promise => { + try { + const response = await fetch('http://con.mind-one.co.kr:11082/api/v1/addr/city', { method: "GET", headers: { accept: "application/json" } }); + if (!response.ok) throw new Error(`시도 데이터 오류: ${response.status}`); + const res = await response.json(); + const city = res.data; + const cityCd = city.map((item: any) => ({ + val: item.adstDstrCd, + text: item.sido, + })); + cityCd.unshift({ val: '0', text: '전체' }); + return cityCd; + } catch (error) { + console.error('시도 데이터를 가져오는데 실패했습니다:', error); + return [{ val: '0', text: '전체', sido: '0' }]; + } + }; + + // 환경청 데이터 + const getEnvPartCd = async (): Promise => { + try { + const response = await fetch('http://con.mind-one.co.kr:11082/api/v1/addr/govArea', { method: "GET", headers: { accept: "application/json" } }); + if (!response.ok) throw new Error(`환경청 데이터 오류: ${response.status}`); + const res = await response.json(); + const lclgvrnmnt = res.data.filter((item: { lclgvrnmntNm: string; }) => { + // item.lclgvrnmntNm이 null이나 undefined가 아닌지 확인 후 endsWith 사용 + return item.lclgvrnmntNm && item.lclgvrnmntNm.endsWith("환경청"); + }); + const lclgvrnmntCd = lclgvrnmnt.map((item: any) => ({ + val: item.lclgvrnmntCd, + text: item.lclgvrnmntNm, + adstDliStrCd: item.adstDliStrCd || '' + })); + lclgvrnmntCd.unshift({ val: '0', text: '전체', adstDliStrCd: '' }); + return lclgvrnmntCd; + } catch (error) { + console.error('환경청 데이터를 가져오는데 실패했습니다:', error); + return [{ val: '0', text: '전체', adstDliStrCd: '', sido: '0' }]; + } + }; + + useEffect(() => { + const loadInitialData = async () => { + const [envParts, sidos, siguns] = await Promise.all([ + getEnvPartCd(), + getCityCd(), + getSggCd() + ]); + + setAllEnvPartOptions(envParts.filter(item => item.val !== '0')); + setFilteredEnvPartData(envParts); + + setAllSidoOptions(sidos.filter(item => item.val !== '0')); + // 초기 filteredEnvPartSidoData는 모든 시/도를 포함 (환경청 "전체" 선택 시) + setFilteredEnvPartSidoData(sidos.map(s => ({ ...s, adstDliStrCd: '' }))); + + + setAllSigunOptions(siguns.filter(item => item.val !== '0')); + // 초기 filteredEnvPartSigunData는 모든 시/군/구를 포함 (환경청 "전체", 시/도 "전체" 선택 시) + setFilteredEnvPartSigunData(siguns); + + // 만약 isShowPart가 true이고, partValue가 'G'(환경청별)로 기본 설정되어 있다면, + // 초기 envPartValue ('0')에 대한 시도/시군구 목록을 설정합니다. + initializeCityDataByEnvPartSido(envPartValue); // envPartValue 기본값 '0' + }; + + if (isShowPart) { + loadInitialData(); + } + }, [isShowPart]); // isShowPart가 변경될 때 데이터를 다시 로드 + + // 시군구 세팅 (행정구역별) + const initializeSigunData = (sidoCd: string) => { + if (sidoCd === '0') { + // setSigunValue('0'); + setFilteredEnvPartSigunData([{ text: "전체", val: '0' }, ...allSigunOptions]); // 전체 시군구 + } else { + const newFilteredData = [ + { text: "전체", val: '0' }, + ...allSigunOptions.filter((item: SelectDataType) => item.val.substring(0, 2) === sidoCd) + ]; + // setSigunValue('0'); + setFilteredEnvPartSigunData(newFilteredData); + } + } + + // 환경청 세팅 - 시도 + const initializeCityDataByEnvPartSido = (envPartCd: string) => { + const foundEnvPart = allEnvPartOptions.find((item: EnvPartSelectDataType) => item.val === envPartCd); + + let relevantSidos: SelectDataType[] = []; // 기본값을 빈 배열로 설정 + let relevantSigunsForSidoDerivation: SelectDataType[] = []; // 기본값을 빈 배열로 설정 + + if (envPartCd === '0') { // "전체" 환경청 선택 시 + relevantSidos = [...allSidoOptions]; + relevantSigunsForSidoDerivation = [...allSigunOptions]; + } else if (foundEnvPart && foundEnvPart.adstDliStrCd) { // 특정 환경청 선택 & 해당 환경청에 adstDliStrCd 정보가 있는 경우 + // adstDliStrCd가 시도 코드를 포함한다고 가정 (주석 참고) + const cityCodesInEnvPart = foundEnvPart.adstDliStrCd.toString().split('|').join('').split(',').filter(c => c); + console.log('cityCodesInEnvPart:', cityCodesInEnvPart); + // 1. allSigunOptions에서 val이 cityCodesInEnvPart에 포함된 데이터의 sido만 추출 + const relevantSigunSidos = Array.from( + new Set( + allSigunOptions + .filter(sigungu => cityCodesInEnvPart.includes(sigungu.val)) + .map(sigungu => sigungu.sido) + ) + ); + console.log('relevantSigunTexts:', relevantSigunSidos); + + // 2. relevantSigunsForSidoDerivation은 해당 sido만을 가진 배열로 반환 + relevantSidos = allSidoOptions.filter(sido => relevantSigunSidos.includes(sido.text)); + console.log('relevantSidos:', relevantSidos); + + // 해당 시도들에 속하는 시군구들 (가나다순 오름차순 정렬) + relevantSigunsForSidoDerivation = allSigunOptions + .filter(sigun => { + const sigunSidoPrefix = sigun.val.substring(0, 2); + return relevantSidos.some(s => s.val === sigunSidoPrefix); + }) + .sort((a, b) => a.text.localeCompare(b.text, 'ko')); + } + // 특정 환경청이 선택되었으나, foundEnvPart가 없거나 adstDliStrCd 정보가 없는 경우, + // relevantSidos와 relevantSigunsForSidoDerivation은 초기 빈 배열 상태를 유지합니다. + + setFilteredEnvPartSidoData([ + { text: "전체", val: '0', adstDliStrCd: '', sido: '0' }, + ...relevantSidos.map((item: SelectDataType) => ({ text: item.text, val: item.val, adstDliStrCd: '', sido: '0' })) + ]); + + setFilteredEnvPartSigunDataSelected(relevantSigunsForSidoDerivation); + + // 시/도 선택이 "전체"('0')로 초기화되므로, 해당 시/군/구 목록도 업데이트 + initializeCityDataByEnvPartSigun('0'); + } + + // 환경청 세팅 - 시군구 + const initializeCityDataByEnvPartSigun = (selectedSidoInEnvPart: string) => { + let sigunsToList: SelectDataType[]; + if (selectedSidoInEnvPart === '0') { // 선택된 시도가 "전체"이면 + // filteredEnvPartSigunDataSelected는 이미 해당 환경청에 맞는 시군구들로 필터링 되어있음 + sigunsToList = [...filteredEnvPartSigunDataSelected]; + } else { + sigunsToList = filteredEnvPartSigunDataSelected.filter((item: SelectDataType) => item.val.substring(0, 2) === selectedSidoInEnvPart); + } + setFilteredEnvPartSigunData([ + { text: "전체", val: '0' }, + ...sigunsToList + ]); + } + + const onSelectValue = (val: string, type: string) => { + switch (type) { + // 환경청 세팅 + case 'envPart': + setEnvPartValue(val); + setRtnEnvPartValue?.(val); + initializeCityDataByEnvPartSido(val); + setEnvPartSidoValue('0'); // 시도 "전체"로 초기화 + setEnvPartSigunValue('0'); // 시군구 "전체"로 초기화 + // onSelectValue(filteredEnvPartSidoData[0]?.val || '0', 'envPartSido'); // 이 부분은 setEnvPartSidoValue('0')와 initializeCityDataByEnvPartSigun('0')으로 대체됨 + break; + case 'envPartSido': // 환경청별 - 시도 선택 + setEnvPartSidoValue(val); + setRtnEnvPartSidoValue?.(val); + initializeCityDataByEnvPartSigun(val); + setEnvPartSigunValue('0'); // 시군구 "전체"로 초기화 + // onSelectValue(filteredEnvPartSigunData[0]?.val || '0', 'envPartSigun'); + break; + case 'envPartSigun': // 환경청별 - 시군구 선택 + setEnvPartSigunValue(val); + setRtnEnvPartSigunValue?.(val); + break; + } + } + + // 리셋 함수 + const resetAllValues = () => { + + setEnvPartValue('0'); + setEnvPartSidoValue('0'); + setEnvPartSigunValue('0'); + + // 필터링된 목록들도 초기 "전체" 또는 전체 목록으로 리셋 + setFilteredEnvPartSigunData([{ text: "전체", val: '0', sido: '0' }, ...allSigunOptions]); + + setFilteredEnvPartData([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }, ...allEnvPartOptions]); + setFilteredEnvPartSidoData([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }, ...allSidoOptions.map(s => ({ ...s, adstDliStrCd: '' }))]); + setFilteredEnvPartSigunData([{ text: "전체", val: '0', sido: '0' }, ...allSigunOptions]); + setFilteredEnvPartSigunDataSelected(allSigunOptions); + + + setRtnPartValue?.('A'); + setRtnSidoValue?.('0'); + setRtnSigunValue?.('0'); + setRtnRegionValue?.('0'); + setRtnUnitWaterShedValue?.('0'); + setRtnMiddleRegionValue?.('0'); + setRtnEnvPartValue?.('0'); + setRtnEnvPartSidoValue?.('0'); + setRtnEnvPartSigunValue?.('0'); + }; + + // 리셋 트리거 변경 감지 + useEffect(() => { + if (resetTrigger) { + resetAllValues(); + // 리셋 후 트리거를 false로 설정하는 로직은 부모 컴포넌트에서 처리해야 할 수 있습니다. + // 여기서는 resetTrigger props가 true일 때 리셋을 수행합니다. + } + }, [resetTrigger]); + + return ( + isShowPart ? ( +
+ <> + onSelectValue(val.toString(), 'envPart')} value={envPartValue} selectClass={'rounded-lg bg-white truncate'} /> + onSelectValue(val.toString(), 'envPartSido')} value={envPartSidoValue} selectClass={'rounded-lg bg-white truncate'} /> + onSelectValue(val.toString(), 'envPartSigun')} value={envPartSigunValue} selectClass={'rounded-lg bg-white truncate'} /> + + {children} +
+ ) : ( +
+ {children} +
+ ) + ) +} + +export { GovSidoSigunguSearchForm }; \ No newline at end of file diff --git a/apps/sn/app/ii/BaseLayout.tsx b/apps/sn/app/ii/BaseLayout.tsx index 129063ba7..ac8a10d15 100644 --- a/apps/sn/app/ii/BaseLayout.tsx +++ b/apps/sn/app/ii/BaseLayout.tsx @@ -1,10 +1,12 @@ 'use client' import ContentLayout from '@/components/ui/contentLayout' -import { SearchDiv, SearchFormLeft, SelectBox, SearchFormRight, Form, UiTable } from '@common/business_components/ui' -import ListDiv from '@common/business_components/ui/list_div' -import { Button } from '@common/components/ui' -import React, { type ReactNode } from 'react' +import { GovSidoSigunguSearchForm } from '@/components/ui/govSidoSigunguSearchForm' +import { Form, SearchFormRight, SelectBox } from '@common/business_components/ui' +import { SidoSigunguSelectBox } from '../components/ui/SidoSigunguSelectBox' import { SelectBox2 } from '@common/business_components/ui/select/index-2' +import ListDiv from '@common/business_components/ui/list_div' +import { Button, Label } from '@common/components/ui' +import { type ReactNode } from 'react' type TitleName = { mainTitle: string @@ -30,6 +32,10 @@ type BaseLayout = { graphBtn?: boolean firstDivBtn?: boolean searchBox?: SelectBox + sido?: number + setSido?: (sido: number) => void + sigungu?: number + setSigungu?: (sigungu: number) => void } // TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ? /** @@ -41,7 +47,7 @@ type BaseLayout = { * @param graphBtn : 하단에 그래프 버튼 유무 처리 * @returns */ -const BaseLayout = ({ children, titleName, form, onSubmitEvent, graphBtn = false, searchBox }: BaseLayout) => { +const BaseLayout = ({ children, titleName, form, onSubmitEvent, graphBtn = false, searchBox, sido, setSido, sigungu, setSigungu }: BaseLayout) => { return ( @@ -57,14 +63,27 @@ const BaseLayout = ({ children, titleName, form, onSubmitEvent, graphBtn = false
)}
-
- - { }} selectClass={undefined} /> - { }} selectClass='' /> - { }} selectClass='' /> - { }} selectClass='' /> - -
+ {/*
+
*/} + {/* { }} /> + { + setSido?.(sido); + setSigungu?.(sigungu ?? 0); + }} + onSigunguChange={setSigungu ?? (() => { })} + /> + { }} /> + { }} /> */} + + { }} /> + { }} /> + + {/*
+
*/} {searchBox?.searchChk ? : null}
diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/ii/irsetting/page.tsx index 6c3952d90..c21610f68 100644 --- a/apps/sn/app/ii/irsetting/page.tsx +++ b/apps/sn/app/ii/irsetting/page.tsx @@ -62,11 +62,11 @@ const IrSetting = () => { const [selectedDate, setSelectedDate] = useState(new Date()); const [input1, setInput1] = useState(''); const [input2, setInput2] = useState(''); - const [select1, setSelect1] = useState(''); - const [select2, setSelect2] = useState(''); - const [select3, setSelect3] = useState(''); - const [select4, setSelect4] = useState(''); - const [select5, setSelect5] = useState(''); + const [select1, setSelect1] = useState(0); + const [select2, setSelect2] = useState(0); + const [select3, setSelect3] = useState(0); + const [select4, setSelect4] = useState(0); + const [select5, setSelect5] = useState(0); const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 // 폼 초기화 함수 @@ -89,11 +89,11 @@ const IrSetting = () => { setSelectedDate(new Date()); // 날짜 선택 초기화 setInput1(''); // 입력 필드 초기화 setInput2(''); // 입력 필드 초기화 - setSelect1(''); // 선택 필드 초기화 - setSelect2(''); - setSelect3(''); - setSelect4(''); - setSelect5(''); + setSelect1(0); // 선택 필드 초기화 + setSelect2(0); + setSelect3(0); + setSelect4(0); + setSelect5(0); setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경 }; @@ -122,13 +122,10 @@ const IrSetting = () => { // 추후 API 엔드포인트에 맞게 URL 수정 필요 const data = await postData('v1/inftwtAnls', analysisData); console.log("Response from backend:", data); - if (data.ok) { + if (data) { alert("분석 데이터가 성공적으로 제출되었습니다."); resetAllForms(); // 성공 시 폼 초기화 함수 호출 - } else { - alert("분석 데이터 제출 중 오류가 발생했습니다."); - // resetAllForms(); // 실패 시 폼 초기화 함수 호출 - } + } else alert("분석 데이터 제출 중 오류가 발생했습니다."); }; // 침입수 분석 실행 버튼 클릭 핸들러 @@ -232,7 +229,11 @@ const IrSetting = () => { form={form} onSubmitEvent={() => { }} graphBtn={false} - searchBox={{ title: '분석 대상지역 선택' }} > + searchBox={{ title: '분석 대상지역 선택' }} + sido={select2} + setSido={setSelect2} + sigungu={select3} + setSigungu={setSelect3}>