feat:
- excelToJson.ts 에 날짜 변환 함수 추가 및 타입 수정 - UITable 레이아웃을 이용한 입력 가능 Table인 EditableFormTable.tsx 컴포넌트 구현 - RDII 분석 페이지 수정 및 JSON 데이터 생성, API 호출 기능 구현
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
]}
|
||||
|
||||
Reference in New Issue
Block a user