빗물지도점검 오류 검증

This commit is contained in:
leedj312
2025-07-16 13:43:03 +09:00
parent 35ff13fb38
commit 2356c10d34
5 changed files with 247 additions and 186 deletions
@@ -28,7 +28,7 @@ export type Buildingtype = {
export const buildingToExcelType = (
buildings: Buildingtype[],
sidoCode: SelectDataType[],
sggCdoe: SelectDataType[],
sggCdoe: SelectDataType[]
) => {
const excelData: string[][] = [];
buildings.forEach((building) => {
@@ -37,8 +37,8 @@ export const buildingToExcelType = (
building.bdstInfoSn.toString(),
sidoCode.find((sido) => sido.val === building.ctpvCd.toString())
?.text || "",
sggCdoe.find((sido) => sido.val === building.sggCd.toString())
?.text || "",
sggCdoe.find((sido) => sido.val === building.sggCd.toString())?.text ||
"",
building.daddr,
building.bdstNm,
"",
@@ -80,121 +80,128 @@ export const buildingToExcelType = (
});
});
return excelData;
};
export const excelToRainChkData = (excelData: any, year: string) => {
// const errors: string[] = [];
const rainChkgdData: rainChkZodType[] = excelData.map((item: any) => {
if (item["점검일"] !== undefined) {
const checkYmd = item["점검일"].replace(/\./g, "")
let vltnDt = item["과태료 부과일(위반일)"]
if (vltnDt !== undefined) {
vltnDt = vltnDt ? vltnDt.replace(/\./g, "") : "";
}
const makeGuidDetail = Array.from({ length: 12 }, (_, i) => {
let month = `${i + 1}`;
const splyData = item[`대체_${month}월`];
const ruseData = item[`빗물_${month}월`];
if (month.length < 2) month = "0" + month;
return {
utztnUsgCd: item["이용용도"],
ym: year + month,
chckYmd: checkYmd,
splyUswtrUtztnQnt: Number(splyData) || 0,
ruseFcltUtztnQnt: Number(ruseData) || 0,
flg: "RNB",
};
});
return {
bdstNm:item["시설명"] ?? "",
bdstRuseFcltSn: Number(item["NO"]),
chckYmd: checkYmd,
spwtPositn: item["유량측정방법"] ?? "",
rgtr: TEST_UUID,
oilqMeasMthd: item["운영관리형태"] ?? "",
anlOperCst: Number(item["연간운영비(원/년)"]) ?? 0,
operMngCd: item["유량측정방법"] ?? "",
guideDetails: makeGuidDetail,
rainyDetails: [
{
utztnUsgCd: item["이용용도"] ?? "",
ph: Number(item["PH"] || 0),
trbt: Number(item["탁도"] || 0),
grssClfg: Number(item["총대장균군수"] || 0),
etc: item["기타"] || "", // 필드 누락 방지
vltnDt: vltnDt || "-",
vltnMttr: item["위반사항"] || "-",
levyAmt: Number(item["과태료부과금액(천원)"] || 0),
export const excelToRainChkData = (excelData: any, year: string) => {
// const errors: string[] = [];
const rainChkgdData: rainChkZodType[] = excelData
.map((item: any) => {
if (item["점검일"] !== undefined) {
const checkYmd = item["점검일"].replace(/\./g, "");
let vltnDt = item["과태료 부과일(위반일)"];
if (vltnDt !== undefined) {
vltnDt = vltnDt ? vltnDt.replace(/\./g, "") : "";
}
const makeGuidDetail = Array.from({ length: 12 }, (_, i) => {
let month = `${i + 1}`;
const splyData = item[`대체_${month}월`];
const ruseData = item[`빗물_${month}월`];
if (month.length < 2) month = "0" + month;
return {
utztnUsgCd: item["이용용도"],
ym: year + month,
chckYmd: checkYmd,
splyUswtrUtztnQnt: Number(splyData) || 0,
ruseFcltUtztnQnt: Number(ruseData) || 0,
flg: "RNB",
};
});
if (checkYmd.length > 7) {
return {
bdstNm: item["시설명"] ?? "",
bdstRuseFcltSn: Number(item["NO"]),
chckYmd: checkYmd,
spwtPositn: item["유량측정방법"] ?? "",
rgtr: TEST_UUID,
},
],
};
}
}).filter((el: any) => el !== undefined);
oilqMeasMthd: item["운영관리형태"] ?? "",
anlOperCst: Number(item["연간운영비(원/년)"]) ?? 0,
operMngCd: item["유량측정방법"] ?? "",
guideDetails: makeGuidDetail,
rainyDetails: [
{
utztnUsgCd: item["이용용도"] ?? "",
ph: Number(item["PH"] || 0),
trbt: Number(item["탁도"] || 0),
grssClfg: Number(item["총대장균군수"] || 0),
etc: item["기타"] || "", // 필드 누락 방지
vltnDt: vltnDt || "-",
vltnMttr: item["위반사항"] || "-",
levyAmt: Number(item["과태료부과금액(천원)"] || 0),
rgtr: TEST_UUID,
},
],
};
}
}
})
.filter((el: any) => el !== undefined);
return rainChkgdData;
// // ✨ 오류 검사 - 가공된 데이터 기준
// const isInvalidNumber = (val: any) =>
// val !== undefined && val !== null && val !== "" && isNaN(Number(val));
// const isInvalidDate = (val: any) =>
// typeof val === "string" && !/^\d{4}\.\d{2}\.\d{2}$/.test(val.trim());
//return rainChkgdData;
const issues: string[] = [];
// const buildErrorMessage = (item: any, issues: string[]): string => {
// const 시설명 = item.bdstNm ?? "";
// const 점검일 = item.chckYmd ?? "";
// const 이용용도 = item.utztnUsgCd ?? "";
// return `시설명: ${시설명}, 점검일: ${점검일}, 이용용도: ${이용용도} (=> ${issues.join(", ")})`;
// };
if (rainChkgdData.length > 0) {
rainChkgdData.forEach((rain: any) => {
const defaultMessage =`[시설명: ${rain.bdstNm}, 점검일: ${rain.chckYmd}, 이용용도: ${rain.guideDetails[0]?.utztnUsgCd}] 항목에서 `
// if (rainChkgdData.length > 0) {
// rainChkgdData.forEach((rain: any) => {
// const item = rain.__origin; // 원본 item
// const issues: string[] = [];
// if (rain.chckYmd && isInvalidDate(rain.chckYmd)) {
// issues.push(`점검일: 날짜 형식 아님 (${rain.chckYmd})`);
// }
if (rain.chckYmd && isInvalidDate(rain.chckYmd)) {
issues.push(defaultMessage+`점검일: 날짜 형식 아님 (${rain.chckYmd})`);
}
if (rain.anlOperCst && isInvalidNumber(rain.anlOperCst)) {
issues.push(defaultMessage+` 연간운영비(원/년): 숫자 형식 아님 (${rain.anlOperCst})`);
}
rain.rainyDetails?.forEach((key) => {
if (isInvalidNumber(key.ph)) {
issues.push(defaultMessage+`PH: 숫자 형식 아님 (${key.ph})`);
} else if (isInvalidNumber(key.trbt)) {
issues.push(defaultMessage+`탁도: 숫자 형식 아님 (${key.trbt})`);
} else if (isInvalidNumber(key.grssClfg)) {
issues.push(defaultMessage+`총대장균군수: 숫자 형식 아님 (${key.grssClfg})`);
} else if (isInvalidNumber(key.levyAmt)) {
issues.push(defaultMessage+`과태료부과금액(천원): 숫자 형식 아님 (${key.levyAmt})`);
} else if (isInvalidDate(key.vltnDt)) {
issues.push(defaultMessage+`과태료 부과일(위반일): 날짜 형식 아님 (${key.vltnDt})`);
}
});
rain.guideDetails?.forEach((guide) => {
const mm = guide.ym.substring(4);
if (isInvalidNumber(guide.splyUswtrUtztnQnt)) {
issues.push(
defaultMessage+`대체_${mm}: 숫자 형식 아님 (${guide.splyUswtrUtztnQnt})`
);
}
if (isInvalidNumber(guide.ruseFcltUtztnQnt)) {
issues.push(defaultMessage+`빗물_${mm}: 숫자 형식 아님 (${guide.ruseFcltUtztnQnt})`);
}
});
});
}
// if (rain.vltnDt && isInvalidDate(rain.vltnDt)) {
// issues.push(`과태료 부과일(위반일): 날짜 형식 아님 (${rain.vltnDt})`);
// }
if (issues.length > 0) {
return { rainChkgdData: [], issues };
}
// // [
// // "연간운영비(원/년)", "PH", "탁도", "총대장균군수", "과태료부과금액(천원)"
// // ].forEach((key) => {
// // if (isInvalidNumber(item[key])) {
// // issues.push(`${key}: 숫자 형식 아님 (${item[key]})`);
// // }
// // }
// // );
return { rainChkgdData, issues: [] };
// for (let i = 1; i <= 12; i++) {
// const month = `${i}월`;
// if (isInvalidNumber(item[`대체_${month}`])) {
// issues.push(`대체_${month}: 숫자 형식 아님 (${item[`대체_${month}`]})`);
// }
// if (isInvalidNumber(item[`빗물_${month}`])) {
// issues.push(`빗물_${month}: 숫자 형식 아님 (${item[`빗물_${month}`]})`);
// }
// }
// if (issues.length > 0) {
// errors.push(buildErrorMessage(item, issues));
// }
// });
// }
// if (errors.length > 0) {
// return { rainChkgdData: [], errors };
// }
// return { rainChkgdData, errors: [] };
// return rainChkgdData;
//return rainChkgdData;
};
const buildErrorMessage = (item: any, issues: string[]) :string=> {
const 시설명 = item.bdstNm ?? "";
const 점검일 = item.chckYmd ?? "";
const 이용용도 = item.guideDetails[0]?.utztnUsgCd ?? "";
return `시설명: ${시설명}, 점검일: ${점검일}, 이용용도: ${이용용도} (=> ${issues.join(", ")})`;
};
// ✨ 오류 검사 - 가공된 데이터 기준
const isInvalidNumber = (val: any) => {
return !val !== undefined && val !== null && val !== "" && isNaN(Number(val));
};
const isInvalidDate = (val: any) => {
//yyyymmdd 즉 20201212 이런형식만 허용
return !(typeof val === "string" && /^\d{8}$/.test(val.trim()));
};
@@ -36,6 +36,7 @@ export const rainChkZod = z.object({
cityText:z.string().nullish(),
sggText:z.string().nullish(),
prcsCpct: z.number().nullable(),
anlOperCst:z.number().nullish(),
spwtPositn: z.string().nullable(),
regDt: z.string().nullable(),
rgtr: z.string().nullable(),
@@ -84,6 +84,7 @@ type rainChkgdnActions = {
sggCd: string,
txt: string
) => void;
setRainChkLoading:(status:boolean) =>void
};
export type rainChkgdnType = rainChkgdnState & rainChkgdnActions;
@@ -375,6 +376,17 @@ export const rainChkgdnActionsExport: (set: any) => rainChkgdnActions = (
} catch (error) {
console.log(error);
}
},
},setRainChkLoading:(status:boolean) =>{
set((state: rainChkgdnState) => ({
...state,
rainChkgdn: {
...state.rainChkgdn,
isLoading: status,
},
}));
}
};
};
+59 -39
View File
@@ -11,9 +11,8 @@ import type { RefObject } from "react";
import toast from "react-hot-toast";
export const getCity = (val: string, sggCd: SelectDataType[]) => {
let data: any
if (val !== '0') {
let data: any;
if (val !== "0") {
data = sggCd
.filter((item) => String(item.val).substring(0, 2) === val)
.map((item) => ({
@@ -21,11 +20,10 @@ export const getCity = (val: string, sggCd: SelectDataType[]) => {
text: item.text, // 한국어 텍스트
}));
} else {
data = sggCd
.map((item) => ({
val: item.val,
text: item.text, // 한국어 텍스트
}));
data = sggCd.map((item) => ({
val: item.val,
text: item.text, // 한국어 텍스트
}));
}
data.unshift({ val: "0", text: "전체" });
return data;
@@ -118,7 +116,6 @@ export const handleCommaNumberChange = (
onChange(raw === "" ? "" : Number(raw));
};
// onchange 텍스트를 숫자로 and ,제거
export const handleMinusNumberChange = (
e: React.ChangeEvent<HTMLInputElement>,
@@ -170,55 +167,78 @@ export const yearOptions: { val: string; text: string }[] = Array.from(
export const getdmdMatFlag = (flag: string | undefined | null) => {
const cleanFlag = (flag ?? "").trim();
switch (cleanFlag) {
case "Sub":
return "검토";
case "Matched":
return "매칭완료";
case "Checked":
return '검토완료';
return "검토완료";
default:
return "신청";
}
};
export const formatPhoneNumber = (tel: string) => {
const onlyNums = tel.replace(/[^0-9]/g, '');
const onlyNums = tel.replace(/[^0-9]/g, "");
if (onlyNums.length === 10) {
return onlyNums.replace(/(\d{2})(\d{4})(\d{4})/, '$1-$2-$3');
return onlyNums.replace(/(\d{2})(\d{4})(\d{4})/, "$1-$2-$3");
} else if (onlyNums.length === 11) {
return onlyNums.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
return onlyNums.replace(/(\d{3})(\d{4})(\d{4})/, "$1-$2-$3");
} else {
return tel; // 유효하지 않으면 빈 문자열
}
}
};
export const confirmToast = (content: string, saveFunc: () => void) => {
toast((t) => (
<span className="flex flex-col items-center">
<div className="mb-6">
{content}
</div>
<div className="flex flex-row space-x-2">
<Button
size={"sm"}
onClick={() => {
saveFunc();
toast.dismiss(t.id);
}}
>
확인
</Button>
<Button
size={"sm"}
onClick={() => toast.dismiss(t.id)}>취소</Button>
</div>
</span>
),
toast(
(t) => (
<span className="flex flex-col items-center">
<div className="mb-6">{content}</div>
<div className="flex flex-row space-x-2">
<Button
size={"sm"}
onClick={() => {
saveFunc();
toast.dismiss(t.id);
}}
>
확인
</Button>
<Button size={"sm"} onClick={() => toast.dismiss(t.id)}>
취소
</Button>
</div>
</span>
),
{
id: "global-confirm-toast", // 중복 토스트 방지
duration: Infinity, // 자동 사라짐 방지
});
}
}
);
};
export const showConfirmToast = (message: string) => {
toast.custom(
(t) => (
<div
className={`w-[500px] min-h-[140px] bg-white shadow-lg rounded-xl p-6 border border-gray-200
${t.visible ? "animate-enter" : "animate-leave"}`}
>
<div className="text-3xl font-extrabold text-red-600 bg-yellow-100 px-4 py-2 rounded-md shadow-md">
{message}
</div>{" "}
<div className="flex justify-end gap-2">
<Button
size={"sm"}
className="px-4 py-1.5 rounded bg-gray-200 text-sm hover:bg-gray-300"
onClick={() => toast.dismiss(t.id)}
>
확인완료
</Button>
</div>
</div>
),
{ duration: Infinity }
); // 수동 닫기
};
@@ -7,6 +7,7 @@ import {
excelToRswtGuideData,
excelToRswtPolution,
} from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn/excel";
import { showConfirmToast } from "@/components/lib/func/commom";
import {
filePicked,
filePickedArray,
@@ -18,22 +19,39 @@ import {
} from "@common/business_components/ui";
import { Button, Input } from "@common/components/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import type { z } from "zod";
export default function ChkgdExcel({ type,setIsRegExcel ,setYearChange }: { type: string, setIsRegExcel:(open:boolean) =>void ,setYearChange:(year:string)=> void }) {
const { rainChkExcelDataUPload, rswtChkExceDataUpload } =
useWrStore((state) => {
return state;
});
export default function ChkgdExcel({
type,
setIsRegExcel,
setYearChange,
}: {
type: string;
setIsRegExcel: (open: boolean) => void;
setYearChange: (year: string) => void;
}) {
const {
rainChkgdn,
rainChkExcelDataUPload,
rswtChkExceDataUpload,
setRainChkLoading,
} = useWrStore((state) => {
return state;
});
const [excelData, setExcelData] = useState<
rswtChkZodType[] | rainChkZodType[]
>();
const [file, setFile] = useState<File | null>(null);
const clearFile = () => {
if (fileInputRef.current) {
fileInputRef.current.value = ""; // ✅ 파일 선택 비우기
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (selectedFile) {
@@ -41,6 +59,7 @@ export default function ChkgdExcel({ type,setIsRegExcel ,setYearChange }: { type
}
};
const [excelRswtData, setExcelRswtData] = useState<rswtChkZodType[]>();
const fileInputRef = useRef<HTMLInputElement>(null);
const currentYear = new Date().getFullYear();
const startYear = 1970;
@@ -70,26 +89,25 @@ export default function ChkgdExcel({ type,setIsRegExcel ,setYearChange }: { type
}
if (file)
if (type === "RNB") {
filePicked(file, 0).then((res) => {
// if (res !== undefined){
const excelData = excelToRainChkData(res, form.getValues("chckYr"))
if(res !== undefined)
setExcelData(excelData)
// if( excelData&&excelData.errors.length>0){
// excelData.errors.forEach((err) =>{
// toast.error(err)
// })
// }else{
// }
// }
})
setRainChkLoading(true);
filePicked(file, 0)
.then(async (res) => {
const excelData = excelToRainChkData(
res,
form.getValues("chckYr")
);
if (excelData.issues && excelData.issues.length > 0) {
excelData.issues.forEach((err) => {
showConfirmToast(err);
});
clearFile();
} else {
setExcelData(excelData.rainChkgdData);
}
})
.finally(() => {
setRainChkLoading(false);
});
} else if (type === "RST") {
filePickedArray(file, 0).then((res) => {
if (res && res?.length > 1) {
@@ -165,9 +183,12 @@ export default function ChkgdExcel({ type,setIsRegExcel ,setYearChange }: { type
return;
} else {
rainChkExcelDataUPload(excelData as rainChkZodType[] , form.getValues("chckYr")) ;
setIsRegExcel(false)
setYearChange(form.getValues("chckYr"))
rainChkExcelDataUPload(
excelData as rainChkZodType[],
form.getValues("chckYr")
);
setIsRegExcel(false);
setYearChange(form.getValues("chckYr"));
}
} else if (type === "RST") {
if (excelRswtData === undefined || excelRswtData?.length === 0) {
@@ -176,13 +197,12 @@ export default function ChkgdExcel({ type,setIsRegExcel ,setYearChange }: { type
return;
} else {
rswtChkExceDataUpload(excelRswtData as rswtChkZodType[]);
setIsRegExcel(false)
setYearChange(form.getValues("chckYr"))
}
setIsRegExcel(false);
setYearChange(form.getValues("chckYr"));
}
}
};
return (
<>
<div>
@@ -231,14 +251,15 @@ export default function ChkgdExcel({ type,setIsRegExcel ,setYearChange }: { type
<FormUi label="점검결과 파일">
<div className="space-y-4 p-4">
<Input
ref={fileInputRef}
type="file"
onChange={(file) =>{
handleFileChange( file)
////TODO : 폼 을 써야하는 지 확인 필요
console.log(field.value)
} }
onChange={(file) => {
handleFileChange(file);
////TODO : 폼 을 써야하는 지 확인 필요
console.log(field.value);
}}
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
/>
/>
</div>
</FormUi>
)}