diff --git a/apps/sn/app/components/lib/utils/excelToJson.ts b/apps/sn/app/components/lib/utils/excelToJson.ts index c4a3a226d..2ed697139 100644 --- a/apps/sn/app/components/lib/utils/excelToJson.ts +++ b/apps/sn/app/components/lib/utils/excelToJson.ts @@ -1,35 +1,106 @@ export const TEST_UUID= '2d4b151d-1828-41e0-91d9-4cd28ea1cc7e' import { z } from "zod"; -// 수용가-맨홀 매칭결과 (아직 안함) +// "YYYY-MM-DDTHH:MM:SS" 형식으로 포맷팅 하는 함수 +function parseAndFormatDateTimeToISOCompatibleString( + dateInput: string, + timeInputForMDY: string | null, + inputType: 'YYYY-MM-DD_H:MM' | 'M/D/YYYY_H:MM' +): string | null { + let year: number, month: number, day: number, hours: number, minutes: number; + let datePartStr: string; + let timePartStr: string; + + if (inputType === 'YYYY-MM-DD_H:MM') { + const parts = String(dateInput).split(' '); + if (parts.length !== 2) { + console.warn(`Invalid dateTime format for ${inputType} (split space):`, dateInput); + return null; + } + datePartStr = parts[0] ?? ""; // "YYYY-MM-DD" + timePartStr = parts[1] ?? ""; // "H:MM" + + const dateParts = datePartStr.split('-'); + if (dateParts.length !== 3) { + console.warn(`Invalid date format for ${inputType} (datePart):`, datePartStr); + return null; + } + year = parseInt(dateParts[0] ?? '', 10); + month = parseInt(dateParts[1] ?? '', 10); + day = parseInt(dateParts[2] ?? '', 10); + } else if (inputType === 'M/D/YYYY_H:MM') { + datePartStr = String(dateInput); // "M/D/YYYY" + if (!timeInputForMDY) { + console.warn(`Time input is missing for ${inputType}:`, dateInput); + return null; + } + timePartStr = String(timeInputForMDY); // "H:MM" + + const dateParts = datePartStr.split('/'); + if (dateParts.length !== 3) { + console.warn(`Invalid date format for ${inputType} (datePart):`, datePartStr); + return null; + } + // Note: Order for M/D/YYYY is month, day, year + month = parseInt(dateParts[0] ?? '', 10); + day = parseInt(dateParts[1] ?? '', 10); + year = parseInt(dateParts[2] ?? '', 10); + } else { + console.warn("Invalid inputType for date parsing."); + return null; + } + + // 시간 파싱 (H:MM) - 공통 로직 + const timeParts = timePartStr.split(':'); + if (timeParts.length !== 2) { + console.warn(`Invalid time format (timePart):`, timePartStr); + return null; + } + hours = parseInt(timeParts[0] ?? '', 10); + minutes = parseInt(timeParts[1] ?? '', 10); + + if (isNaN(year) || isNaN(month) || isNaN(day) || isNaN(hours) || isNaN(minutes)) { + console.warn("Invalid parsed date or time numeric values:", { year, month, day, hours, minutes }); + return null; + } + + // "YYYY-MM-DDTHH:MM:SS" 형식으로 포맷팅 - 공통 로직 + const formattedYear = String(year).padStart(4, '0'); // 연도는 4자리로 보장 (M/D/YYYY의 경우) + const formattedMonth = String(month).padStart(2, '0'); + const formattedDay = String(day).padStart(2, '0'); + const formattedHours = String(hours).padStart(2, '0'); + const formattedMinutes = String(minutes).padStart(2, '0'); + // 초는 00으로 고정, 'T'를 사용하여 Date 생성자 호환성 확보 + return `${formattedYear}-${formattedMonth}-${formattedDay} ${formattedHours}:${formattedMinutes}:00`; +} + + +// 수용가-맨홀 매칭결과 (수정 필요) export const MatchingResultZod = z.object({ - anls_sn: z.number(), - mhl_mng_no: z.string(), // 일련번호 0 포함인지? number? string? - rcptr_no: z.string(), + anls_sn: z.number(), // 분석 일련번호 + mhl_mng_no: z.string(), // 맨홀 번호 (임시값이라 추후 수정 필요) + rcptr_no: z.string(), // 수용가 번호 }); - export type MatchingResultZodType = z.infer; - export const excelToMatchingResultData = (excelData: any) => { const MatchingResultData: MatchingResultZodType[] = excelData.map((item: any) => { return { - anls_sn: Number(item["NO"]), - mhl_mng_no: String(item["FTR_IDN"]), // 임시값이라 추후 수정 필요 + anls_sn: Number(item["NO"]), + mhl_mng_no: String(item["FTR_IDN"]), // (임시값이라 추후 수정 필요) rcptr_no: String(item["수용가"]), }; }).filter((el: any) => el !== undefined); return MatchingResultData; }; + // 시간패턴 export const timePatternZod = z.object({ - hr_id: z.number(), - rt: z.number().nullish(), + hr_id: z.number(), // 시간 + rt: z.number().nullish(), // 비율 }); - export type timePatternZodType = z.infer; - export const excelToTimePatternData = (excelData: any) => { const timePatternData: timePatternZodType[] = excelData.map((item: any) => { @@ -41,23 +112,59 @@ export const excelToTimePatternData = (excelData: any) => { return timePatternData; }; + // 유량자료 export const flowRateZod = z.object({ - ppl_mng_no: z.string(), - mtrng_dt: z.number(), // type 뭘로 해야할지 Date? timeStamp임 - oilq: z.number().nullish(), + ppl_mng_no: z.string(), // 관로번호 + mtrng_dt: z.string(), // 시간 + oilq: z.number().nullish(), // 유량(㎥/h) }); - export type flowRateZodType = z.infer; - -export const excelToflowRateData = (excelData: any) => { +export const excelToFlowRateData = (excelData: any) => { const flowRateData: flowRateZodType[] = excelData.map((item: any) => { + const dateTimeStr = String(item["시간"]); // "YYYY-MM-DD H:MM" 형식 (예: "2025-02-23 8:50") + + const formattedDateTime = parseAndFormatDateTimeToISOCompatibleString(dateTimeStr, null, 'YYYY-MM-DD_H:MM'); + + if (!formattedDateTime) return undefined; + + const dateObject = new Date(formattedDateTime); + const mtrngDtValue = !isNaN(dateObject.getTime()) ? dateObject.toISOString() : null; + + if (!mtrngDtValue) { + console.warn("Failed to create Date object for flowRateData:", formattedDateTime); + return undefined; + } return { ppl_mng_no: String(item["관로번호"]), - mtrng_dt: new Date(item["시간"]).toISOString(), // type 뭘로 해야할지 Date? timeStamp임 + mtrng_dt: mtrngDtValue, oilq: Number(item["유량(㎥/h)"]), }; }).filter((el: any) => el !== undefined); return flowRateData; +}; + + +// 강우자료 (수정 필요) +export const rainZod = z.object({ + mtrng_dt: z.string(), // 날짜 + 시간 + amrf: z.number().nullish(), // 강우량(mm) +}); +export type rainZodType = z.infer; +export const excelToRainData = (excelData: any) => { + const rainData: rainZodType[] = excelData.map((item: any) => { + const datePartStr = String(item["날짜"]); // "M/D/YYYY" 형식 (예: "5/9/2021") + const timePartStr = String(item["시간"]); // "H:MM" 형식 (예: "8:00") + + const formattedForRainZod = parseAndFormatDateTimeToISOCompatibleString(datePartStr, timePartStr, 'M/D/YYYY_H:MM'); + + if (!formattedForRainZod) return undefined; + + return { + mtrng_dt: formattedForRainZod, + amrf: Number(item["강우량(mm)"]), + }; + }).filter((el: any) => el !== undefined); + return rainData; }; \ No newline at end of file diff --git a/apps/sn/app/components/ui/EditableFormTable.tsx b/apps/sn/app/components/ui/EditableFormTable.tsx new file mode 100644 index 000000000..89fd8f005 --- /dev/null +++ b/apps/sn/app/components/ui/EditableFormTable.tsx @@ -0,0 +1,131 @@ +import { Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components/ui"; +import type { UseFormReturn, FieldArrayWithId, Path, FieldValues } from "react-hook-form"; +import { Controller } from "react-hook-form"; + +// 편집 가능한 테이블 헤더 항목 인터페이스 +export interface EditableTableHeadItem { + id: keyof TItem | 'response'; // 'response'는 직접적인 키가 아닐 수 있지만 특별히 처리될 수 있음 + title: string; + editable?: boolean; // 편집 가능 여부 + width?: string; // 컬럼 너비 추가 + align?: 'left' | 'center' | 'right'; // 정렬 + cellClassName?: string; // 셀 클래스 이름 + inputClassName?: string; // 입력 필드 클래스 이름 + inputProps?: React.ComponentProps; // Input 컴포넌트에 전달할 props (예: type="number") +} + +// EditableFormTable 컴포넌트 props 타입 정의 +type EditableFormTableProps< + TFormValues extends FieldValues, + TFieldArrayName extends Path, + TFieldArrayItem extends FieldValues = TFormValues[TFieldArrayName] extends (infer U)[] ? U extends FieldValues ? U : never : never +> = { + form: UseFormReturn; // react-hook-form의 form 객체 + name: TFieldArrayName; // 필드 배열 이름 (예: "unitFactors") + fields: FieldArrayWithId[]; // useFieldArray에서 반환된 필드 배열 + headlist: EditableTableHeadItem[]; // 테이블 헤더 설정 + tableClassName?: string; // 테이블 클래스 이름 + headerClassName?: string; // 헤더 클래스 이름 + bodyClassName?: string; // 바디 클래스 이름 + rowClassName?: string; // 행 클래스 이름 +}; + +// EditableFormTable 컴포넌트 +export function EditableFormTable< + TFormValues extends FieldValues, + TFieldArrayName extends Path, + TFieldArrayItem extends FieldValues = TFormValues[TFieldArrayName] extends (infer U)[] ? U extends FieldValues ? U : never : never +>({ + form, + name, + fields, + headlist, + tableClassName = "w-full", + headerClassName = "font-bold", + bodyClassName = 'text-center', + rowClassName = "", +}: EditableFormTableProps) { + return ( +
+ + + + {headlist.map((head) => ( + + {head.title} + + ))} + + + + {fields.map((rowItem, rowIndex) => ( + + {headlist.map((head) => { + const fieldName = `${name}.${rowIndex}.${String(head.id)}` as Path; + const cellValue = (rowItem as any)[head.id as string]; + + return ( + + {head.editable ? ( + { + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + + if (newValue === "") { + field.onChange(""); // 빈 문자열 허용 + return; + } + + // 숫자와 '.'만 허용 (단일 '.' 허용) + const isValidChars = /^[0-9]*\.?[0-9]*$/.test(newValue); + if (!isValidChars) { + return; // 유효하지 않은 문자 또는 여러 개의 '.' 포함 시 변경 무시 + } + + // '.' 기호가 포함된 경우, 처음에는 올 수 없음 (예: '.5' 방지, '0.5' 유도) + // 이 조건은 사용자의 요구사항에 따라 조정될 수 있습니다. + // 현재 로직은 '.5'와 같은 입력을 허용하지 않습니다. + if (newValue.includes('.') && newValue.startsWith('.')) { + // 만약 '.'만 입력된 경우 허용하려면 아래 주석 해제 + // if (newValue === '.') { + // field.onChange(newValue); + // return; + // } + return; + } + + field.onChange(newValue); + }; + + return ( + + ); + }} + /> + ) : ( + {String(cellValue ?? '')} + )} + + ); + })} + + ))} + +
+
+ ); +} \ No newline at end of file diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/ii/irsetting/page.tsx index a299e9337..084fef559 100644 --- a/apps/sn/app/ii/irsetting/page.tsx +++ b/apps/sn/app/ii/irsetting/page.tsx @@ -8,7 +8,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' -import { excelToflowRateData, excelToMatchingResultData, excelToTimePatternData } from '../../components/lib/utils/excelToJson' +import { excelToFlowRateData, excelToMatchingResultData, excelToTimePatternData } from '../../components/lib/utils/excelToJson' import BaseLayout from '../BaseLayout' const formSchema = z.object({ @@ -135,7 +135,7 @@ const IrSetting = () => { } }; - // 버튼 클릭 핸들러 + // 침입수 분석 실행 버튼 클릭 핸들러 const handleAnalysisClick = async () => { console.log(uploadedFilesData); const file1 = uploadedFilesData[0]; // 오수관망도 @@ -160,7 +160,7 @@ const IrSetting = () => { // 여기서는 File 객체를 받고, 내부적으로 ArrayBuffer로 변환하여 parser에 전달한다고 가정 { file: file2, parser: excelToMatchingResultData }, { file: file3, parser: excelToTimePatternData }, - { file: file4, parser: excelToflowRateData } + { file: file4, parser: excelToFlowRateData } // 오수관망도 파일은 파싱 대상이 아닐 수 있으므로, 필요에 따라 추가/제외 ]; @@ -207,11 +207,11 @@ const IrSetting = () => { sggCd: select3, // 시군구코드 swageTmplntCd: select4, // 하수처리장코드 swtrmtZoneCd: select5, // 하수처리구역코드 - aplcnYmd: selectedDate.toISOString(), // 적용연월 - dtrwSwtcRt: input1, // 오수전환율 + aplcnYmd: selectedDate.toISOString().substring(0, 7).replace("-", ""), // 적용연월 + dtrwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환) anlsInfo: input2, // 분석정보 - atflCd: file1?.atflCd, // 첨부파일CD - atflId: file1?.atflId, // 첨부파일ID + atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) + atflId: file1?.atflId, // 첨부파일ID (오수관망도) tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 tbSnHrPttn: timePatternData, // 오수량 시간패턴 tbSnOilqData: flowRateData, // 유량자료 @@ -255,7 +255,6 @@ const IrSetting = () => { showPreview={true} accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={1} /> - {/* */} { showPreview={true} accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={2} /> - {/* */} { showPreview={true} accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={3} /> - {/* */}
diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx index 94cb94f40..7abaa7814 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/ii/rdii/page.tsx @@ -1,57 +1,296 @@ "use client" -import { date, z } from "zod" -import { useForm, useFieldArray } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Input, Button, DatePicker } from "@common/components/ui" -import { UiTable } from "@common/business_components/ui" -import React, { useState } from "react" -import BaseLayout from "@/ii/BaseLayout" +import { postData } from '@/components/lib/api' +import FileUploadForm from "@/components/ui/fileUploadForm" import FormSection from "@/components/ui/formSection" +import BaseLayout from "@/ii/BaseLayout" +import { UiTable, filePicked } from "@common/business_components/ui" +import { Button, DatePicker, Input } from "@common/components/ui" +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { useFieldArray, useForm } from "react-hook-form" +import { z } from "zod" +import { excelToMatchingResultData, excelToRainData } from '../../components/lib/utils/excelToJson' +import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/EditableFormTable'; +import { Label } from '@radix-ui/react-label' + // TODO: 컴포넌트 호출 ~ 필요 코드 수정해야댐 // zod 스키마 정의 const formSchema = z.object({ - region: z.string(), - rdiiArea: z.string(), - rtkSource: z.string(), - unitFactors: z.array(z.object({ + enoCd: z.string(), // 환경청코드 + ctpvCd: z.string(), // 시도코드 + sggCd: z.string(), // 시군구코드 + swageTmplntCd: z.string(), // 하수처리장코드 + swtrmtZoneCd: z.string(), // 하수처리구역코드 + rtkSource: z.string(), // RTK계수 출처 + rdiiArea: z.string(), // RDII 기여면적 + unitFactors: z.array(z.object({ // 단위도 계수 response: z.string(), R: z.string(), T: z.string(), K: z.string(), })), - initialDepths: z.array(z.object({ + initialDepths: z.array(z.object({ // 초기 차단 깊이 response: z.string(), Dmax: z.string(), Drec: z.string(), Do: z.string(), })), - suyeonga: z.string(), - rate: z.string(), - date: z.coerce.date(), - rain: z.string(), + // osumap: z.instanceof(File).optional(), // 오수관망도 + // matching: z.instanceof(File).optional(), // 수용가-맨홀 매칭결과 + aplcnYm: z.string(), // 상수사용량 적용기간 (YYYYMM 형식) + rate: z.string(), // 오수전환율 + // rain: z.instanceof(File).optional(), // 강우자료 + analysisInfo: z.string().optional(), // 분석 정보 + date: z.date(), + // date: z.coerce.date(), }) +type FormSchemaType = z.infer; + +// 기본 폼 값 정의 +const defaultFormValues: FormSchemaType = { + enoCd: "", + ctpvCd: "", + sggCd: "", + swageTmplntCd: "", + swtrmtZoneCd: "", + rtkSource: "", + rdiiArea: "", // 문자열로 기본값 설정 + unitFactors: [ + { response: "Short-Term", R: "", T: "", K: "" }, // 문자열로 기본값 설정 + { response: "Medium-Term", R: "", T: "", K: "" }, + { response: "Long-Term", R: "", T: "", K: "" }, + ], + initialDepths: [ + { response: "Short-Term", Dmax: "", Drec: "", Do: "" }, // 문자열로 기본값 설정 + { response: "Medium-Term", Dmax: "", Drec: "", Do: "" }, + { response: "Long-Term", Dmax: "", Drec: "", Do: "" }, + ], + // osumap: undefined, + // matchingResultFile: undefined, + aplcnYm: "", + rate: "", // 문자열로 기본값 설정 + // rainFile: undefined, + analysisInfo: "", + date: new Date(), +}; + +// 각 파일 업로드 폼의 데이터를 저장할 인터페이스 정의 +interface UploadedFileInfo { + file: File | null; + atflCd: string | null; + atflId: string | null; + // 필요하다면 파일 이름 등 추가 정보 저장 가능 + // fileName: string; +} + +// 초기 파일 정보 상태 +const initialFileInfo: UploadedFileInfo = { file: null, atflCd: null, atflId: null }; + export default function RTKForm() { - const form = useForm>({ + const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 + + // FileUploadForm에서 전달받은 파일 정보를 저장할 상태 배열 + // 각 인덱스는 FileUploadForm의 fileIdx와 매칭됨 + // 예: 0: 오수관망도, 1: 수용가-맨홀, 2: 강우자료 + const [uploadedFilesData, setUploadedFilesData] = useState([ + initialFileInfo, // 0: 오수관망도 + initialFileInfo, // 1: 수용가-맨홀 매칭결과 + initialFileInfo, // 2: 강우자료 + ]); + + // form 인스턴스 생성 + const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: { - region: "", - rdiiArea: "", - rtkSource: "", - unitFactors: [ - { response: "Short-Term", R: "", T: "", K: "" }, - { response: "Medium-Term", R: "", T: "", K: "" }, - { response: "Long-Term", R: "", T: "", K: "" }, - ], - initialDepths: [ - { response: "Short-Term", Dmax: "", Drec: "", Do: "" }, - { response: "Medium-Term", Dmax: "", Drec: "", Do: "" }, - { response: "Long-Term", Dmax: "", Drec: "", Do: "" }, - ], + defaultValues: defaultFormValues + }); + // const form = useForm>({ + // resolver: zodResolver(formSchema), + // defaultValues: { + // enoCd: "", // 환경청코드 + // ctpvCd: "", // 시도코드 + // sggCd: "", // 시군구코드 + // swageTmplntCd: "", // 하수처리장코드 + // swtrmtZoneCd: "", // 하수처리구역코드 + // rtkSource: "", // RTK계수 출처 + // rdiiArea: 0, // RDII 기여면적 + // unitFactors: [ + // { response: "Short-Term", R: 0, T: 0, K: 0 }, + // { response: "Medium-Term", R: 0, T: 0, K: 0 }, + // { response: "Long-Term", R: 0, T: 0, K: 0 }, + // ], + // initialDepths: [ + // { response: "Short-Term", Dmax: 0, Drec: 0, Do: 0 }, + // { response: "Medium-Term", Dmax: 0, Drec: 0, Do: 0 }, + // { response: "Long-Term", Dmax: 0, Drec: 0, Do: 0 }, + // ], + // // osumap: undefined, + // // matching: undefined, + // aplcnYm: "", + // rate: 0, + // // rain: undefined, + // analysisInfo: "", + // } + // }); + + // 폼 초기화 함수 + const resetAllForms = () => { + form.reset(defaultFormValues); // 폼 값 초기화 + setUploadedFilesData([ // 파일 업로드 상태 초기화 (3개 파일) + initialFileInfo, + initialFileInfo, + initialFileInfo, + ]); + 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; + }); + console.log(`Page: File data received for form index ${index}`, { fileName: selectedFile?.name, cd, id }); + }; + + const registFile = async (analysisData: any) => { + console.log("Submitting combined analysis data to backend"); + // 추후 API 엔드포인트에 맞게 URL 수정 필요 + const data = await postData('v1/rdiiAnls', analysisData); + if (data.ok) { + alert("분석 데이터가 성공적으로 제출되었습니다."); + resetAllForms(); // 성공 시 폼 초기화 함수 호출 + } else { + alert("분석 데이터 제출 중 오류가 발생했습니다."); + // resetAllForms(); } - }) + }; + + // 침입수 분석 실행 버튼 클릭 핸들러 + const handleAnalysisClick = async () => { + const formData = form.getValues(); + console.log(uploadedFilesData); + const file1 = uploadedFilesData[0]; // 오수관망도 + const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과 + const file3 = uploadedFilesData[2]; // 강우자료 + + console.log("오수관망도 정보:", file1?.file); + console.log("수용가-맨홀 매칭결과 정보:", file2); + console.log("강우자료:", file3); + + // 모든 필수 파일이 선택되었는지 확인 + if (!file1?.file || !file2?.file || !file3?.file) { + alert('모든 분석용 파일을 선택해주세요.'); + return; + } + + const filePickers = [ + { file: file2, parser: excelToMatchingResultData }, + { file: file3, parser: excelToRainData }, + ]; + + try { + const parsedResults = await Promise.all( + filePickers.map(async ({ file, parser }) => { + if (!file || !file.file) return null; // fileInfo 객체 및 file 객체 null 체크 + + // filePicked 함수에 File 객체와 headerNum 전달 + 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 함수로 처리 + // parser 함수가 filePicked가 반환하는 JSON 형식을 직접 처리한다고 가정 + 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 rainData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData; + + // 새로운 JSON 데이터 생성 + const combinedAnalysisData = { + enoCd: "", // 환경청코드 + aplcnYm: formData.date.toISOString().substring(0, 7).replace("-", ""), // 적용연월 (YYYYMM 형식) + ctpvCd: "", // 시도코드 + sggCd: "", // 시군구코드 + swageTmplntCd: "", // 하수처리장코드 + swtrmtZoneCd: "", // 하수처리구역코드 + atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) + atflId: file1?.atflId, // 첨부파일ID (오수관망도) + cntbArea: parseFloat("rdiiArea") || null, // RDII 기여면적(ha) + srcNm: "", // 출처이름 (RTK계수 출처) + + shortPrdR: parseFloat(formData.unitFactors[0]?.R !== undefined ? String(formData.unitFactors[0]?.R) : "0") || null, // 단기간R + shortPrdT: parseFloat(formData.unitFactors[0]?.T !== undefined ? String(formData.unitFactors[0]?.T) : "0") || null, // 단기간T + shortPrdK: parseFloat(formData.unitFactors[0]?.K !== undefined ? String(formData.unitFactors[0]?.K) : "0") || null, // 단기간K + + mdPrdR: parseFloat(formData.unitFactors[1]?.R !== undefined ? String(formData.unitFactors[1]?.R) : "0") || null, // 중간기간R + mdPrdT: parseFloat(formData.unitFactors[1]?.T !== undefined ? String(formData.unitFactors[1]?.T) : "0") || null, // 중간기간T + mdPrdK: parseFloat(formData.unitFactors[1]?.K !== undefined ? String(formData.unitFactors[1]?.K) : "0") || null, // 중간기간K + + longPrdR: parseFloat(formData.unitFactors[2]?.R !== undefined ? String(formData.unitFactors[2]?.R) : "0") || null, // 장기간R + longPrdT: parseFloat(formData.unitFactors[2]?.T !== undefined ? String(formData.unitFactors[2]?.T) : "0") || null, // 장기간T + longPrdK: parseFloat(formData.unitFactors[2]?.K !== undefined ? String(formData.unitFactors[2]?.K) : "0") || null, // 장기간K + + shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax !== undefined ? String(formData.initialDepths[0]?.Dmax) : "0") || null, // 단기간최대깊이 + shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec !== undefined ? String(formData.initialDepths[0]?.Drec) : "0") || null, // 단기간회복비율 + shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do !== undefined ? String(formData.initialDepths[0]?.Do) : "0") || null, // 단기간시작깊이 + + mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax !== undefined ? String(formData.initialDepths[1]?.Dmax) : "0") || null, // 중간기간최대깊이 + mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec !== undefined ? String(formData.initialDepths[1]?.Drec) : "0") || null, // 중간기간회복비율 + mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do !== undefined ? String(formData.initialDepths[1]?.Do) : "0") || null, // 중간기간시작깊이 + + longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax !== undefined ? String(formData.initialDepths[2]?.Dmax) : "0") || null, // 장기간최대깊이 + longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec !== undefined ? String(formData.initialDepths[2]?.Drec) : "0") || null, // 장기간회복비율 + longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do !== undefined ? String(formData.initialDepths[2]?.Do) : "0") || null, // 장기간시작깊이 + + dtrwSwtcRt: parseFloat(formData.rate !== undefined ? String(formData.rate) : "0") || null, // 오수전환율 + anlsInfo: "", // 분석정보 + + tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 + tbSnRainData: rainData, // 강우자료 + }; + + console.log("통합 분석 데이터 (JSON):", combinedAnalysisData); + + // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. + await registFile(combinedAnalysisData); + + } catch (error) { + console.error("파일 파싱 중 오류 발생:", error); + alert("파일 처리 중 오류가 발생했습니다."); + return; + } + } const { fields: unitFactorFields } = useFieldArray({ control: form.control, @@ -67,88 +306,107 @@ export default function RTKForm() { console.log("✅ 제출 데이터:", data) } - // 단위도 계수 테이블 설정 - const unitFactorHeadlist = [ - { id: 'response', title: 'Response' }, - { id: 'R', title: 'R' }, - { id: 'T', title: 'T' }, - { id: 'K', title: 'K' } - ] + // 단위도 계수 테이블 설정 (EditableTableHeadItem 타입 사용 및 id 수정) + const unitFactorHeadlist: EditableTableHeadItem[] = [ + { id: 'response', title: 'Response', editable: false, width: '150px' }, // width 속성 추가 + { id: 'R', title: 'R', editable: true, inputProps: { type: 'text' } }, + { id: 'T', title: 'T', editable: true, inputProps: { type: 'text' } }, + { id: 'K', title: 'K', editable: true, inputProps: { type: 'text' } } + ]; + + // 초기 차단 깊이 테이블 설정 (EditableTableHeadItem 타입 사용) + const initialDepthHeadlist: EditableTableHeadItem[] = [ + { id: 'response', title: 'Response', editable: false, width: '150px' }, // width 속성 추가 + { id: 'Dmax', title: 'Dmax', editable: true, inputProps: { type: 'text' } }, + { id: 'Drec', title: 'Drec', editable: true, inputProps: { type: 'text' } }, + { id: 'Do', title: 'Do', editable: true, inputProps: { type: 'text' } } + ]; - // 초기 차단 깊이 테이블 설정 - const initialDepthHeadlist = [ - { id: 'response', title: 'Response' }, - { id: 'Dmax', title: 'Dmax' }, - { id: 'Drec', title: 'Drec' }, - { id: 'Do', title: 'Do' } - ] // 테이블 페이지 변경 핸들러 const handlePageChange = (page: number) => { console.log('Page changed:', page) } - const [selectedDate, setSelectedDate] = useState(new Date()) + return ( { }} graphBtn={false} > - {[
+ {[
+
+ + + + + +
- -
-
-
○ 단위도 계수
- ○ 단위도 계수 + - {null} - + tableClassName="w-[400px]" + />
-
○ 초기 차단 깊이
- ○ 초기 차단 깊이 + - {null} - + tableClassName="w-[400px]" + />
- + handleFileSelection(0, file, cd, id)} + showButton={false} + showPreview={true} + accept={""} + fileIdx={0} />
- + handleFileSelection(1, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={1} />
- { if (date) setSelectedDate(date) }} mode={"single"} /> + { + if (date) { + form.setValue("date", date, { shouldValidate: true }); + } + }} + mode={"single"} + /> + + + -
- + handleFileSelection(2, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={2} />
- + + +
- +
]}