Files
frontend/apps/sn/app/ii/BaseLayout.tsx
T
swcho 39628cd12f feat:
- 분석 대상지역 선택 기능 구현 (API 연동)
- SearchModal.tsx 주석 삭제
2025-06-02 16:09:19 +09:00

225 lines
10 KiB
TypeScript

'use client'
import { getData } from '@/components/lib/api'
import ContentLayout from '@/components/ui/contentLayout'
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 { useEffect, useState, type ReactNode } from 'react'
type TitleName = {
mainTitle: string
firstCardTitle: string
secondCardTitle?: string
}
type SelectBox = {
labelNm?: Label
searchChk?: boolean
title?: string // SearchDiv의 제목 (선택 사항)
}
type Label = {
first: string
second: string
third: string
fourth: string
}
// API로부터 받아올 데이터 타입을 정의 (SelectBox에 사용될 형태)
interface ApiSelectOption {
text: string;
val: string;
}
type BaseLayout = {
children?: ReactNode[]
titleName: TitleName
form: any
onSubmitEvent: Function
graphBtn?: boolean
firstDivBtn?: boolean
searchBox?: SelectBox
enoCd?: string | number
setEnoCd?: (enoCd: string) => void
ctpvCd?: string | number
setCtpvCd?: (ctpvCd: string) => void
sigungu?: ApiSelectOption
setSigungu?: (sigungu: ApiSelectOption) => void
selectedSwageTreatmentPlant?: string | number;
onSwageTreatmentPlantChange?: (value: string | number) => void;
selectedSwageProcessingZone?: string | number;
onSwageProcessingZoneChange?: (value: string | number) => void;
}
// TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ?
/**
*
* @param children
* @param titleName : 메인 타이틀, 첫번째 요소 타이틀, 두번째 요소 타이틀
* @param form : zod 스키마
* @param onSubmitEvent : 폼 이벤트 처리
* @param graphBtn : 하단에 그래프 버튼 유무 처리
* @returns
*/
const BaseLayout = ({
children,
titleName,
form,
onSubmitEvent,
graphBtn = false,
searchBox,
enoCd,
setEnoCd,
ctpvCd,
setCtpvCd,
sigungu,
setSigungu,
selectedSwageTreatmentPlant,
onSwageTreatmentPlantChange,
selectedSwageProcessingZone,
onSwageProcessingZoneChange,
}: BaseLayout) => {
const defaultOption: ApiSelectOption = { val: '0', text: '선택' };
const [processingZoneOptions, setProcessingZoneOptions] = useState<ApiSelectOption[]>([]);
const [treatmentFacilityOptions, setTreatmentFacilityOptions] = useState<ApiSelectOption[]>([]);
const [isLoadingAreaData, setIsLoadingAreaData] = useState<boolean>(false);
useEffect(() => {
const fetchData = async () => {
if (!sigungu || sigungu.val === '0' || !sigungu.val) {
console.log("BaseLayout: Skipping fetchData because sigungu.val is:", sigungu?.val);
setProcessingZoneOptions([defaultOption]);
setTreatmentFacilityOptions([defaultOption]);
setIsLoadingAreaData(false);
return;
}
setIsLoadingAreaData(true);
try {
const queryParamsZone = new URLSearchParams({
lyrTypeNm: 'tb_ms_swage_prcs_zone',
page: '0',
size: '100000',
sort: 'string',
});
const cqlFiltersZone: string[] = [];
if (sigungu.val && sigungu.val !== '0') {
cqlFiltersZone.push(`sgg_cd='${sigungu.val}'`);
}
if (cqlFiltersZone.length > 0) {
queryParamsZone.append('cqlFiter', cqlFiltersZone.join(' and '));
}
const queryParamsFacility = new URLSearchParams({
lyrTypeNm: 'tb_il_swage_prcs_fclt',
page: '0',
size: '100000', // 요청하신 URL에 맞게 50으로 변경 가능
sort: 'string',
});
const cqlFiltersFacility: string[] = [];
if (sigungu.text && sigungu.val !== '0') {
cqlFiltersFacility.push(`sgg_nm ILIKE '%${sigungu.text}%'`);
}
if (cqlFiltersFacility.length > 0) {
queryParamsFacility.append('cqlFiter', cqlFiltersFacility.join(' and ')); // API 명세에 따라 cqlFilter 또는 cqlFiter
}
console.log("BaseLayout API URL for Zones:", `v1/fclts?${queryParamsZone.toString()}`);
console.log("BaseLayout API URL for Facilities:", `v1/fclts?${queryParamsFacility.toString()}`);
const [zoneRes, facilityRes] = await Promise.all([
getData(`v1/fclts?${queryParamsZone.toString()}`),
getData(`v1/fclts?${queryParamsFacility.toString()}`)
]);
const zones = (zoneRes.data || []).map((item: any) => ({
text: item.swtrmtZoneNm || item.name || `구역 ${item.id || item.gid || item.objectid}`,
val: item.fid || item.id || item.gid || item.objectid
}));
setProcessingZoneOptions([defaultOption, ...zones]);
console.log("BaseLayout Fetched Processing Zones:", [defaultOption, ...zones]);
const facilities = (facilityRes.data || []).map((item: any) => ({
text: item.swtrmtFcltNm || item.name || `시설 ${item.id || item.gid || item.objectid}`,
val: item.fid || item.id || item.gid || item.objectid
}));
setTreatmentFacilityOptions([defaultOption, ...facilities]);
console.log("BaseLayout Fetched Treatment Facilities:", [defaultOption, ...facilities]);
} catch (error) {
console.error("BaseLayout: 지역/시설 데이터 조회 중 오류 발생:", error);
setProcessingZoneOptions([defaultOption]);
setTreatmentFacilityOptions([defaultOption]);
} finally {
setIsLoadingAreaData(false);
}
};
fetchData();
}, [sigungu]); // sigungu 상태가 변경될 때마다 실행
return (
<ContentLayout title={titleName.mainTitle}>
<ListDiv>
{/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */}
{searchBox?.title && (
<div className="card-header">
<h3 className="text-2xl font-medium">{searchBox.title}</h3>
<div>
{graphBtn ? <Button className="!mr-[1.2rem]">다운로드</Button> : null}
{/* 그래프 있을 시 */}
{graphBtn ? <Button className="!mr-[1.2rem]">삭제</Button> : null}
</div>
</div>
)}
<div className="flex items-center w-full">
<GovSidoSigunguSearchForm
isShowPart={true}
setRtnEnvPartValue={(val) => setEnoCd?.(val)}
setRtnEnvPartSidoValue={(val) => setCtpvCd?.(val)}
setRtnEnvPartSigunValue={setSigungu} // 선택된 시군구 값을 받을 콜백 함수 전달
>
<SelectBox2 label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} />
<SelectBox2 label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} />
</GovSidoSigunguSearchForm>
{searchBox?.searchChk ? <SearchFormRight><Button>조회</Button></SearchFormRight> : null}
</div>
</ListDiv>
<ListDiv>
<div className="card-header">
<h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3>
<div>
{graphBtn ? <Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size='sm'>다운로드</Button> : null}
{/* 그래프 있을 시 */}
{graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size='sm'>삭제</Button> : null}
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmitEvent)}>
<div className="card !m-[1.2rem]">
{/* 폼 정보 입력 칸 */}
{children?.[0]}
</div>
</form>
</Form>
</ListDiv>
{
children?.[1] != null ? <ListDiv>
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl font-medium">{titleName?.secondCardTitle}</h3>
<div>
{graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size='sm'>다운로드</Button> : null}
{/* 그래프 있을 시 */}
{graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size='sm'>삭제</Button> : null}
</div>
</div>
<div className="card !m-[1.2rem]">{children?.[1]}</div>
</ListDiv> : null
}
</ContentLayout >
)
}
export default BaseLayout