Merge commit 'c8f633be07112bf29237b10737a548547a6b8566' into iipbs/sn/oi
This commit is contained in:
@@ -43,6 +43,9 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const accessToken = tokenManager.getToken(CLAIM_NAME);
|
||||
const tokenId = tokenManager.getToken(AUTH_USER_ID);
|
||||
|
||||
//TODO 삭제
|
||||
console.log(accessToken)
|
||||
|
||||
const response = await axios.get<IUserInfo>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ interface SidoSigunguSelectBoxProps {
|
||||
}
|
||||
|
||||
const sidoList: { id: number, name: string }[] = [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
{ id: 26, name: '부산광역시' },
|
||||
{ id: 28, name: '인천광역시' },
|
||||
{ id: 30, name: '대전광역시' },
|
||||
@@ -29,19 +29,19 @@ const sidoList: { id: number, name: string }[] = [
|
||||
];
|
||||
const sigunguMap: Record<number, { id: number; name: string }[]> = {
|
||||
26: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
// ...부산산광역시의 시군구 추가
|
||||
],
|
||||
28: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
// ...인천광역시의 시군구 추가
|
||||
],
|
||||
30: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
// ...대전광역시의 시군구 추가
|
||||
],
|
||||
41: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
{ id: 41820, name: '가평군' },
|
||||
{ id: 41550, name: '안성시' },
|
||||
{ id: 41830, name: '양평군' },
|
||||
@@ -54,19 +54,19 @@ const sigunguMap: Record<number, { id: number; name: string }[]> = {
|
||||
// ...경기도의 시군구 추가
|
||||
],
|
||||
42: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
{ id: 42170, name: '동해시' },
|
||||
{ id: 42110, name: '춘천시' },
|
||||
{ id: 42720, name: '홍천군' },
|
||||
// ...강원도의 시군구 추가
|
||||
],
|
||||
44: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
{ id: 44770, name: '서천군' },
|
||||
// ...충청남도의 시군구 추가
|
||||
],
|
||||
45: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
{ id: 45130, name: '군산시' },
|
||||
{ id: 45190, name: '남원시' },
|
||||
{ id: 45730, name: '무주군' },
|
||||
@@ -79,7 +79,7 @@ const sigunguMap: Record<number, { id: number; name: string }[]> = {
|
||||
// ...전라북도의 시군구 추가
|
||||
],
|
||||
46: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
{ id: 46810, name: "강진군" },
|
||||
{ id: 46770, name: "고흥군" },
|
||||
{ id: 46720, name: "곡성군" },
|
||||
@@ -95,7 +95,7 @@ const sigunguMap: Record<number, { id: number; name: string }[]> = {
|
||||
// ...전라남도의 시군구 추가
|
||||
],
|
||||
47: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
{ id: 47130, name: "경주시" },
|
||||
{ id: 47830, name: "고령군" },
|
||||
{ id: 47190, name: "구미시" },
|
||||
@@ -116,7 +116,7 @@ const sigunguMap: Record<number, { id: number; name: string }[]> = {
|
||||
// ...경상북도의 시군구 추가
|
||||
],
|
||||
48: [
|
||||
{ id: 0, name: '선택' },
|
||||
{ id: 0, name: '전체' },
|
||||
{ id: 48310, name: "거제시" },
|
||||
{ id: 48250, name: "김해시" },
|
||||
{ id: 48270, name: "밀양시" },
|
||||
@@ -134,7 +134,7 @@ export function SidoSigunguSelectBox({
|
||||
className = "",
|
||||
}: SidoSigunguSelectBoxProps) {
|
||||
// 선택된 시도에 따라 시군구 리스트를 가져옴
|
||||
const sigunguList = useMemo(() => sigunguMap[sido] || [{ id: 0, name: '선택' }], [sido]);
|
||||
const sigunguList = useMemo(() => sigunguMap[sido] || [{ id: 0, name: '전체' }], [sido]);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center ${className}`}>
|
||||
@@ -150,7 +150,7 @@ export function SidoSigunguSelectBox({
|
||||
className="w-[150px] mr-8"
|
||||
selectClass="rounded-lg bg-white"
|
||||
label="시군구"
|
||||
selectArray={sidoList.map(opt => ({ val: opt.id, text: opt.name }))}
|
||||
selectArray={sigunguList.map(opt => ({ val: opt.id, text: opt.name }))}
|
||||
value={sigungu}
|
||||
onSelectValue={val => onSigunguChange(Number(val))}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,7 @@ interface FormSectionProps {
|
||||
|
||||
const FormSection = ({ children, titleNm, form, name }: FormSectionProps) => {
|
||||
return (
|
||||
<div className="grid grid-cols-[130px_1fr] items-center">
|
||||
<div className="grid grid-cols-[200px_1fr] items-center">
|
||||
<div className="text-xl font-medium">{titleNm}</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -65,16 +65,16 @@ const BaseLayout = ({ children, titleName, form, onSubmitEvent, graphBtn = false
|
||||
<SelectBox2 label={searchBox?.labelNm?.fourth ?? ''} selectArray={[{ text: '양평하수처리장', val: '1' }]} className="col-span-3" onSelectValue={() => { }} selectClass='' />
|
||||
</SearchFormLeft>
|
||||
</div>
|
||||
{searchBox?.searchChk ? <SearchFormRight><Button>조회</Button></SearchFormRight> : null}
|
||||
{searchBox?.searchChk ? <SearchFormRight><Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm">조회</Button></SearchFormRight> : null}
|
||||
</div>
|
||||
</ListDiv>
|
||||
<ListDiv>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3>
|
||||
<div>
|
||||
{graphBtn ? <Button className="!mr-[1.2rem]">다운로드</Button> : null}
|
||||
{graphBtn ? <Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size='sm'>다운로드</Button> : null}
|
||||
{/* 그래프 있을 시 */}
|
||||
{graphBtn ? <Button className="!mr-[1.2rem]">삭제</Button> : null}
|
||||
{graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size='sm'>삭제</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
@@ -91,9 +91,9 @@ const BaseLayout = ({ children, titleName, form, onSubmitEvent, graphBtn = false
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-medium">{titleName?.secondCardTitle}</h3>
|
||||
<div>
|
||||
{graphBtn ? <Button className="!mr-[1.2rem]">다운로드</Button> : null}
|
||||
{graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size='sm'>다운로드</Button> : null}
|
||||
{/* 그래프 있을 시 */}
|
||||
{graphBtn ? <Button className="!mr-[1.2rem]">삭제</Button> : null}
|
||||
{graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size='sm'>삭제</Button> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card !m-[1.2rem]">{children?.[1]}</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({
|
||||
@@ -95,11 +95,6 @@ const IrSetting = () => {
|
||||
setSelect4('');
|
||||
setSelect5('');
|
||||
setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경
|
||||
// FileUploadForm 내부의 파일명 표시 등을 초기화하려면
|
||||
// FileUploadForm 컴포넌트 자체에 reset 로직 또는 key 변경을 통한 재마운트 전략이 필요할 수 있습니다.
|
||||
// 현재 구조에서는 FileUploadForm의 내부 상태를 직접 초기화하기 어렵습니다.
|
||||
// 가장 간단한 방법은 FileUploadForm에 key prop을 주고, 초기화 시 key를 변경하여 강제로 재렌더링하는 것입니다.
|
||||
// 예: <FileUploadForm key={formResetKey} ... /> setFormResetKey(prev => prev + 1)
|
||||
};
|
||||
|
||||
// FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수
|
||||
@@ -135,7 +130,7 @@ const IrSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 버튼 클릭 핸들러
|
||||
// 침입수 분석 실행 버튼 클릭 핸들러
|
||||
const handleAnalysisClick = async () => {
|
||||
console.log(uploadedFilesData);
|
||||
const file1 = uploadedFilesData[0]; // 오수관망도
|
||||
@@ -160,7 +155,7 @@ const IrSetting = () => {
|
||||
// 여기서는 File 객체를 받고, 내부적으로 ArrayBuffer로 변환하여 parser에 전달한다고 가정
|
||||
{ file: file2, parser: excelToMatchingResultData },
|
||||
{ file: file3, parser: excelToTimePatternData },
|
||||
{ file: file4, parser: excelToflowRateData }
|
||||
{ file: file4, parser: excelToFlowRateData }
|
||||
// 오수관망도 파일은 파싱 대상이 아닐 수 있으므로, 필요에 따라 추가/제외
|
||||
];
|
||||
|
||||
@@ -207,11 +202,15 @@ 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 (오수관망도)
|
||||
atflInpNm: file1?.file?.name, // 오수관망도 파일명
|
||||
atflHrPttnNm: file3?.file?.name, // 오수량 시간패턴 파일명
|
||||
atflMpngNm: file2?.file?.name, // 수용가-맨홀 매칭결과 파일명
|
||||
atflOilqNm: file4?.file?.name, // 유량자료 파일명
|
||||
tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과
|
||||
tbSnHrPttn: timePatternData, // 오수량 시간패턴
|
||||
tbSnOilqData: flowRateData, // 유량자료
|
||||
@@ -255,7 +254,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 +263,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 +320,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>
|
||||
|
||||
+323
-94
@@ -1,57 +1,266 @@
|
||||
"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 { filePicked } from "@common/business_components/ui"
|
||||
import { Button, DatePicker, Input } from "@common/components/ui"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Label } from '@radix-ui/react-label'
|
||||
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'
|
||||
|
||||
|
||||
// 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(),
|
||||
})
|
||||
// zod 스키마 타입 추론
|
||||
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;
|
||||
}
|
||||
|
||||
// 초기 파일 정보 상태
|
||||
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와 매칭됨
|
||||
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 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 (오수관망도)
|
||||
atflInpNm: file1?.file?.name || "", // 오수관망도 파일명
|
||||
atflMpngNm: file2?.file?.name || "", // 수용가-맨홀 매칭결과 파일명
|
||||
atflRainNm: file3?.file?.name || "", // 강우자료 파일명
|
||||
cntbArea: parseFloat("rdiiArea") || null, // RDII 기여면적(ha)
|
||||
srcNm: "", // 출처이름 (RTK계수 출처)
|
||||
|
||||
shortPrdR: parseFloat(formData.unitFactors[0]?.R !== undefined ? formData.unitFactors[0]?.R : "0") || null, // 단기간R
|
||||
shortPrdT: parseFloat(formData.unitFactors[0]?.T !== undefined ? formData.unitFactors[0]?.T : "0") || null, // 단기간T
|
||||
shortPrdK: parseFloat(formData.unitFactors[0]?.K !== undefined ? formData.unitFactors[0]?.K : "0") || null, // 단기간K
|
||||
|
||||
mdPrdR: parseFloat(formData.unitFactors[1]?.R !== undefined ? formData.unitFactors[1]?.R : "0") || null, // 중간기간R
|
||||
mdPrdT: parseFloat(formData.unitFactors[1]?.T !== undefined ? formData.unitFactors[1]?.T : "0") || null, // 중간기간T
|
||||
mdPrdK: parseFloat(formData.unitFactors[1]?.K !== undefined ? formData.unitFactors[1]?.K : "0") || null, // 중간기간K
|
||||
|
||||
longPrdR: parseFloat(formData.unitFactors[2]?.R !== undefined ? formData.unitFactors[2]?.R : "0") || null, // 장기간R
|
||||
longPrdT: parseFloat(formData.unitFactors[2]?.T !== undefined ? formData.unitFactors[2]?.T : "0") || null, // 장기간T
|
||||
longPrdK: parseFloat(formData.unitFactors[2]?.K !== undefined ? formData.unitFactors[2]?.K : "0") || null, // 장기간K
|
||||
|
||||
shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax !== undefined ? formData.initialDepths[0]?.Dmax : "0") || null, // 단기간최대깊이
|
||||
shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec !== undefined ? formData.initialDepths[0]?.Drec : "0") || null, // 단기간회복비율
|
||||
shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do !== undefined ? formData.initialDepths[0]?.Do : "0") || null, // 단기간시작깊이
|
||||
|
||||
mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax !== undefined ? formData.initialDepths[1]?.Dmax : "0") || null, // 중간기간최대깊이
|
||||
mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec !== undefined ? formData.initialDepths[1]?.Drec : "0") || null, // 중간기간회복비율
|
||||
mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do !== undefined ? formData.initialDepths[1]?.Do : "0") || null, // 중간기간시작깊이
|
||||
|
||||
longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax !== undefined ? formData.initialDepths[2]?.Dmax : "0") || null, // 장기간최대깊이
|
||||
longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec !== undefined ? formData.initialDepths[2]?.Drec : "0") || null, // 장기간회복비율
|
||||
longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do !== undefined ? formData.initialDepths[2]?.Do : "0") || null, // 장기간시작깊이
|
||||
|
||||
dtrwSwtcRt: parseFloat(formData.rate !== undefined ? 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 +276,108 @@ 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-12 text-center text-xl w-[225px]" />
|
||||
</FormSection>
|
||||
<FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)">
|
||||
<Input {...form.register("rdiiArea")} className="h-12 text-center text-xl w-[225px]" />
|
||||
</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" />
|
||||
<FormSection name="suyeonga" form={form} titleNm="○ 오수관망도"><div className="w-[350px]">
|
||||
<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" />
|
||||
<FormSection name="regopm" form={form} titleNm="○ 수용가-맨홀 매칭결과"><div className="w-[350px]">
|
||||
<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"} />
|
||||
<FormSection name="rain" form={form} titleNm="○ 강우자료"><div className="w-[350px]">
|
||||
<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 {...form.register("rate")} className="h-12 text-center text-xl w-[225px] ml-5" />
|
||||
</FormSection>
|
||||
<FormSection name='date' form={form} titleNm='○ 상수사용량 적용기간'>
|
||||
<DatePicker
|
||||
selected={form.watch("date")}
|
||||
onSelect={(date: Date | undefined) => {
|
||||
if (date) {
|
||||
form.setValue("date", date, { shouldValidate: true });
|
||||
}
|
||||
}}
|
||||
mode={"single"}
|
||||
className="ml-5"
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection name='rate' form={form} titleNm='○ 분석 정보'>
|
||||
<Input {...form.register("analysisInfo")} className="h-12 text-center text-xl w-[225px] ml-5" />
|
||||
</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" />
|
||||
</div></FormSection>
|
||||
<FormSection name='rate' form={form} titleNm='○ 분석 정보'><Input 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>
|
||||
]}
|
||||
|
||||
@@ -372,6 +372,7 @@ const SearchModal: React.FC<SearchModalProps> = ({ open, onOpenChange, onFeature
|
||||
console.log('필터링된 API 결과(데이터):', res.data.data);
|
||||
setTableData(res.data.data || []);
|
||||
setTableCount(res.data.count || []);
|
||||
console.log('tableCount 상태 업데이트 후:', res.data.count); // tableCount 로그 추가
|
||||
setShowTable(true);
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -574,7 +575,10 @@ const SearchModal: React.FC<SearchModalProps> = ({ open, onOpenChange, onFeature
|
||||
<SidoSigunguSelectBox
|
||||
sido={시도}
|
||||
sigungu={시군구}
|
||||
onSidoChange={set시도}
|
||||
onSidoChange={(sidoId) => {
|
||||
set시도(sidoId);
|
||||
set시군구(0);
|
||||
}}
|
||||
onSigunguChange={set시군구}
|
||||
/>
|
||||
<Label className="w-auto text-xl text-gray-600 mr-2">하수처리구역명</Label>
|
||||
@@ -811,7 +815,7 @@ const SearchModal: React.FC<SearchModalProps> = ({ open, onOpenChange, onFeature
|
||||
<div className="relative flex flex-col border rounded-lg p-6 mb-4 w-full h-[23vh]">
|
||||
<div className="flex items-center mb-6">
|
||||
<SelectBox2
|
||||
className="w-auto mr-8"
|
||||
className="w-60 mr-8"
|
||||
selectClass="rounded-lg bg-white text-xl"
|
||||
label="업무구분"
|
||||
selectArray={WORK_TYPES.map(w => ({ val: w.val, text: w.text }))}
|
||||
@@ -856,7 +860,10 @@ const SearchModal: React.FC<SearchModalProps> = ({ open, onOpenChange, onFeature
|
||||
<SidoSigunguSelectBox
|
||||
sido={시도}
|
||||
sigungu={시군구}
|
||||
onSidoChange={set시도}
|
||||
onSidoChange={(sidoId) => {
|
||||
set시도(sidoId);
|
||||
set시군구(0);
|
||||
}}
|
||||
onSigunguChange={set시군구}
|
||||
/>
|
||||
</div>
|
||||
@@ -975,7 +982,7 @@ const SearchModal: React.FC<SearchModalProps> = ({ open, onOpenChange, onFeature
|
||||
z-[101]
|
||||
`}
|
||||
body={renderBody()}
|
||||
headerTitle="검색"
|
||||
// headerTitle="검색"
|
||||
// 필요시 footerContent 등 추가
|
||||
/>
|
||||
);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -14,6 +14,9 @@ import CircleStyle from 'ol/style/Circle';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import { Point } from 'ol/geom';
|
||||
import { boundingExtent } from 'ol/extent';
|
||||
import TileWMTS from 'ol/source/WMTS';
|
||||
import WMTS from 'ol/tilegrid/WMTS';
|
||||
import { get as getProjection } from 'ol/proj';
|
||||
import olSourceImageWMS from 'ol/source/ImageWMS';
|
||||
import { GeoJSON } from 'ol/format';
|
||||
import GisOlEditLayer from './gis.edit';
|
||||
@@ -32,20 +35,26 @@ GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows';
|
||||
GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms';
|
||||
|
||||
//맵 정보
|
||||
GisApp.EMAP_URL = '/emap/';
|
||||
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
|
||||
GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png';
|
||||
GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg';
|
||||
GisApp.GOOGLE_API_KEY = 'AIzaSyCf1sgeELFalfPuqlykRKBMj6X9u_DQH6M';
|
||||
GisApp.GOOGLE_ROADMAP_URL = `https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
|
||||
GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
|
||||
GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
|
||||
GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`;
|
||||
|
||||
// TODO: NGII 정보
|
||||
GisApp.NGII_API_KEY = '305CC242F2920F46BB93BCC13A52DD8A'; // NGII에서 발급받은 API 키
|
||||
GisApp.NGII_WMTS_URL = `http://map.ngii.go.kr/openapi/wmts_ngiiMap.js?apikey=${GisApp.NGII_API_KEY}`;
|
||||
|
||||
GisApp.EMAP_URL = '/emap/';
|
||||
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
|
||||
GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png';
|
||||
GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg';
|
||||
GisApp.OPENSTREET_URL = '//tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
GisApp.BASE_MAP_TYPES = [];
|
||||
|
||||
GisApp.FROM_PROJ = 'EPSG:4326';
|
||||
GisApp.TO_PROJ = 'EPSG:3857';
|
||||
// GisApp.TO_PROJ = 'EPSG:3857';
|
||||
GisApp.TO_PROJ = 'EPSG:5179'; //ngii 좌표계
|
||||
GisApp.TO_DATA_PROJ = 'EPSG:4326';
|
||||
GisApp.CONTEXT_LAYER_PATH = '/images/map/layericon/';
|
||||
GisApp.layerCode = {};
|
||||
@@ -86,6 +95,7 @@ GisApp.core.prototype = {
|
||||
minZoom: 5,
|
||||
maxZoom: 22,
|
||||
},
|
||||
|
||||
options: {
|
||||
/*1: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.3227711, centerY: 129.1836202, dataProj: 'EPSG:5186' },
|
||||
4: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.0140499, centerY: 126.7110757, dataProj: 'EPSG:5186' },
|
||||
@@ -109,11 +119,15 @@ GisApp.core.prototype = {
|
||||
GisApp.Module.core = this;
|
||||
GisApp.Module.layer = new GisOlLayer(this.defaults);
|
||||
GisApp.Module.edit = new GisOlEditLayer(this.defaults);
|
||||
|
||||
// TODO: vworld layer 정보 (getNGIIbaseMap)활성화 하면 ngii base map으로 변경됨
|
||||
this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base');
|
||||
this.baseLayers['base'] = this.getBaseMap('base');
|
||||
this.baseLayers['satellite'] = this.getBaseMap('satellite');
|
||||
this.baseLayers['3d'] = this.getBaseMap('3d');
|
||||
// this.baseLayers['3d'] = this.getNGIIBaseMap('3d');
|
||||
// this.baseMiniMapLayers['base'] = this.getNGIIBaseMap('minimap-base');
|
||||
// this.baseLayers['base'] = this.getNGIIBaseMap('base');
|
||||
// this.baseLayers['satellite'] = this.getNGIIBaseMap('satellite');
|
||||
},
|
||||
layerType: {
|
||||
point: 'POINT',
|
||||
@@ -126,6 +140,46 @@ GisApp.core.prototype = {
|
||||
this.setCenter(Number(this.defaults.centerX), Number(this.defaults.centerY));
|
||||
this.setZoom(15);
|
||||
},
|
||||
// NGII Base Map
|
||||
getNGIIBaseMap: function (_id) {
|
||||
const layers = {
|
||||
base: 'korean_map', // 일반지도
|
||||
satellite: 'satellite_map', // 위성지도
|
||||
night: 'night_map', // 야간지도
|
||||
};
|
||||
// 선택된 레이어
|
||||
const layerName = layers[_id] || layers.base;
|
||||
// EPSG:5179 좌표계 설정
|
||||
const projection = getProjection('EPSG:5179');
|
||||
const extent = [705680.0000, 1349270.0000,1388291.0000, 2581448.0000]; // NGII 좌표계 범위
|
||||
const resolutions = [
|
||||
2088.96, 1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255,
|
||||
];
|
||||
const tileGrid = new WMTS({
|
||||
extent: extent,
|
||||
resolutions: resolutions,
|
||||
matrixIds: resolutions.map((_, index) => index.toString()), // WMTS matrix IDs
|
||||
});
|
||||
|
||||
// WMTS 소스 생성
|
||||
const wmtsSource = new TileWMTS({
|
||||
url: GisApp.NGII_WMTS_URL,
|
||||
layer: layerName,
|
||||
matrixSet: 'EPSG:5179',
|
||||
format: 'image/png',
|
||||
projection: projection,
|
||||
tileGrid: tileGrid,
|
||||
style: 'default',
|
||||
crossOrigin: 'anonymous',
|
||||
});
|
||||
|
||||
// TileLayer 생성
|
||||
return new TileLayer({
|
||||
source: wmtsSource,
|
||||
visible: true,
|
||||
zIndex: 0,
|
||||
});
|
||||
},
|
||||
getBaseMap: function (_id) {
|
||||
let url = GisApp.VWORLD_URL;
|
||||
if (_id === 'satellite') {
|
||||
@@ -182,7 +236,7 @@ GisApp.core.prototype = {
|
||||
setCenter: function (x, y, transYn) {
|
||||
if (!x || !y) return;
|
||||
let view = this.map.getView();
|
||||
|
||||
|
||||
if (!transYn) {
|
||||
// 값이 없으면
|
||||
let center = olProj.transform([Number(y), Number(x)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
|
||||
@@ -553,7 +607,7 @@ GisApp.core.prototype = {
|
||||
*/
|
||||
convertLayerInfosToLayerCode: function (array, key, parentName) {
|
||||
let initialValue = {};
|
||||
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
let item = {};
|
||||
Object.assign(item, array[i]);
|
||||
|
||||
@@ -59,44 +59,54 @@ export function UiTable2({ tableData, headlist, pageSize, total, children, headN
|
||||
<h3 className="text-2xl font-medium text-label">{headName}</h3>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={`card !m-[1.2rem] !p-0r w-full relative ${className}`}>
|
||||
<div>
|
||||
<Table variant="secondary" className="w-full">
|
||||
{/* 스크롤 관련 div 제거 */}
|
||||
<TableHeader className="bg-white">
|
||||
{children ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<TableRow>
|
||||
{headlist?.map((head) => (
|
||||
<TableHead key={head.id}
|
||||
className="min-w-[120px] max-w-[180px] w-[150px]
|
||||
<div className={`card !m-[1.2rem] !p-0r w-full relative flex flex-col ${className}`}
|
||||
style={{ height: tableData && tableData.length > 0 ? 'auto' : 'calc(100% - 2.4rem)' }} // 데이터 있을 땐 auto, 없을 땐 부모 높이 채움
|
||||
>
|
||||
{/* 테이블 또는 "데이터 없음" 메시지를 담는 컨테이너, flex-grow로 공간 차지 및 스크롤 처리 */}
|
||||
<div className="overflow-auto flex-grow">
|
||||
{(!tableData || tableData.length === 0) ? (
|
||||
// "데이터 없음" 메시지를 flex를 이용해 수직/수평 중앙 정렬
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
조회된 데이터가 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<Table variant="secondary" className="w-full">
|
||||
{/* 스크롤 관련 div 제거 */}
|
||||
<TableHeader className="bg-white">
|
||||
{children ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<TableRow>
|
||||
{headlist?.map((head) => (
|
||||
<TableHead key={head.id}
|
||||
className="min-w-[120px] max-w-[180px] w-[150px]
|
||||
text-center whitespace-nowrap overflow-hidden
|
||||
text-ellipsis bg-white">
|
||||
{head.title}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody className='text-center'>
|
||||
{tableData.map((el: { [key: string]: string | number | boolean | object }, index) => (
|
||||
<TableRow key={index + "row"}>
|
||||
{headlist?.map((head) => (
|
||||
<TableCell
|
||||
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id] === 'number' ? 'text-right' : 'text-center'} ${cellClick && idx === index ? '!font-[1000] cursor-pointer' :
|
||||
cellClick ? 'cursor-pointer' : ''
|
||||
}`}
|
||||
onClick={() => handleCellClick(startIdx + index, head.id)}
|
||||
key={head.id + index}
|
||||
>
|
||||
{typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{head.title}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableHeader>
|
||||
<TableBody className='text-center'>
|
||||
{tableData.map((el: { [key: string]: string | number | boolean | object }, index) => (
|
||||
<TableRow key={index + "row"}>
|
||||
{headlist?.map((head) => (
|
||||
<TableCell
|
||||
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id] === 'number' ? 'text-right' : 'text-center'} ${cellClick && idx === index ? '!font-[1000] cursor-pointer' :
|
||||
cellClick ? 'cursor-pointer' : ''
|
||||
}`}
|
||||
onClick={() => handleCellClick(startIdx + index, head.id)}
|
||||
key={head.id + index}
|
||||
>
|
||||
{typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
{/* 페이지네이션을 sticky 하단에 고정 */}
|
||||
{total > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user