- excelToJson.ts 에 날짜 변환 함수 추가 및 타입 수정
- UITable 레이아웃을 이용한 입력 가능 Table인 EditableFormTable.tsx 컴포넌트 구현
- RDII 분석 페이지 수정 및 JSON 데이터 생성, API 호출 기능 구현
This commit is contained in:
swcho
2025-05-28 11:07:04 +09:00
parent c8be143b7c
commit a6e3731289
4 changed files with 610 additions and 117 deletions
+126 -19
View File
@@ -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<typeof MatchingResultZod>;
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<typeof timePatternZod>;
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<typeof flowRateZod>;
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<typeof rainZod>;
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;
};
@@ -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<TItem extends FieldValues> {
id: keyof TItem | 'response'; // 'response'는 직접적인 키가 아닐 수 있지만 특별히 처리될 수 있음
title: string;
editable?: boolean; // 편집 가능 여부
width?: string; // 컬럼 너비 추가
align?: 'left' | 'center' | 'right'; // 정렬
cellClassName?: string; // 셀 클래스 이름
inputClassName?: string; // 입력 필드 클래스 이름
inputProps?: React.ComponentProps<typeof Input>; // Input 컴포넌트에 전달할 props (예: type="number")
}
// EditableFormTable 컴포넌트 props 타입 정의
type EditableFormTableProps<
TFormValues extends FieldValues,
TFieldArrayName extends Path<TFormValues>,
TFieldArrayItem extends FieldValues = TFormValues[TFieldArrayName] extends (infer U)[] ? U extends FieldValues ? U : never : never
> = {
form: UseFormReturn<TFormValues>; // react-hook-form의 form 객체
name: TFieldArrayName; // 필드 배열 이름 (예: "unitFactors")
fields: FieldArrayWithId<TFieldArrayItem, any, "id">[]; // useFieldArray에서 반환된 필드 배열
headlist: EditableTableHeadItem<TFieldArrayItem>[]; // 테이블 헤더 설정
tableClassName?: string; // 테이블 클래스 이름
headerClassName?: string; // 헤더 클래스 이름
bodyClassName?: string; // 바디 클래스 이름
rowClassName?: string; // 행 클래스 이름
};
// EditableFormTable 컴포넌트
export function EditableFormTable<
TFormValues extends FieldValues,
TFieldArrayName extends Path<TFormValues>,
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<TFormValues, TFieldArrayName, TFieldArrayItem>) {
return (
<div className={`card !m-[1.2rem] !p-0r`}>
<Table variant="secondary" className={tableClassName}>
<TableHeader className={headerClassName}>
<TableRow>
{headlist.map((head) => (
<TableHead
key={String(head.id)}
className={head.align ? `text-${head.align}` : 'text-center'}
style={{ width: head.width }} // head.width 스타일 적용
>
{head.title}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody className={bodyClassName}>
{fields.map((rowItem, rowIndex) => (
<TableRow key={rowItem.id} className={rowClassName}>
{headlist.map((head) => {
const fieldName = `${name}.${rowIndex}.${String(head.id)}` as Path<TFormValues>;
const cellValue = (rowItem as any)[head.id as string];
return (
<TableCell key={`${rowItem.id}-${String(head.id)}`} className={`${head.align ? `text-${head.align}` : 'text-center'} ${head.cellClassName || ''}`}>
{head.editable ? (
<Controller
name={fieldName}
control={form.control}
render={({ field }) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<Input
{...field} // field의 value, onBlur, name, ref 등을 전달
onChange={handleChange} // 커스텀 handleChange 사용
className={`h-8 w-full text-center ${head.inputClassName || ''}`}
{...head.inputProps} // 기존 inputProps (예: type="text")
// type="text"를 명시적으로 사용하거나 head.inputProps에서 설정하는 것이 좋습니다.
// type="number"와 함께 사용 시 브라우저 기본 동작과 충돌할 수 있습니다.
/>
);
}}
/>
) : (
<span>{String(cellValue ?? '')}</span>
)}
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
+7 -10
View File
@@ -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} />
{/* <Input id="fileNm" ref={fileInputRef1} type="file" accept=".xls, .xlsx, .xlsm, .xltx" /> */}
</FormSection>
<FormSection name='pattern' form={form} titleNm='▷ 오수량 시간패턴'>
<FileUploadForm
@@ -265,7 +264,6 @@ const IrSetting = () => {
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={2} />
{/* <Input id="fileNm" ref={fileInputRef2} type="file" accept=".xls, .xlsx, .xlsm, .xltx" /> */}
</FormSection>
<FormSection
name='inputdate'
@@ -323,7 +321,6 @@ const IrSetting = () => {
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={3} />
{/* <Input id="fileNm" ref={fileInputRef3} type="file" accept=".xls, .xlsx, .xlsm, .xltx" /> */}
</FormSection>
<div className="flex justify-center mt-1">
<Button type="submit" onClick={handleAnalysisClick}>침입수분석 실행</Button>
+346 -88
View File
@@ -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<typeof formSchema>;
// 기본 폼 값 정의
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<z.infer<typeof formSchema>>({
const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태
// FileUploadForm에서 전달받은 파일 정보를 저장할 상태 배열
// 각 인덱스는 FileUploadForm의 fileIdx와 매칭됨
// 예: 0: 오수관망도, 1: 수용가-맨홀, 2: 강우자료
const [uploadedFilesData, setUploadedFilesData] = useState<UploadedFileInfo[]>([
initialFileInfo, // 0: 오수관망도
initialFileInfo, // 1: 수용가-맨홀 매칭결과
initialFileInfo, // 2: 강우자료
]);
// form 인스턴스 생성
const form = useForm<FormSchemaType>({
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<z.infer<typeof formSchema>>({
// 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<FormSchemaType['unitFactors'][number]>[] = [
{ 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<FormSchemaType['initialDepths'][number]>[] = [
{ 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 (
<BaseLayout searchBox={{ title: '분석 대장지역 선택' }} titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} form={form} onSubmitEvent={() => { }} graphBtn={false} >
{[<div className="space-y-[1.2rem]" key="form-section">
{[<div className="space-y-[1.2rem] overflow-x-auto" key="form-section">
<div className="grid grid-cols-2 gap-4">
<FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처">
<Input {...form.register("rtkSource")} className="h-10 text-center text-xl w-[150px]" />
</FormSection>
<FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)">
<Input {...form.register("rdiiArea")} className="h-10 text-center text-xl w-[150px]" />
</FormSection></div>
<div className="grid grid-cols-2 gap-4">
<FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처"><Input
className="h-10 text-center text-xl w-[150px]"
placeholder=""
/></FormSection>
<FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)"><Input
className="h-10 text-center text-xl w-[150px]"
placeholder=""
/></FormSection></div>
<div className="grid grid-cols-2">
<div>
<div className="mb-[1.2rem] text-xl font-medium">○ 단위도 계수</div>
<UiTable
headName=""
className="w-[400px]"
tableData={unitFactorFields}
<Label className="mb-2 text-xl font-medium">○ 단위도 계수</Label>
<EditableFormTable
form={form}
name="unitFactors"
fields={unitFactorFields}
headlist={unitFactorHeadlist}
pageSize={10}
total={0}
onPageChange={handlePageChange}
cellClick={undefined}
>
{null}
</UiTable>
tableClassName="w-[400px]"
/>
</div>
<div>
<div className="mb-[1.2rem] text-xl font-medium">○ 초기 차단 깊이</div>
<UiTable
headName=""
className="w-[400px]"
tableData={initialDepthFields}
<Label className="mb-[1.2rem] text-xl font-medium">○ 초기 차단 깊이</Label>
<EditableFormTable
form={form}
name="initialDepths"
fields={initialDepthFields}
headlist={initialDepthHeadlist}
pageSize={10}
total={0}
onPageChange={handlePageChange}
cellClick={undefined}
>
{null}
</UiTable>
tableClassName="w-[400px]"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<FormSection name="suyeonga" form={form} titleNm="○ 오수관망도"><div className="w-[150px]">
<Input id="fileNm" type="file" />
<FileUploadForm
key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)}
showButton={false}
showPreview={true}
accept={""}
fileIdx={0} />
</div></FormSection>
<FormSection name="regopm" form={form} titleNm="○ 수용가-맨홀 매칭결과"><div className="w-[150px]">
<Input id="fileNm2" type="file" />
<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></FormSection>
<FormSection name='date' form={form} titleNm='○ 상수사용량 적용기간'>
<DatePicker selected={selectedDate} onSelect={(date: Date | undefined) => { if (date) setSelectedDate(date) }} mode={"single"} />
<DatePicker
selected={form.watch("date")}
onSelect={(date: Date | undefined) => {
if (date) {
form.setValue("date", date, { shouldValidate: true });
}
}}
mode={"single"}
/>
</FormSection>
<FormSection name='rate' form={form} titleNm='○ 오수전환율'>
<Input {...form.register("rate")} className="h-[3.2rem] text-center text-xl w-[150px]" />
</FormSection>
<FormSection name='rate' form={form} titleNm='○ 오수전환율'><Input className="h-[3.2rem] text-center text-xl w-[150px]" /></FormSection>
<FormSection name="rain" form={form} titleNm="○ 강우자료"><div className="w-[150px]">
<Input id="fileNm3" type="file" />
<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} />
</div></FormSection>
<FormSection name='rate' form={form} titleNm='○ 분석 정보'><Input className="h-[3.2rem] text-center text-xl w-[150px]" /></FormSection>
<FormSection name='rate' form={form} titleNm='○ 분석 정보'>
<Input {...form.register("analysisInfo")} className="h-[3.2rem] text-center text-xl w-[150px]" />
</FormSection>
</div>
<div className="flex justify-end mt-1">
<Button type="submit">RDII분석 실행</Button>
<Button type="submit" onClick={handleAnalysisClick}>RDII분석 실행</Button>
</div>
</div>
]}