- 침입수 분석 페이지에서 유량자료 선택값으로 수정
- BaseLayout.tsx의 searchBox props의 selectBox로 변수명 수정, Excel 다운로드 관련 props 추가
- 침입수분석결과 파일 다운로드, 파일 삭제 기능 구현
This commit is contained in:
swcho
2025-06-09 14:17:33 +09:00
parent 9435978291
commit e4bf427fca
4 changed files with 164 additions and 65 deletions
+23 -25
View File
@@ -1,5 +1,5 @@
'use client'
import { getData, deleteData } from '@/components/lib/api'
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'
@@ -7,7 +7,6 @@ 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'
import { handleDownload } from '@/components/lib/utils/fileDownload'
type TitleName = {
mainTitle: string
@@ -38,7 +37,7 @@ type BaseLayout = {
onSubmitEvent: Function
graphBtn?: boolean
firstDivBtn?: boolean
searchBox?: SelectBox
selectBox?: SelectBox
showDatePicker?: boolean;
enoCd?: string | number
setEnoCd?: (enoCd: string) => void
@@ -52,6 +51,10 @@ type BaseLayout = {
onSwageProcessingZoneChange?: (value: string | number) => void;
aplcnYm?: Date
setAplcnYm?: (aplcnYm: Date | undefined) => void;
// Excel 다운로드 관련 props 추가
onExcelDownload?: () => void;
onDeleteFile?: () => void;
selectedRowData?: any;
}
// TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ?
/**
@@ -69,7 +72,7 @@ const BaseLayout = ({
form,
onSubmitEvent,
graphBtn = false,
searchBox,
selectBox,
showDatePicker = false,
enoCd,
setEnoCd,
@@ -82,7 +85,11 @@ const BaseLayout = ({
selectedSwageProcessingZone,
onSwageProcessingZoneChange,
aplcnYm,
setAplcnYm
setAplcnYm,
// Excel 다운로드 관련 props 추가
onExcelDownload,
onDeleteFile,
selectedRowData
}: BaseLayout) => {
const defaultOption: ApiSelectOption = { val: '0', text: '선택' };
@@ -171,28 +178,19 @@ const BaseLayout = ({
// 파일 다운로드
const isChckFile = () => {
if (fileNm === '' || fileNm === undefined) {
alert('파일을 선택해주세요');
return false
// Excel 다운로드 함수가 있으면 Excel 다운로드 실행
if (onExcelDownload) {
onExcelDownload();
return;
}
handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm)
return true
}
// 파일 삭제
const isDeleteFile = async () => {
if (dcrpPplMtncBizSn === '' || dcrpPplMtncBizSn === undefined) {
alert('삭제할 파일을 선택해주세요');
return false
}
const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`)
if (data) {
const delData = await deleteData(`v1/dcrpPpl/${dcrpPplMtncBizSn}`);
if (delData.status == 200) {
alert('삭제되었습니다.')
searchBox?.onSearchClick?.();
} else {
alert('삭제에 실패했습니다.')
}
// Excel 다운로드 함수가 있으면 Excel 다운로드 실행
if (onDeleteFile) {
onDeleteFile();
return;
}
}
@@ -201,9 +199,9 @@ const BaseLayout = ({
<ContentLayout title={titleName.mainTitle} className="flex-grow flex flex-col overflow-hidden">
<ListDiv>
{/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */}
{searchBox?.title && (
{selectBox?.title && (
<div className="card-header">
<h3 className="text-2xl font-medium">{searchBox.title}</h3>
<h3 className="text-2xl font-medium">{selectBox.title}</h3>
</div>
)}
<div className="flex items-center w-full">
@@ -228,7 +226,7 @@ const BaseLayout = ({
<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 onClick={searchBox.onSearchClick}>조회</Button></SearchFormRight> : null}
{selectBox?.searchChk ? <SearchFormRight><Button onClick={() => selectBox.onSearchClick?.()}>조회</Button></SearchFormRight> : null}
</div>
</ListDiv>
<ListDiv>
+23 -12
View File
@@ -141,18 +141,27 @@ const IrSetting = () => {
console.log("유량자료 정보:", file4);
// 모든 필수 파일이 선택되었는지 확인 (UploadedFileInfo 객체와 그 안의 file 객체 모두 확인)
if (!file1?.file || !file2?.file || !file3?.file || !file4?.file) {
// 유량자료는 선택값
if (!file1?.file || !file2?.file || !file3?.file) {
alert('모든 분석용 파일을 선택해주세요.');
return;
}
const filePickers = [
// filePicked 유틸리티가 File 객체를 직접 받는지, ArrayBuffer 등을 받는지 확인 필요
// 여기서는 File 객체를 받고, 내부적으로 ArrayBuffer로 변환하여 parser에 전달한다고 가정
{ file: file2, parser: excelToMatchingResultData },
{ file: file3, parser: excelToTimePatternData },
{ file: file4, parser: excelToFlowRateData }
];
type MatchingResult = { anlsSn: number; mhlMngNo: string; rcptrNo: string; };
type TimePattern = { hrId: number; rt?: number | null | undefined; };
type FlowRate = { pplMngNo: string; mtrngDt: string; oilq?: number | null | undefined; };
const filePickers: {
file: UploadedFileInfo;
parser: (excelData: any) => MatchingResult[] | TimePattern[] | FlowRate[];
}[] = [
{ file: file2, parser: excelToMatchingResultData },
{ file: file3, parser: excelToTimePatternData }
];
if (file4?.file) {
filePickers.push({ file: file4, parser: excelToFlowRateData });
}
try {
const parsedResults = await Promise.all(
@@ -186,7 +195,9 @@ const IrSetting = () => {
// 각 파싱 결과 접근
const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData;
const timePatternData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData;
const flowRateData = parsedResults.find(r => r && r.fileName === file4.file?.name)?.parsedData;
const flowRateData = file4?.file
? parsedResults.find(r => r && r.fileName === file4.file?.name)?.parsedData
: null;
// 새로운 JSON 데이터 생성
const combinedAnalysisData = {
@@ -203,10 +214,10 @@ const IrSetting = () => {
atflInpNm: file1?.file?.name, // 오수관망도 파일명
atflHrPttnNm: file3?.file?.name, // 오수량 시간패턴 파일명
atflMpngNm: file2?.file?.name, // 수용가-맨홀 매칭결과 파일명
atflOilqNm: file4?.file?.name, // 유량자료 파일명
atflOilqNm: file4?.file?.name || null, // 유량자료 파일명 (없을 수 있음)
tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과
tbSnHrPttn: timePatternData, // 오수량 시간패턴
tbSnOilqData: flowRateData, // 유량자료
tbSnOilqData: flowRateData, // 유량자료 (없을 수 있음)
};
console.log("통합 분석 데이터 (JSON):", combinedAnalysisData);
@@ -228,7 +239,7 @@ const IrSetting = () => {
form={form}
onSubmitEvent={() => { }}
graphBtn={false}
searchBox={{ title: '분석 대상지역 선택' }}
selectBox={{ title: '분석 대상지역 선택' }}
enoCd={select1}
setEnoCd={setSelect1}
ctpvCd={select2}
+1 -1
View File
@@ -313,7 +313,7 @@ export default function RTKForm() {
return (
// <BaseLayout searchBox={{ title: '분석 대상지역 선택' }} titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} form={form} onSubmitEvent={() => { }} graphBtn={false} >
<BaseLayout
searchBox={{ title: '분석 대상지역 선택' }}
selectBox={{ title: '분석 대상지역 선택' }}
titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }}
form={form} // react-hook-form 인스턴스 전달
onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit
+117 -27
View File
@@ -1,13 +1,12 @@
'use client'
import { getData } from '@/components/lib/api'
import { UiTable } from '@common/business_components/ui'
import { getData, deleteData } from '@/components/lib/api'
import { UiTable, downloadExcel } from '@common/business_components/ui'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import BaseLayout from '../BaseLayout'
interface ApiSelectOption {
text: string;
val: string;
@@ -27,7 +26,6 @@ const formSchema = z.object({
type ResultFormValues = z.infer<typeof formSchema>
// TODO: 분석구분에 따라서 테이블 컬럼 변경필요
const tableColumn = [
// { id: 'no', title: '번호' },
{ id: 'inftwtAnlsSn', title: '분석번호' },
@@ -36,14 +34,8 @@ const tableColumn = [
{ id: 'aplcnYm', title: '적용연월' },
{ id: 'drtwSwtcRt', title: '오수 전환율' },
{ id: 'anlsInfo', title: '침입수 분석정보' },
{
id: 'regDt',
title: '분석일자',
},
{
id: 'rgtrNm',
title: '분석자',
}
{ id: 'regDt', title: '분석일자' },
{ id: 'rgtrNm', title: '분석자' }
]
const tableColumn2 = [
{ id: 'no', title: '번호' },
@@ -75,21 +67,36 @@ const Result = () => {
const [tableData, setTableData] = useState<any[]>([]); // API 조회 결과 저장 상태
const [totalData, setTotalData] = useState(0) //총 데이터 수
const [rdiiResultData, setRdiiResultData] = useState<any[]>([]); // RDII 결과 저장 상태 (필요시)
const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태
const [selectedRowData, setSelectedRowData] = useState<any>(null);
const [selectedRowIndex, setSelectedRowIndex] = useState<number | undefined>(undefined);
const handleSearchButtonClick = async () => {
const handleSearchButtonClick = async (customFormValues?: Partial<ResultFormValues>) => {
setIsLoadingResult(true);
const formValues = form.getValues(); // react-hook-form에서 현재 값 가져오기
const formValues = customFormValues || form.getValues(); // react-hook-form에서 현재 값 가져오기
console.log("formValues", formValues);
const inftwtAnlsSearchDto = {
"aplcnYm": formValues.aplcnYm.toISOString().substring(0, 7).replace("-", ""),
"enoCd": formValues.enoCd === '0' ? null : formValues.enoCd,
"ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd,
"sggCd": formValues.sggCd.val === '0' ? null : formValues.sggCd.val,
// "swageTmplntCd": formValues.swageTmplntCd === '0' ? null : formValues.swageTmplntCd,
"swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd,
};
let inftwtAnlsSearchDto;
if (customFormValues) {
inftwtAnlsSearchDto = {
"aplcnYm": formValues.aplcnYm,
"enoCd": formValues.enoCd,
"ctpvCd": formValues.ctpvCd,
"sggCd": formValues.sggCd,
"swtrmtZoneCd": formValues.swtrmtZoneCd,
};
} else {
inftwtAnlsSearchDto = {
"aplcnYm": formValues.aplcnYm ? formValues.aplcnYm.toISOString().substring(0, 7).replace("-", "") : null,
"enoCd": formValues.enoCd === '0' ? null : formValues.enoCd,
"ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd,
"sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val,
"swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd,
};
}
console.log("Search DTO:", inftwtAnlsSearchDto);
const queryParams = new URLSearchParams({
page: '0',
@@ -103,7 +110,7 @@ const Result = () => {
});
const queryString = queryParams.toString();
console.log("API: ", `v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); // 디버깅용 로그
console.log("API: ", `v1/inftwtAnls${queryString ? `?${queryString}` : ''}`);
try {
const data = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`);
setTableData(data.content || []);
@@ -120,9 +127,89 @@ const Result = () => {
}
};
// 테이블 셀 클릭 핸들러 추가
const handleCellClick = (index: number | undefined, headId: string) => {
if (index !== undefined && tableData[index]) {
setSelectedRowData(tableData[index]);
setSelectedRowIndex(index);
console.log("Selected row data:", tableData[index]);
}
};
// Excel 다운로드 핸들러 추가
const handleExcelDownload = async () => {
console.log("Selected row data for download:", selectedRowData);
if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) {
alert('다운로드할 데이터를 선택해주세요.');
return;
}
try {
const data = await getData(`v1/inftwtAnlsRslt/ppl/${selectedRowData.inftwtAnlsSn}`);
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = data.content; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const formValues = form.getValues();
const fileName = `침입수분석결과_${selectedRowData.inftwtAnlsSn}_${selectedRowData.aplcnYm}`;
downloadExcel({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: ["침입수 분석결과"],
headerRange: "A1:D1" // 테이블 컬럼 수에 맞춰 조정
});
} catch (error) {
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
};
// 파일 삭제 핸들러 추가
const handleFileDelete = async () => {
console.log("Selected row data for deletion:", selectedRowData);
if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) {
alert('삭제할 파일을 선택해주세요.');
return;
}
// 삭제 확인 대화상자
const confirmDelete = window.confirm('선택된 분석 결과를 삭제하시겠습니까?');
if (!confirmDelete) {
return;
}
try {
// 삭제 API 호출
const response1 = await deleteData(`v1/inftwtAnlsRslt/${selectedRowData.inftwtAnlsSn}`);
const response2 = await deleteData(`v1/inftwtAnls/${selectedRowData.inftwtAnlsSn}`);
console.log("File delete response1:", response1);
console.log("File delete response2:", response2);
// 삭제 후 테이블 새로고침을 위해 재검색
const customFormValues: Partial<ResultFormValues> = {
aplcnYm: selectedRowData.aplcnYm || '0',
enoCd: selectedRowData.enoCd || '0',
ctpvCd: selectedRowData.ctpvCd || '0',
sggCd: selectedRowData.sggCd || '0',
swtrmtZoneCd: selectedRowData.swtrmtZoneCd || '0',
};
await handleSearchButtonClick(customFormValues);
// 선택된 행 초기화
setSelectedRowData(null);
setSelectedRowIndex(undefined);
alert('삭제가 완료되었습니다.');
} catch (error) {
console.error("분석결과 파일 삭제 중 오류 발생:", error);
alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요.");
}
};
return (
<BaseLayout
searchBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }}
selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }}
titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력', secondCardTitle: 'RDII 산정결과 현황' }}
form={form} // form 인스턴스 전달
onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit
@@ -146,7 +233,10 @@ const Result = () => {
form.setValue("aplcnYm", new Date(val))
}
}}
// DatePicker는 BaseLayout 내부에서 form.watch("aplcnYm")과 form.setValue("aplcnYm", ...)을 사용하도록 수정 필요
// Excel 다운로드 핸들러 추가
onExcelDownload={handleExcelDownload}
onDeleteFile={handleFileDelete}
selectedRowData={selectedRowData}
>
{[<div key='table1' className="h-[55vh] flex flex-col p-4 border rounded-lg bg-white shadow-md">
<div className="mb-[1.2rem] text-xl font-medium">○ 침입수 산정결과 현황</div>
@@ -158,7 +248,7 @@ const Result = () => {
total={totalData}
pageSize={10}
children={undefined}
cellClick={(e) => { }}
cellClick={handleCellClick}
onPageChange={(e) => { }}
/>
</div>