diff --git a/apps/sn/app/(with-base-layout)/fileValidation.tsx b/apps/sn/app/(with-base-layout)/fileValidation.tsx new file mode 100644 index 000000000..c0711a7d2 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/fileValidation.tsx @@ -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'] +}; \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx index edee9b4ce..0e7a5c331 100644 --- a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx @@ -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 = () => { ]} - {/*
- - handleFileSelection(3, file, cd, id)} - showButton={false} - showPreview={true} - accept={".xls, .xlsx, .xlsm, .xltx"} - fileIdx={3} /> - -
- -
-
*/} ) diff --git a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx index 0ce49c797..970c6d1a9 100644 --- a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx @@ -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 ( // { }} graphBtn={false} > diff --git a/apps/sn/app/(with-base-layout)/selectionValidation.tsx b/apps/sn/app/(with-base-layout)/selectionValidation.tsx new file mode 100644 index 000000000..8cc506d0e --- /dev/null +++ b/apps/sn/app/(with-base-layout)/selectionValidation.tsx @@ -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; +}; \ No newline at end of file