feat:
- 파일 확장자 검증 컴포넌트 구현 및 적용 - 환경청, 시도, 시군구, 하수처리장 입력 검증 컴포넌트 구현 및 적용 - RDII 분석 페이지 하수처리장 선택 로직 에러 수정
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user