소스 구조 수정 및 메뉴 적용

This commit is contained in:
yschoi
2025-06-18 17:56:20 +09:00
parent 1c507887f8
commit deea89fbb2
85 changed files with 1318 additions and 252 deletions
@@ -0,0 +1,274 @@
'use client'
import { getData } from '@/components/lib/api'
import ContentLayout from '@/components/ui/contentLayout'
import { GovSidoSigunguSearchForm } from '@/components/ui/govSidoSigunguSearchForm'
import { DatePicker, Form, SearchFormRight, SelectBox } from '@common/business_components/ui'
import ListDiv from '@common/business_components/ui/list_div'
import { SelectBox2 } from '@common/business_components/ui/select/index-2'
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;
onSearchClick?: () => Promise<void>;
}
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
analysisBtn?: string
firstDivBtn?: boolean
selectBox?: SelectBox
showDatePicker?: boolean;
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;
aplcnYm?: Date
setAplcnYm?: (aplcnYm: Date | undefined) => void;
isLoading?: boolean;
// Excel 다운로드 관련 props 추가
onExcelDownload?: () => void;
onDeleteFile?: () => void;
onAnalysis?: () => void;
selectedRowData?: any;
}
// TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ?
/**
*
* @param children
* @param titleName : 메인 타이틀, 첫번째 요소 타이틀, 두번째 요소 타이틀
* @param form : zod 스키마
* @param onSubmitEvent : 폼 이벤트 처리
* @param graphBtn : 하단에 그래프 버튼 유무 처리
* @returns
*/
const BaseLayout = ({
children,
titleName,
form,
onSubmitEvent,
graphBtn = false,
analysisBtn,
selectBox,
showDatePicker = false,
enoCd,
setEnoCd,
ctpvCd,
setCtpvCd,
sigungu,
setSigungu,
selectedSwageTreatmentPlant,
onSwageTreatmentPlantChange,
selectedSwageProcessingZone,
onSwageProcessingZoneChange,
aplcnYm,
setAplcnYm,
isLoading,
// Excel 다운로드 관련 props 추가
onExcelDownload,
onDeleteFile,
onAnalysis,
selectedRowData
}: BaseLayout) => {
const defaultOption: ApiSelectOption = { val: '0', text: '선택' };
const [processingZoneOptions, setProcessingZoneOptions] = useState<ApiSelectOption[]>([]);
const [treatmentFacilityOptions, setTreatmentFacilityOptions] = useState<ApiSelectOption[]>([]);
const [isLoadingAreaData, setIsLoadingAreaData] = useState<boolean>(false);
const [atflId, setAtflId] = useState('')
const [fileNm, setFileNm] = useState('')
const [dcrpPplMtncBizSn, setDcrpPplMtncBizSn] = useState('')
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 상태가 변경될 때마다 실행
// 파일 다운로드
const isChckFile = () => {
// Excel 다운로드 함수가 있으면 Excel 다운로드 실행
if (onExcelDownload) {
onExcelDownload();
return;
}
return true
}
// 파일 삭제
const isDeleteFile = async () => {
// Excel 다운로드 함수가 있으면 Excel 다운로드 실행
if (onDeleteFile) {
onDeleteFile();
return;
}
}
return (
<Form {...form} className="min-h-screen flex flex-col">
<ListDiv>
{/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */}
{selectBox?.title && (
<div className="card-header">
<h3 className="text-2xl font-medium">{selectBox.title}</h3>
</div>
)}
<div className="flex items-center w-full">
{showDatePicker && ( // showDatePicker 값에 따라 DatePicker 렌더링
<>
<Label className="ml-4 w-20 text-xl text-gray-600 whitespace-nowrap">분석기간</Label>
<DatePicker
mode={"single"}
field={{
value: aplcnYm ?? new Date(),
onChange: setAplcnYm ?? ((date: Date | undefined) => console.log("DatePicker onChange:", date))
}}
selectClass="ml-5 date-picker-trigger" />
</>
)}
<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>
{selectBox?.searchChk ? <SearchFormRight><Button size="sm" onClick={() => selectBox.onSearchClick?.()}>조회</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" onClick={isChckFile}>파일 다운로드</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" onClick={isDeleteFile}>삭제</Button> : null}
{/* 분석 버튼 */}
{analysisBtn ? <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" onClick={onAnalysis}>{analysisBtn}</Button> : null}
</div>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmitEvent)} className="flex-grow flex flex-col">
<div className="card !m-[1.2rem] flex-grow flex flex-col">
{/* 폼 정보 입력 칸 */}
{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
}
</Form>
)
}
export default BaseLayout