643 lines
23 KiB
TypeScript
643 lines
23 KiB
TypeScript
/**
|
|
* ➡️ 경로 : I/I 분석 - 침입수 분석
|
|
*
|
|
* 📄 IrSetting Page
|
|
* 침입수 분석 설정 페이지 컴포넌트
|
|
*
|
|
* - 오수관망도, 수용가-맨홀 매칭결과, 오수량 시간패턴 등 파일 업로드 기능
|
|
* - 오수전환율 및 분석 정보 입력 기능
|
|
* - 엑셀 파일 데이터 파싱 및 JSON 변환 처리
|
|
* - 분석 데이터 서버 제출 및 폼 초기화 기능
|
|
* - 파일 확장자 및 입력 값 검증 기능
|
|
* - 로딩 오버레이 및 에러 핸들링 지원
|
|
*
|
|
* 🔗 사용 API
|
|
* - 침입수분석 등록 첨부파일INP 포함: POST /api/v1/inftwtAnls/savewithfile (sewerage-service)
|
|
* - 로그인된 사용자의 사용자 정보 조회: GET /api/v1/users/info/me (user-service)
|
|
*/
|
|
"use client";
|
|
import { postMutiPart } from "@/components/lib/api";
|
|
import FileUploadForm from "@/components/ui/fileUploadForm";
|
|
import {
|
|
StandardFormField,
|
|
StandardFormSection,
|
|
} from "@common/business_components";
|
|
import { DateMonthPicker, filePicked } from "@common/business_components/ui";
|
|
import { Input } from "@common/components/ui";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { z } from "zod";
|
|
import {
|
|
excelToFlowRateData,
|
|
excelToMatchingResultData,
|
|
excelToTimePatternData,
|
|
excelToRcptrInfoData,
|
|
} from "@/components/lib/utils";
|
|
import BaseLayout from "../BaseLayout";
|
|
import { useAnalysisCompletionWatcher } from "../useAnalysisCompletionWatcher";
|
|
import { openAnalysisResultMapView } from "../analysisResultView";
|
|
import {
|
|
validateMultipleFiles,
|
|
FILE_EXTENSIONS,
|
|
type FileValidationConfig,
|
|
} from "../../fileValidation";
|
|
import { validateBasicSelections } from "../../selectionValidation";
|
|
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
|
import { AUTH_USER_ID } from "@/shared/config";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { ENDPOINT } from "@common/components/config";
|
|
import { getUserInfo } from "@common/components/api";
|
|
import LoadingOverlay from "@common/business_components/ui/loadingBar";
|
|
import { useAlertStore } from "@/shared/lib";
|
|
|
|
const formSchema = z.object({
|
|
managenm: z.string(),
|
|
region: z.string(),
|
|
suyeonga: z.number(),
|
|
inputdate: z.coerce.date(),
|
|
rtkSource: z.string(),
|
|
suyeongaPlace: z.string(),
|
|
pattern: z.string(),
|
|
});
|
|
|
|
// 각 파일 업로드 폼의 데이터를 저장할 인터페이스 정의
|
|
interface UploadedFileInfo {
|
|
file: File | null;
|
|
atflCd: string | null;
|
|
atflId: string | null;
|
|
// 필요하다면 파일 이름 등 추가 정보 저장 가능
|
|
// fileName: string;
|
|
}
|
|
// 초기 파일 정보 상태
|
|
const initialFileInfo: UploadedFileInfo = {
|
|
file: null,
|
|
atflCd: null,
|
|
atflId: null,
|
|
};
|
|
const DEFAULT_CTPV_CD = "41";
|
|
const DEFAULT_SGG_CD = "41830";
|
|
const DEFAULT_SGG_NAME = "\uC591\uD3C9\uAD70";
|
|
const DEFAULT_SWTRMT_FCLT_NM = "\uC591\uD3C9";
|
|
|
|
const IrSetting = () => {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const [input1, setInput1] = useState("0.8"); // 오수전환율 초기값
|
|
const [input2, setInput2] = useState("");
|
|
const [selectedSwagePrcsFclt, setSelectedSwagePrcsFclt] =
|
|
useState<string>("0"); // ?????? ?? ??
|
|
const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태
|
|
const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태
|
|
const [ctpvCd, setCtpvCd] = useState<string>(DEFAULT_CTPV_CD); // 시도 코드 상태
|
|
const [sggCd, setSggCd] = useState<string>(DEFAULT_SGG_CD); // 시군구 코드 상태
|
|
const [sggName, setSggName] = useState<string>(DEFAULT_SGG_NAME); // 시군구 이름 상태
|
|
const [swtrmtFcltNm, setSwtrmtFcltNm] = useState<string>(
|
|
DEFAULT_SWTRMT_FCLT_NM,
|
|
); // 하수처리시설명 상태
|
|
const [uploadedFilesData, setUploadedFilesData] = useState<
|
|
UploadedFileInfo[]
|
|
>([
|
|
initialFileInfo, // 0: 오수관망도
|
|
initialFileInfo, // 1: 수용가-맨홀 매칭결과
|
|
initialFileInfo, // 2: 오수량 시간패턴
|
|
initialFileInfo, // 3: 유량자료 등록
|
|
]);
|
|
// selectedDate를 yyyyMM 문자열 형식으로 변경
|
|
const [selectedDate, setSelectedDate] = useState(() => {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
return `${year}${month}`;
|
|
});
|
|
|
|
const { data: userInfo } = useQuery({
|
|
queryKey: [ENDPOINT.USER_SERVICE.USERS],
|
|
queryFn: () => getUserInfo(),
|
|
enabled: true,
|
|
});
|
|
|
|
const { startWatching } = useAnalysisCompletionWatcher({
|
|
buildStatusUrl: (analysisSn) => `v1/inftwtAnls/${analysisSn}/status`,
|
|
successMessage: "침입수 분석이 완료되었습니다.",
|
|
failureMessage: "침입수 분석이 실패했습니다. 로그를 확인해주세요.",
|
|
analysisType: "INFTWT",
|
|
analysisName: "침입수 분석",
|
|
initialProgressPercent: 10,
|
|
initialProgressStep: "REQUEST_ACCEPTED",
|
|
initialProgressMessage: "요청 접수 완료",
|
|
});
|
|
|
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
resolver: zodResolver(formSchema),
|
|
defaultValues: {
|
|
managenm: "",
|
|
region: "",
|
|
suyeonga: 0,
|
|
inputdate: new Date(),
|
|
rtkSource: "",
|
|
suyeongaPlace: "",
|
|
pattern: "",
|
|
},
|
|
});
|
|
|
|
// 폼 초기화 함수
|
|
const resetAllForms = () => {
|
|
form.reset({
|
|
// react-hook-form 초기화
|
|
managenm: "",
|
|
region: "",
|
|
suyeonga: 0,
|
|
inputdate: new Date(),
|
|
rtkSource: "",
|
|
suyeongaPlace: "",
|
|
pattern: "",
|
|
});
|
|
setUploadedFilesData([
|
|
// 파일 업로드 상태 초기화
|
|
initialFileInfo,
|
|
initialFileInfo,
|
|
initialFileInfo,
|
|
initialFileInfo,
|
|
]);
|
|
// selectedDate를 현재 연월로 초기화
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
setSelectedDate(`${year}${month}`);
|
|
setInput1(""); // 입력 필드 초기화
|
|
setInput2(""); // 입력 필드 초기화
|
|
setSelectedSwagePrcsFclt("0");
|
|
setCtpvCd(DEFAULT_CTPV_CD);
|
|
setSggCd(DEFAULT_SGG_CD);
|
|
setSggName(DEFAULT_SGG_NAME);
|
|
setSwtrmtFcltNm(DEFAULT_SWTRMT_FCLT_NM);
|
|
setFormResetKey((prevKey) => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경
|
|
};
|
|
|
|
// FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수
|
|
const handleFileSelection = (
|
|
index: number, // FileUploadForm의 fileIdx 값
|
|
selectedFile: File | null,
|
|
cd: string | null,
|
|
id: string | null,
|
|
) => {
|
|
setUploadedFilesData((prevData) => {
|
|
const newData = [...prevData];
|
|
newData[index] = {
|
|
file: selectedFile,
|
|
atflCd: cd,
|
|
atflId: id,
|
|
};
|
|
return newData;
|
|
});
|
|
};
|
|
|
|
const registFile = async (analysisData: any) => {
|
|
const data = await postMutiPart("v1/inftwtAnls/savewithfile", analysisData);
|
|
if (!data) {
|
|
throw new Error("분석 요청 접수 응답이 비어 있습니다.");
|
|
}
|
|
|
|
const nextAnalysisSn = Number(data?.inftwtAnlsSn ?? 0);
|
|
setIsLoadingResult(false);
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
alert("분석 데이터가 성공적으로 제출되었습니다.", () => {
|
|
if (Number.isFinite(nextAnalysisSn) && nextAnalysisSn > 0) {
|
|
startWatching(nextAnalysisSn);
|
|
}
|
|
resetAllForms();
|
|
});
|
|
};
|
|
|
|
// 침입수 분석 실행 버튼 클릭 핸들러
|
|
const handleAnalysisClick = async () => {
|
|
setIsLoadingResult(true); // 로딩 상태 시작
|
|
|
|
// 기본 선택 항목들 검증 (환경청, 시도, 시군구, 하수처리장)
|
|
const selectionValidation = validateBasicSelections(selectedSwagePrcsFclt, {
|
|
requireEnv: true,
|
|
requireSido: true,
|
|
requireSigungu: true,
|
|
requireTreatmentPlant: true,
|
|
treatmentPlantLabel: "하수처리시설",
|
|
});
|
|
if (!selectionValidation.isValid) {
|
|
setIsLoadingResult(false);
|
|
alert(
|
|
selectionValidation.errorMessage ?? "필수 선택 항목을 확인해주세요.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const file1 = uploadedFilesData[0]; // 오수관망도
|
|
const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과
|
|
const file3 = uploadedFilesData[2]; // 오수량 시간패턴
|
|
const file4 = uploadedFilesData[3]; // 유량자료
|
|
const file5 = uploadedFilesData[4]; // 수용가 상수사용량
|
|
|
|
// 모든 필수 파일이 선택되었는지 확인 (UploadedFileInfo 객체와 그 안의 file 객체 모두 확인)
|
|
// 유량자료는 선택값
|
|
if (!file1?.file || !file2?.file || !file3?.file) {
|
|
setIsLoadingResult(false);
|
|
alert("모든 분석용 파일을 선택해주세요.");
|
|
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: "유량자료",
|
|
});
|
|
}
|
|
// 수용가 상수사용량 파일이 있는 경우 검증 목록에 추가
|
|
if (file5?.file) {
|
|
validationConfigs.push({
|
|
file: file5.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);
|
|
console.error("오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.");
|
|
alert("오수전환율을 입력해주세요.");
|
|
return;
|
|
}
|
|
|
|
const numericInput1 = parseFloat(input1);
|
|
if (isNaN(numericInput1) || numericInput1 < 0 || numericInput1 > 1) {
|
|
setIsLoadingResult(false);
|
|
console.error("오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.");
|
|
alert("오수전환율은 0에서 1 사이의 값으로 입력해야 합니다.");
|
|
return;
|
|
}
|
|
|
|
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;
|
|
};
|
|
type rcptrInfo = {
|
|
rcptrNo: string;
|
|
useYm: string;
|
|
usqty?: number | null | undefined;
|
|
};
|
|
|
|
const filePickers: {
|
|
file: UploadedFileInfo;
|
|
parser: (
|
|
excelData: any,
|
|
) => MatchingResult[] | TimePattern[] | FlowRate[] | rcptrInfo[];
|
|
}[] = [
|
|
{ file: file2, parser: excelToMatchingResultData },
|
|
{ file: file3, parser: excelToTimePatternData },
|
|
];
|
|
|
|
if (file4?.file) {
|
|
// 유량자료 파일이 있는 경우
|
|
filePickers.push({ file: file4, parser: excelToFlowRateData });
|
|
}
|
|
if (file5?.file) {
|
|
// 수용가 상수사용량 파일이 있는 경우
|
|
filePickers.push({ file: file5, parser: excelToRcptrInfoData });
|
|
}
|
|
|
|
try {
|
|
const parsedResults = await Promise.all(
|
|
filePickers.map(async ({ file, parser }) => {
|
|
if (!file || !file.file) return null;
|
|
|
|
const jsonData = await filePicked(file.file, 0);
|
|
|
|
if (jsonData === undefined) {
|
|
console.warn(
|
|
`Warning: filePicked returned undefined for ${file.file.name}`,
|
|
);
|
|
return {
|
|
fileName: file.file.name,
|
|
atflCd: file.atflCd,
|
|
atflId: file.atflId,
|
|
parsedData: null,
|
|
};
|
|
}
|
|
|
|
// jsonData를 각 parser 함수로 처리
|
|
const parsedDataBySpecificFunction = parser(jsonData);
|
|
|
|
return {
|
|
fileName: file.file.name,
|
|
atflCd: file.atflCd,
|
|
atflId: file.atflId,
|
|
parsedData: parsedDataBySpecificFunction,
|
|
};
|
|
}),
|
|
);
|
|
|
|
// 각 파싱 결과 접근
|
|
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 = file4?.file
|
|
? parsedResults.find((r) => r && r.fileName === file4.file?.name)
|
|
?.parsedData
|
|
: null;
|
|
const rcptrInfoData = file5?.file
|
|
? parsedResults.find((r) => r && r.fileName === file5.file?.name)
|
|
?.parsedData
|
|
: null;
|
|
|
|
if (!swtrmtFcltNm || !swtrmtFcltNm.trim()) {
|
|
setIsLoadingResult(false);
|
|
alert("하수처리시설명이 비어 있습니다. 시설을 다시 선택해주세요.");
|
|
return;
|
|
}
|
|
|
|
if (
|
|
!Array.isArray(matchingResultData) ||
|
|
matchingResultData.length === 0
|
|
) {
|
|
setIsLoadingResult(false);
|
|
alert(
|
|
"수용가-맨홀 매칭결과 파일에서 저장할 데이터를 읽지 못했습니다. 엑셀 헤더를 확인해주세요.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!Array.isArray(timePatternData) || timePatternData.length === 0) {
|
|
setIsLoadingResult(false);
|
|
alert(
|
|
"오수량 시간패턴 파일에서 저장할 데이터를 읽지 못했습니다. 엑셀 헤더를 확인해주세요.",
|
|
);
|
|
return;
|
|
}
|
|
// 새로운 JSON 데이터 생성
|
|
const jsonData = {
|
|
ctpvCd: ctpvCd !== "0" ? String(ctpvCd).trim() : null,
|
|
sggCd: sggCd !== "0" ? String(sggCd) : null, // 시군구코드 (선택값)
|
|
swtrmtFcltNm: swtrmtFcltNm ?? null,
|
|
aplcnYm: selectedDate, // 적용연월
|
|
drtwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환)
|
|
anlsInfo: input2, // 분석정보
|
|
rgtr: tokenManager.getToken(AUTH_USER_ID) || null, // 등록자 (사용자 ID)
|
|
rgtrNm: userInfo?.userName, // 등록자 (사용자 ID)
|
|
//atflCd: file1?.atflCd, // 첨부파일CD (오수관망도)
|
|
//atflId: file1?.atflId, // 첨부파일ID (오수관망도)
|
|
atflInpNm: file1?.file?.name, // 오수관망도 파일명
|
|
atflHrPttnNm: file3?.file?.name, // 오수량 시간패턴 파일명
|
|
atflMpngNm: file2?.file?.name, // 수용가-맨홀 매칭결과 파일명
|
|
atflOilqNm: file4?.file?.name || null, // 유량자료 파일명 (없을 수 있음)
|
|
tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과
|
|
tbSnHrPttn: timePatternData, // 오수량 시간패턴
|
|
tbSnOilqData: flowRateData, // 유량자료 (없을 수 있음)
|
|
tbSnRcptrInfo: rcptrInfoData, // 수용가 상수사용량 (없을 수 있음)
|
|
};
|
|
|
|
const jsonBlob = new Blob([JSON.stringify(jsonData)], {
|
|
type: "application/json",
|
|
});
|
|
const combinedAnalysisData = new FormData();
|
|
combinedAnalysisData.append("data", jsonBlob);
|
|
if (file1?.file) combinedAnalysisData.append("inpFile", file1.file);
|
|
|
|
await registFile(combinedAnalysisData);
|
|
setIsLoadingResult(false);
|
|
} catch (error) {
|
|
setIsLoadingResult(false);
|
|
console.error("침입수 분석 요청 처리 중 오류 발생:", error);
|
|
alert(
|
|
error instanceof Error && error.message.trim()
|
|
? error.message
|
|
: "파일 처리 중 오류가 발생했습니다.",
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<LoadingOverlay isLoading={isLoadingResult} />
|
|
<BaseLayout
|
|
titleName={{
|
|
mainTitle: "침입수 분석",
|
|
firstCardTitle: "침입수 분석 자료 입력",
|
|
}}
|
|
form={form}
|
|
onSubmitEvent={() => {}}
|
|
graphBtn={false}
|
|
resultViewBtn={"결과보기"}
|
|
analysisBtn={"침입수분석 실행"}
|
|
selectBox={{ title: "분석 대상지역 선택" }}
|
|
ctpvCd={ctpvCd}
|
|
sggCd={sggCd}
|
|
sggName={sggName}
|
|
selectedSwagePrcsFclt={selectedSwagePrcsFclt}
|
|
onSwagePrcsFcltChange={setSelectedSwagePrcsFclt}
|
|
setCtpvCd={setCtpvCd}
|
|
setSggCd={setSggCd}
|
|
setSggName={setSggName}
|
|
setSwtrmtFcltNm={setSwtrmtFcltNm}
|
|
defaultSwtrmtFcltNm={swtrmtFcltNm}
|
|
setIsLoading={setIsLoadingResult}
|
|
onResultView={() =>
|
|
openAnalysisResultMapView("inftwtResultPopup", alert)
|
|
}
|
|
onAnalysis={handleAnalysisClick}
|
|
isIr={true}
|
|
>
|
|
{[
|
|
<StandardFormSection
|
|
key="irsetting-layout-key"
|
|
title="분석 조건 및 자료"
|
|
columns={2}
|
|
>
|
|
<StandardFormField label="상수사용량 적용월" required columns={2}>
|
|
{/* 연월 선택 라이브러리로 수정 필요 */}
|
|
<DateMonthPicker
|
|
field={{
|
|
value: selectedDate, // yyyyMM 형식
|
|
onChange: (value: string) => {
|
|
setSelectedDate(value);
|
|
},
|
|
}}
|
|
selectClass="w-full"
|
|
startYear={1900} // 시작 연도 설정
|
|
/>
|
|
</StandardFormField>
|
|
<StandardFormField
|
|
label="수용가 상수사용량"
|
|
columns={2}
|
|
className="[&_.card]:!m-0 [&_.card]:!border-0 [&_.card]:!p-0"
|
|
>
|
|
<FileUploadForm
|
|
key={`file-upload-4-${formResetKey}`} // 폼 초기화를 위한 key
|
|
onFileSelect={(file, cd, id) =>
|
|
handleFileSelection(4, file, cd, id)
|
|
}
|
|
showButton={false}
|
|
showPreview={true}
|
|
accept={".xls, .xlsx, .xlsm, .xltx"}
|
|
fileIdx={4}
|
|
/>
|
|
</StandardFormField>
|
|
<StandardFormField
|
|
label="오수관망도"
|
|
required
|
|
columns={2}
|
|
className="[&_.card]:!m-0 [&_.card]:!border-0 [&_.card]:!p-0"
|
|
>
|
|
<FileUploadForm
|
|
key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key
|
|
onFileSelect={(file, cd, id) =>
|
|
handleFileSelection(0, file, cd, id)
|
|
}
|
|
showButton={false}
|
|
showPreview={true}
|
|
accept={".inp"}
|
|
fileIdx={0}
|
|
/>
|
|
</StandardFormField>
|
|
<StandardFormField
|
|
label="수용가-맨홀 매칭결과"
|
|
required
|
|
columns={2}
|
|
className="[&_.card]:!m-0 [&_.card]:!border-0 [&_.card]:!p-0"
|
|
>
|
|
<div className="flex items-center">
|
|
<FileUploadForm
|
|
key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key
|
|
onFileSelect={(file, cd, id) =>
|
|
handleFileSelection(1, file, cd, id)
|
|
}
|
|
showButton={false}
|
|
showPreview={true}
|
|
accept={".xls, .xlsx, .xlsm, .xltx"}
|
|
fileIdx={1}
|
|
/>
|
|
</div>
|
|
</StandardFormField>
|
|
<StandardFormField
|
|
label="오수량 시간패턴"
|
|
required
|
|
columns={2}
|
|
className="[&_.card]:!m-0 [&_.card]:!border-0 [&_.card]:!p-0"
|
|
>
|
|
<FileUploadForm
|
|
key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key
|
|
onFileSelect={(file, cd, id) =>
|
|
handleFileSelection(2, file, cd, id)
|
|
}
|
|
showButton={false}
|
|
showPreview={true}
|
|
accept={".xls, .xlsx, .xlsm, .xltx"}
|
|
fileIdx={2}
|
|
/>
|
|
</StandardFormField>
|
|
<StandardFormField label="오수전환율" required columns={2}>
|
|
<Input
|
|
value={input1}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
|
|
if (newValue === "") {
|
|
setInput1(newValue);
|
|
return;
|
|
}
|
|
|
|
// 숫자와 '.'만 허용하고, '.'은 최대 하나인지 확인
|
|
const isValidChars = /^[0-9]*\.?[0-9]*$/.test(newValue);
|
|
if (!isValidChars) return; // 유효하지 않은 문자 또는 여러 개의 '.' 포함
|
|
|
|
// '.' 기호가 포함된 경우, 처음이나 끝에 오면 안 됨
|
|
if (newValue.includes(".") && newValue.startsWith("."))
|
|
return; // '.'이 처음에는 올 수 없음
|
|
|
|
// 숫자 값으로 변환하여 범위 체크 (0 이상 1 이하)
|
|
const numericValue = parseFloat(newValue);
|
|
if (
|
|
isNaN(numericValue) ||
|
|
numericValue < 0 ||
|
|
numericValue > 1
|
|
)
|
|
return;
|
|
|
|
setInput1(newValue);
|
|
}}
|
|
className="w-full"
|
|
placeholder="예: 0.75"
|
|
/>
|
|
</StandardFormField>
|
|
<StandardFormField label="분석 정보" required columns={2}>
|
|
<Input
|
|
value={input2}
|
|
onChange={(e) => setInput2(e.target.value)}
|
|
className="w-full"
|
|
placeholder="세부 분석정보 입력"
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault(); // Enter 키 기본 동작 방지
|
|
handleAnalysisClick();
|
|
}
|
|
}}
|
|
/>
|
|
</StandardFormField>
|
|
<StandardFormField
|
|
label="유량자료"
|
|
columns={2}
|
|
className="[&_.card]:!m-0 [&_.card]:!border-0 [&_.card]:!p-0"
|
|
>
|
|
<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}
|
|
/>
|
|
</StandardFormField>
|
|
</StandardFormSection>,
|
|
]}
|
|
</BaseLayout>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default IrSetting;
|