- 파일 확장자 검증 컴포넌트 구현 및 적용
- 환경청, 시도, 시군구, 하수처리장 입력 검증 컴포넌트 구현 및 적용
- RDII 분석 페이지 하수처리장 선택 로직 에러 수정
This commit is contained in:
swcho
2025-07-04 17:26:10 +09:00
parent 26c36dd966
commit 5b62171b01
4 changed files with 303 additions and 49 deletions
@@ -0,0 +1,48 @@
/**
* 파일 확장자 검증 유틸리티
*/
// 파일 확장자 검증 함수
export const validateFileExtension = (file: File, allowedExtensions: string[]): boolean => {
const fileName = file.name.toLowerCase();
return allowedExtensions.some(ext => fileName.endsWith(ext.toLowerCase()));
};
// 파일 확장자 검증 결과 타입
export interface FileValidationResult {
isValid: boolean;
errorMessage?: string;
}
// 파일 검증 설정 타입
export interface FileValidationConfig {
file: File;
allowedExtensions: string[];
fileDescription: string;
}
/**
* 여러 파일의 확장자를 한 번에 검증하는 함수
* @param validationConfigs 검증할 파일들의 설정 배열
* @returns 검증 결과. 실패 시 첫 번째 에러 메시지 반환
*/
export const validateMultipleFiles = (validationConfigs: FileValidationConfig[]): FileValidationResult => {
for (const config of validationConfigs) {
if (!validateFileExtension(config.file, config.allowedExtensions)) {
const allowedExtStr = config.allowedExtensions.join(', ');
return {
isValid: false,
errorMessage: `${config.fileDescription} 파일은 ${allowedExtStr} 확장자만 업로드 가능합니다.`
};
}
}
return { isValid: true };
};
// 자주 사용되는 확장자 상수
export const FILE_EXTENSIONS = {
INP: ['.inp'],
EXCEL: ['.xls', '.xlsx', '.xlsm', '.xltx'],
ALL_OFFICE: ['.xls', '.xlsx', '.xlsm', '.xltx', '.doc', '.docx', '.ppt', '.pptx']
};
@@ -6,11 +6,13 @@ import { DateMonthPicker, filePicked } from '@common/business_components/ui'
import ListDiv from '@common/business_components/ui/list_div'
import { Input } from '@common/components/ui'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState, useEffect } from 'react'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { excelToFlowRateData, excelToMatchingResultData, excelToTimePatternData } from '@/components/lib/utils'
import BaseLayout from '../BaseLayout'
import { validateMultipleFiles, FILE_EXTENSIONS, type FileValidationConfig } from '../../fileValidation';
import { validateBasicSelectionsWithAlert } from '../../selectionValidation';
const formSchema = z.object({
managenm: z.string(),
@@ -148,6 +150,17 @@ const IrSetting = () => {
const handleAnalysisClick = async () => {
setIsLoadingResult(true); // 로딩 상태 시작
// 기본 선택 항목들 검증 (환경청, 시도, 시군구, 하수처리장)
if (!validateBasicSelectionsWithAlert(select1, select2, select3, select4, {
requireEnv: true,
requireSido: true,
requireSigungu: true,
requireTreatmentPlant: true
})) {
setIsLoadingResult(false);
return;
}
console.log(uploadedFilesData);
const file1 = uploadedFilesData[0]; // 오수관망도
const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과
@@ -167,6 +180,42 @@ const IrSetting = () => {
return;
}
// 파일 확장자 검증 (분리된 유틸리티 사용)
const validationConfigs: FileValidationConfig[] = [
{
file: file1.file,
allowedExtensions: FILE_EXTENSIONS.INP,
fileDescription: '오수관망도'
},
{
file: file2.file,
allowedExtensions: FILE_EXTENSIONS.EXCEL,
fileDescription: '수용가-맨홀 매칭결과'
},
{
file: file3.file,
allowedExtensions: FILE_EXTENSIONS.EXCEL,
fileDescription: '오수량 시간패턴'
}
];
// 유량자료 파일이 있는 경우 검증 목록에 추가
if (file4?.file) {
validationConfigs.push({
file: file4.file,
allowedExtensions: FILE_EXTENSIONS.EXCEL,
fileDescription: '유량자료'
});
}
// 파일 확장자 검증 실행
const validationResult = validateMultipleFiles(validationConfigs);
if (!validationResult.isValid) {
setIsLoadingResult(false);
alert(validationResult.errorMessage);
return;
}
// 오수전환율 값 0 ~ 1 사이인지 검증
if (input1 === '' || input1 === null || input1 === undefined) {
setIsLoadingResult(false);
@@ -394,21 +443,6 @@ const IrSetting = () => {
</div>
</ListDiv>
</div>]}
{/* <div className="flex items-center gap-4">
<FormSection
name='region' form={form} titleNm='▷ 유량자료 등록'>
<FileUploadForm
key={`file-upload-3-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(3, file, cd, id)}
showButton={false}
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={3} />
</FormSection>
<div className="flex justify-center mt-1">
<Button size="sm" type="submit" onClick={handleAnalysisClick}>침입수분석 실행</Button>
</div>
</div> */}
</BaseLayout>
</>
)
+44 -33
View File
@@ -13,6 +13,8 @@ import { useFieldArray, useForm } from "react-hook-form"
import { z } from "zod"
import { excelToMatchingResultData, excelToRainData } from '@/components/lib/utils'
import { EditableFormTable, type EditableTableHeadItem } from '../../../components/ui/EditableFormTable'
import { validateMultipleFiles, FILE_EXTENSIONS, type FileValidationConfig } from '../../fileValidation';
import { validateBasicSelectionsWithAlert } from '../../selectionValidation';
// zod 스키마 정의
@@ -167,7 +169,18 @@ export default function RTKForm() {
// 침입수 분석 실행 버튼 클릭 핸들러
const handleAnalysisClick = async () => {
setIsLoadingResult(true); // 로딩 상태 시작
// 기본 선택 항목들 검증 (환경청, 시도, 시군구, 하수처리장)
if (!validateBasicSelectionsWithAlert(select1, select2, select3, select4, {
requireEnv: true,
requireSido: true,
requireSigungu: true,
requireTreatmentPlant: true
})) {
setIsLoadingResult(false);
return;
}
const formData = form.getValues();
console.log(uploadedFilesData);
const file1 = uploadedFilesData[0]; // 오수관망도
@@ -185,6 +198,33 @@ export default function RTKForm() {
return;
}
// 파일 확장자 검증 (분리된 유틸리티 사용)
const validationConfigs: FileValidationConfig[] = [
{
file: file1.file,
allowedExtensions: FILE_EXTENSIONS.INP,
fileDescription: '오수관망도'
},
{
file: file2.file,
allowedExtensions: FILE_EXTENSIONS.EXCEL,
fileDescription: '수용가-맨홀 매칭결과'
},
{
file: file3.file,
allowedExtensions: FILE_EXTENSIONS.EXCEL,
fileDescription: '오수량 시간패턴'
}
];
// 파일 확장자 검증 실행
const validationResult = validateMultipleFiles(validationConfigs);
if (!validationResult.isValid) {
setIsLoadingResult(false);
alert(validationResult.errorMessage);
return;
}
// 오수전환율 값 0 ~ 1 사이인지 검증
const rateValue = form.getValues().rate;
if (rateValue === '' || rateValue === null || rateValue === undefined) {
@@ -193,7 +233,7 @@ export default function RTKForm() {
alert('오수전환율을 입력해주세요.');
return;
}
const numericInput1 = parseFloat(rateValue);
if (isNaN(numericInput1) || numericInput1 < 0 || numericInput1 > 1) {
setIsLoadingResult(false);
@@ -341,35 +381,6 @@ export default function RTKForm() {
console.log('Page changed:', page)
}
// 선택된 값을 처리하는 공통 함수
const handleSelectValueChange = (
val: string | number,
setter: (value: string | number) => void
) => {
// val이 0이 아닐 경우 .을 기준으로 슬라이싱해서 두번째 값을 설정
if (val && val !== '0' && val !== 0) {
const valString = String(val);
const parts = valString.split('.');
if (parts.length > 1 && parts[1] !== undefined) {
setter(parts[1]); // 두 번째 값 (인덱스 1)
} else {
setter(val); // .이 없으면 원래 값 사용
}
} else {
setter('0'); // val이 0이면 '0' 설정
}
};
// 하수처리장 변경 핸들러
const handleSwageTreatmentPlantChange = (val: string | number) => {
handleSelectValueChange(val, setSelect4);
};
// 하수처리구역 변경 핸들러
const handleSwageProcessingZoneChange = (val: string | number) => {
handleSelectValueChange(val, setSelect5);
};
return (
// <BaseLayout searchBox={{ title: '분석 대상지역 선택' }} titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} form={form} onSubmitEvent={() => { }} graphBtn={false} >
<BaseLayout
@@ -386,9 +397,9 @@ export default function RTKForm() {
sigungu={select3}
setSigungu={setSelect3}
selectedSwageTreatmentPlant={select4}
onSwageTreatmentPlantChange={handleSwageTreatmentPlantChange}
onSwageTreatmentPlantChange={setSelect4}
selectedSwageProcessingZone={select5}
onSwageProcessingZoneChange={handleSwageProcessingZoneChange}
onSwageProcessingZoneChange={setSelect5}
isLoading={isLoadingResult}
onAnalysis={handleAnalysisClick}
>
@@ -0,0 +1,161 @@
/**
* 선택 항목 검증 유틸리티
*/
export interface ApiSelectOption {
text: string;
val: string;
}
// 선택 항목 검증 설정 타입
export interface SelectionValidationConfig {
value: string | number | ApiSelectOption;
description: string;
required: boolean;
}
// 선택 항목 검증 결과 타입
export interface SelectionValidationResult {
isValid: boolean;
errorMessage?: string;
}
/**
* 단일 선택 항목이 유효한지 검증하는 함수
* @param value 검증할 값
* @param isRequired 필수 선택 여부
* @returns 유효한지 여부
*/
export const isValidSelection = (
value: string | number | ApiSelectOption,
isRequired: boolean = true
): boolean => {
if (!isRequired) return true;
if (typeof value === 'object' && value !== null) {
// ApiSelectOption 객체인 경우
return value.val !== '0' && value.val !== '' && value.val !== null && value.val !== undefined;
}
// 문자열 또는 숫자인 경우
return value !== '0' && value !== 0 && value !== '' && value !== null && value !== undefined;
};
/**
* 여러 선택 항목을 한 번에 검증하는 함수
* @param validationConfigs 검증할 선택 항목들의 설정 배열
* @returns 검증 결과. 실패 시 첫 번째 에러 메시지 반환
*/
export const validateMultipleSelections = (
validationConfigs: SelectionValidationConfig[]
): SelectionValidationResult => {
for (const config of validationConfigs) {
if (!isValidSelection(config.value, config.required)) {
return {
isValid: false,
errorMessage: `${config.description}을(를) 선택해주세요.`
};
}
}
return { isValid: true };
};
/**
* 여러 선택 항목을 한 번에 검증하고 alert 표시하는 함수
* @param validationConfigs 검증할 선택 항목들의 설정 배열
* @returns 검증 결과
*/
export const validateMultipleSelectionsWithAlert = (
validationConfigs: SelectionValidationConfig[]
): boolean => {
const result = validateMultipleSelections(validationConfigs);
if (!result.isValid && result.errorMessage) {
alert(result.errorMessage);
return false;
}
return true;
};
/**
* 기본 선택 항목들 검증 (환경청, 시도, 시군구, 하수처리장)
* @param select1 환경청코드
* @param select2 시도코드
* @param select3 시군구 객체
* @param select4 하수처리장코드
* @param options 각 항목의 필수 여부 설정
* @returns 검증 결과
*/
export const validateBasicSelections = (
select1: string,
select2: string,
select3: ApiSelectOption,
select4: string | number,
options: {
requireEnv?: boolean;
requireSido?: boolean;
requireSigungu?: boolean;
requireTreatmentPlant?: boolean;
} = {}
): SelectionValidationResult => {
const {
requireEnv = true,
requireSido = true,
requireSigungu = true,
requireTreatmentPlant = true
} = options;
const validationConfigs: SelectionValidationConfig[] = [
{
value: select1,
description: '환경청',
required: requireEnv
},
{
value: select2,
description: '시도',
required: requireSido
},
{
value: select3,
description: '시군구',
required: requireSigungu
},
{
value: select4,
description: '하수처리장',
required: requireTreatmentPlant
}
];
return validateMultipleSelections(validationConfigs);
};
/**
* 기본 선택 항목들 검증 및 alert 표시
* @param select1 환경청코드
* @param select2 시도코드
* @param select3 시군구 객체
* @param select4 하수처리장코드
* @param options 각 항목의 필수 여부 설정
* @returns 검증 결과
*/
export const validateBasicSelectionsWithAlert = (
select1: string,
select2: string,
select3: ApiSelectOption,
select4: string | number,
options: {
requireEnv?: boolean;
requireSido?: boolean;
requireSigungu?: boolean;
requireTreatmentPlant?: boolean;
} = {}
): boolean => {
const result = validateBasicSelections(select1, select2, select3, select4, options);
if (!result.isValid && result.errorMessage) {
alert(result.errorMessage);
return false;
}
return true;
};