물재이용 사례관리 엑셀 등록 유효성 검사 및 저장 로직 변경

This commit is contained in:
leedj312
2025-09-04 14:43:29 +09:00
parent c4c5e68267
commit cbc7c4918f
5 changed files with 700 additions and 460 deletions
@@ -6,18 +6,18 @@ import { excelDownload, getCity } from "@/components/lib/func/commom";
import RuseCaseMngExcel from "@/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal";
import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/bdstRegModal";
import {
ContentsDiv,
filePickedBlobArray,
SearchDiv,
SearchFormLeft,
SearchFormRight,
SearchInput,
SelectBox,
Title,
UiModal,
UiTable,
type SelectDataType,
type SheetParameter,
ContentsDiv,
filePickedBlobArray,
SearchDiv,
SearchFormLeft,
SearchFormRight,
SearchInput,
SelectBox,
Title,
UiModal,
UiTable,
type SelectDataType,
type SheetParameter,
} from "@common/business_components/ui";
import ListDiv from "@common/business_components/ui/list_div";
import { Button, Label } from "@common/components/ui";
@@ -26,277 +26,294 @@ import toast, { Toaster } from "react-hot-toast";
import { useTranslation } from "react-i18next";
export default function RuseCaseMng() {
const { code, ruseCaseMng, selectRuseCaseMng, ruseCaseMngInit } = useWrStore((state) => state);
const { ruseCaseMngList, status, count, excelList } = ruseCaseMng;
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>([]);
const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" });
const { code, ruseCaseMng, selectRuseCaseMng, ruseCaseMngInit } = useWrStore(
(state) => state
);
const { ruseCaseMngList, status, count, excelList } = ruseCaseMng;
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>([]);
const [objSearch, setObjSearch] = useState<objSearchType>({
sgg: "",
city: "",
});
const [sidoCd, setSidoCd] = useState<string>("");
const [sigunguCd, setSigunguCd] = useState<string>("");
// const [selectUtz, setSelectUtz] = useState<string[] | []>(["조경용수"]);
const [selectUtz, setSelectUtz] = useState<string>("");
const [name, setName] = useState<string>("");
const [resetPage, isResetPage] = useState(false);
const { t } = useTranslation('base');
const [sidoCd, setSidoCd] = useState<string>("");
const [sigunguCd, setSigunguCd] = useState<string>("");
// const [selectUtz, setSelectUtz] = useState<string[] | []>(["조경용수"]);
const [selectUtz, setSelectUtz] = useState<string>("");
const [name, setName] = useState<string>("");
const [resetPage, isResetPage] = useState(false);
const { t } = useTranslation("base");
// 물재이용사례 조회
const searchRuseCaseMng = async (isExcel: boolean, pageNo: number, pageSize: number) => {
await selectRuseCaseMng(
isExcel,
pageNo,
pageSize,
sidoCd ?? "",
sigunguCd ?? "",
selectUtz ?? "",
name ?? ""
);
// 엑셀 등록 모달
const [isExcelOpen, setIsExcelOpen] = useState<boolean>(false);
// 물재이용사례 조회
useEffect(() => {
ruseCaseMngInit();
}, []);
// 시도 선택
const getSelCity = (val: string | number) => {
const cityValue = String(val);
const cityText = code.cityCd.find(
(item: { val: string; text: string }) => item.val === cityValue
)?.text;
setSidoCd(cityText || "");
const data = getCity(cityValue, code.sggCd);
setSelectSgg(data);
setObjSearch({ city: cityValue, sgg: "" });
};
// 시군구 선택
const getSgg = (val: string | number) => {
const sggValue = String(val);
const sggText = selectSgg?.find(
(item: { val: string; text: string }) => item.val === sggValue
)?.text;
setSigunguCd(sggText || "");
setObjSearch({ ...objSearch, sgg: String(val) });
};
useEffect(() => {
if (excelList !== undefined && excelList.length === 0) {
toast.error(t("excelDownErrMsg"));
return;
}
useEffect(() => {
ruseCaseMngInit();
}, [])
// 시도 선택
const getSelCity = (val: string | number) => {
const cityValue = String(val);
const cityText = code.cityCd.find((item: { val: string; text: string }) => item.val === cityValue)?.text;
setSidoCd(cityText || "");
const data = getCity(cityValue, code.sggCd);
setSelectSgg(data);
setObjSearch({ city: cityValue, sgg: "" });
};
// 시군구 선택
const getSgg = (val: string | number) => {
const sggValue = String(val);
const sggText = selectSgg?.find((item: { val: string; text: string }) => item.val === sggValue)?.text;
setSigunguCd(sggText || "");
setObjSearch({ ...objSearch, sgg: String(val) });
};
// 엑셀 등록 모달
const [isExcelOpen, setIsExcelOpen] = useState<{
isModal: boolean;
}>({ isModal: false });
useEffect(() => {
if (excelList !== undefined && excelList.length === 0) {
toast.error(t('excelDownErrMsg'));
return;
}
fetch(`/sample_ruseCaseMng.xlsx`, {}).then(async (response) => {
if (!response.ok) {
throw Error(response.statusText);
}
// 받은 파일 바인너리 처리
const blobRet = await response.arrayBuffer();
if(excelList){
const excelData1 = excelList.map((item) => {
return [
item.ruseBizNm,
item.bizBgngDate,
item.bizEndDate,
item.splyFcltKnd,
item.splyFcltNm,
item.splyFcltCpct,
item.dmdFcltNm,
item.dmdFcltAddr,
item.ruseFcltCpct,
item.ruseFcltPplLen,
item.ruseFcltUsg,
item.ruseFcltSplyCpct,
item.ruseFcltPrcsMsrc,
item.grnteWqUsg,
item.grnteWqGrssClfg,
item.grnteWqCombResdch,
item.grnteWqTrbt,
item.grnteWqBod,
item.grnteWqSmll,
item.grnteWqCrmty,
item.grnteWqTn,
item.grnteWqTp,
item.grnteWqPh,
item.grnteWqClIonQnt,
item.grnteWqCndctStat,
item.grnteWqRmrk,
item.reqBizGrssCst,
item.reqBizReuseFcltCst,
item.reqBizPplCst,
item.reqBizArchCst,
item.reqBizMchnCst,
item.reqBizMtrngCst,
item.reqBizGisCst,
item.reqBizCctvCst,
item.reqBizCmcnCst,
item.reqBizWstCst,
item.reqBizDesignCst,
item.reqBizSpvsCst,
item.reqBizSbsdCst,
item.reqBizEtc,
item.prcmPlanGrssCst,
item.prcmPlanNlep,
item.prcmPlanLcep,
item.prcmPlanBrdnCst,
item.prcmPlanEcqrRt,
item.clclnGrssCst,
item.clclnNlep,
item.clclnLcep,
item.clclnBrdnCst,
item.clclnEcqrRt,
];
});
const sheetParameter: SheetParameter[] = [
{
data: excelData1,
headerNum: 2,
headerRange: 'A1:AX1',
sheetName: '물재이용사례',
startCol: 'A'
}
];
// 버퍼에 있는 엑셀데이터 전달 후 json으로 변환
await filePickedBlobArray(blobRet, '물재이용사례현황', sheetParameter);
}
fetch(`/sample_ruseCaseMng.xlsx`, {}).then(async (response) => {
if (!response.ok) {
throw Error(response.statusText);
}
// 받은 파일 바인너리 처리
const blobRet = await response.arrayBuffer();
if (excelList) {
const excelData1 = excelList.map((item) => {
return [
item.ruseBizNm,
item.bizBgngDate,
item.bizEndDate,
item.splyFcltKnd,
item.splyFcltNm,
item.splyFcltCpct,
item.dmdFcltNm,
item.dmdFcltAddr,
item.ruseFcltCpct,
item.ruseFcltPplLen,
item.ruseFcltUsg,
item.ruseFcltSplyCpct,
item.ruseFcltPrcsMsrc,
item.grnteWqUsg,
item.grnteWqGrssClfg,
item.grnteWqCombResdch,
item.grnteWqTrbt,
item.grnteWqBod,
item.grnteWqSmll,
item.grnteWqCrmty,
item.grnteWqTn,
item.grnteWqTp,
item.grnteWqPh,
item.grnteWqClIonQnt,
item.grnteWqCndctStat,
item.grnteWqRmrk,
item.reqBizGrssCst,
item.reqBizReuseFcltCst,
item.reqBizPplCst,
item.reqBizArchCst,
item.reqBizMchnCst,
item.reqBizMtrngCst,
item.reqBizGisCst,
item.reqBizCctvCst,
item.reqBizCmcnCst,
item.reqBizWstCst,
item.reqBizDesignCst,
item.reqBizSpvsCst,
item.reqBizSbsdCst,
item.reqBizEtc,
item.prcmPlanGrssCst,
item.prcmPlanNlep,
item.prcmPlanLcep,
item.prcmPlanBrdnCst,
item.prcmPlanEcqrRt,
item.clclnGrssCst,
item.clclnNlep,
item.clclnLcep,
item.clclnBrdnCst,
item.clclnEcqrRt,
];
});
const sheetParameter: SheetParameter[] = [
{
data: excelData1,
headerNum: 2,
headerRange: "A1:AX1",
sheetName: "물재이용사례",
startCol: "A",
},
];
// 버퍼에 있는 엑셀데이터 전달 후 json으로 변환
await filePickedBlobArray(blobRet, "물재이용사례현황", sheetParameter);
}
});
}, [excelList]);
}, [excelList])
// 양식받기
const sampleDownLoad = () => {
excelDownload(
[[]],
"물재이용사례 관리 등록양식",
"sample_ruseCaseMng.xlsx",
"A1:AX1",
2
);
useEffect(() => {
if (status === 200) {
setIsExcelOpen(false);
}
}, [status]);
useEffect(() => {
if (status === 200) {
setIsExcelOpen({ isModal: false });
}
}, [status]);
return (
<>
<ContentsDiv>
<div className="grid grid-cols-4 gap-4">
<div className="col-span-4">
<Title title={"물재이용사례 관리"} />
<SearchDiv>
<SearchFormLeft>
<div className="col-span-8 flex flex-row items-center">
<SelectBox
className="col-span-2 w-1/3 h-full text-2xl"
label="시도"
selectClass={""}
selectArray={code.cityCd || []}
onSelectValue={getSelCity}
/>
<SelectBox
className="col-span-2 w-1/3 h-full text-2xl ml-4"
label="시군구"
selectClass={""}
selectArray={selectSgg || []}
onSelectValue={getSgg}
/>
<SelectBox
className="col-span-2 w-1/3 h-full text-2xl ml-4"
label="용도"
selectClass={""}
selectArray={ruseUtzType}
onSelectValue={(val) => {
if (val) setSelectUtz(String(val));
}}
value={selectUtz || ruseUtzType[0]?.val}
/>
<Label className="w-28 text-xl ml-4 mr-6">
{"수요처명"}
</Label>
<SearchInput
className={"col-span-2 w-1/3 text-2xl"}
label=""
textValue={name}
setText={(e) => {
setName(e);
}}
/>
</div>
</SearchFormLeft>
<SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2">
<div className="space-y">
<div className="flex flex-row space-x-2">
<Button size={"sm"} onClick={() => {
searchRuseCaseMng(false, 1, 10);
isResetPage(true);
}}>
조회
</Button>
<Button size={"sm"} onClick={() => { setIsExcelOpen({ isModal: true }); }}>
엑셀등록
</Button>
<Button size={"sm"} onClick={() => {
selectRuseCaseMng(
true,
1,
count,
sidoCd ?? "",
sigunguCd ?? "",
selectUtz ?? "",
name ?? ""
);
// download()
}} className={"green"}>
다운로드
</Button>
<Button size={"sm"} onClick={() => { sampleDownLoad() }}>
양식받기
</Button>
</div>
</div>
</SearchFormRight>
</SearchDiv>
</div>
return (
<>
<ContentsDiv>
<div className="grid grid-cols-4 gap-4">
<div className="col-span-4">
<Title title={"물재이용사례 관리"} />
<SearchDiv>
<SearchFormLeft>
<div className="col-span-8 flex flex-row items-center">
<SelectBox
className="col-span-2 w-1/3 h-full text-2xl"
label="시도"
selectClass={""}
selectArray={code.cityCd || []}
onSelectValue={getSelCity}
/>
<SelectBox
className="col-span-2 w-1/3 h-full text-2xl ml-4"
label="시군구"
selectClass={""}
selectArray={selectSgg || []}
onSelectValue={getSgg}
/>
<SelectBox
className="col-span-2 w-1/3 h-full text-2xl ml-4"
label="용도"
selectClass={""}
selectArray={ruseUtzType}
onSelectValue={(val) => {
if (val) setSelectUtz(String(val));
}}
value={selectUtz || ruseUtzType[0]?.val}
/>
<Label className="w-28 text-xl ml-4 mr-6">{"수요처명"}</Label>
<SearchInput
className={"col-span-2 w-1/3 text-2xl"}
label=""
textValue={name}
setText={(e) => {
setName(e);
}}
/>
</div>
<div className="grid grid-cols-6 gap-4">
<div className="col-span-6">
<ListDiv className="overflow-x-auto">
<div className="w-[350%]">
<UiTable
headName={"물재이용사례 현황"}
tableData={ruseCaseMngList}
headlist={ruseCaseMng.ruseHeadList}
pageSize={20}
total={count}
cellClick={undefined}
onPageChange={(pageNo: number) => {
searchRuseCaseMng(false, pageNo, 10);
isResetPage(false)
}}
resetPageNo={resetPage}
/>
</div>
</ListDiv>
</div>
</SearchFormLeft>
<SearchFormRight className="w-2/12 mx-5 flex flex-col items-center justify-center space-x-2">
<div className="space-y">
<div className="flex flex-row space-x-2">
<Button
size={"sm"}
onClick={() => {
selectRuseCaseMng(
false,
1,
10,
sidoCd ?? "",
sigunguCd ?? "",
selectUtz ?? "",
name ?? ""
);
isResetPage(true);
}}
>
조회
</Button>
<Button
size={"sm"}
onClick={() => {
setIsExcelOpen(true);
}}
>
엑셀등록
</Button>
<Button
size={"sm"}
onClick={() => {
selectRuseCaseMng(
true,
1,
count,
sidoCd ?? "",
sigunguCd ?? "",
selectUtz ?? "",
name ?? ""
);
// download()
}}
className={"green"}
>
다운로드
</Button>
<Button
size={"sm"}
onClick={() => {
excelDownload(
[[]],
"물재이용사례 관리 등록양식",
"sample_ruseCaseMng.xlsx",
"A1:AX1",
2
);
}}
>
양식받기
</Button>
</div>
</div>
</ContentsDiv>
<Toaster position="top-center" />
{/* 엑셀 등록 모달 */}
<UiModal
open={isExcelOpen.isModal}
onOpenChange={() => {
setIsExcelOpen({ isModal: !isExcelOpen.isModal })
}}
bodyClass={"w-[35vw] max-w-none"}
body={<RuseCaseMngExcel />}
/>
</>
);
}
</SearchFormRight>
</SearchDiv>
</div>
</div>
<div className="grid grid-cols-6 gap-4">
<div className="col-span-6">
<ListDiv className="overflow-x-auto">
<div className="w-[350%]">
<UiTable
headName={"물재이용사례 현황"}
tableData={ruseCaseMngList}
headlist={ruseCaseMng.ruseHeadList}
pageSize={20}
total={count}
cellClick={undefined}
onPageChange={(pageNo: number) => {
selectRuseCaseMng(
false,
pageNo,
10,
sidoCd ?? "",
sigunguCd ?? "",
selectUtz ?? "",
name ?? ""
);
isResetPage(false);
}}
resetPageNo={resetPage}
/>
</div>
</ListDiv>
</div>
</div>
</ContentsDiv>
<Toaster position="top-center" />
{/* 엑셀 등록 모달 */}
<UiModal
open={isExcelOpen}
onOpenChange={() => {
setIsExcelOpen(!isExcelOpen );
}}
bodyClass={"w-[35vw] max-w-none"}
body={<RuseCaseMngExcel setIsExcelOpen={setIsExcelOpen} />}
/>
</>
);
}
+256 -66
View File
@@ -1,40 +1,93 @@
import { z } from "zod";
export const reUseCaseExcel = z.object({
file: z.any()
file: z.any(),
});
type nameMap = {
[key: string]: string;
export interface ruseCaseMngExcelProps {
ruseBizNm: string;
bizBgngDate: string;
bizEndDate: string;
splyFcltKnd: string;
splyFcltNm: string;
splyFcltCpct: number | 0 | "";
dmdFcltNm: string;
dmdFcltAddr: string;
ruseFcltCpct: number | 0 | "";
ruseFcltPplLen: string;
ruseFcltUsg: string;
ruseFcltSplyCpct: number | 0 | "";
ruseFcltPrcsMsrc: string;
grnteWqUsg: string;
grnteWqGrssClfg: number | 0 | "";
grnteWqCombResdch: number | 0 | "";
grnteWqTrbt: string;
grnteWqBod: string;
grnteWqSmll: string;
grnteWqCrmty: string;
grnteWqTn: string;
grnteWqTp: string;
grnteWqPh: string;
grnteWqClIonQnt: string;
grnteWqCndctStat: string;
grnteWqRmrk: string;
reqBizReuseFcltCst: number | 0 | "";
reqBizPplCst: number | 0 | "";
reqBizArchCst: number | 0 | "";
reqBizMchnCst: number | 0 | "";
reqBizMtrngCst: number | 0 | "";
reqBizGisCst: number | 0 | "";
reqBizCctvCst: number | 0 | "";
reqBizCmcnCst: number | 0 | "";
reqBizWstCst: number | 0 | "";
reqBizDesignCst: number | 0 | "";
reqBizSpvsCst: number | 0 | "";
reqBizSbsdCst: number | 0 | "";
reqBizEtc: string;
reqBizGrssCst: number | 0 | "";
prcmPlanNlep: number | 0 | "";
prcmPlanLcep: number | 0 | "";
prcmPlanBrdnCst: number | 0 | "";
prcmPlanEcqrRt: number | 0 | "";
prcmPlanGrssCst: number | 0 | "";
clclnNlep: number | 0 | "";
clclnLcep: number | 0 | "";
clclnBrdnCst: number | 0 | "";
clclnEcqrRt: number | 0 | "";
clclnGrssCst: number | 0 | "";
}
type nameMap = {
[key: string]: string;
};
export const reUseCaseNames: nameMap = {
"사업명": "ruseBizNm",
"사업시작일": "bizBgngDate",
"사업종료일": "bizEndDate",
"공급처_시설종류": "splyFcltKnd",
"공급처명": "splyFcltNm",
사업명: "ruseBizNm",
사업시작일: "bizBgngDate",
사업종료일: "bizEndDate",
공급처_시설종류: "splyFcltKnd",
공급처명: "splyFcltNm",
"공급처_시설용량(㎥/일)": "splyFcltCpct",
"수요처명": "dmdFcltNm",
"수요처_주소": "dmdFcltAddr",
수요처명: "dmdFcltNm",
수요처_주소: "dmdFcltAddr",
"재이용시설_시설용량(㎥/일)": "ruseFcltCpct",
"재이용시설_관로(km)": "ruseFcltPplLen",
"재이용시설_용도": "ruseFcltUsg",
재이용시설_용도: "ruseFcltUsg",
"재이용시설_공급량(㎥/일)": "ruseFcltSplyCpct",
"재이용시설_처리공법": "ruseFcltPrcsMsrc",
"보증수질_용도별": "grnteWqUsg",
"보증수질_총대장균군수": "grnteWqGrssClfg",
"보증수질_결합잔류염소": "grnteWqCombResdch",
"보증수질_탁도": "grnteWqTrbt",
재이용시설_처리공법: "ruseFcltPrcsMsrc",
보증수질_용도별: "grnteWqUsg",
보증수질_총대장균군수: "grnteWqGrssClfg",
보증수질_결합잔류염소: "grnteWqCombResdch",
보증수질_탁도: "grnteWqTrbt",
"보증수질_생물학적산소요구량(BOD)(mg/L)": "grnteWqBod",
"보증수질_냄새": "grnteWqSmll",
보증수질_냄새: "grnteWqSmll",
"보증수질_색도(도)": "grnteWqCrmty",
"보증수질_총질소(T-N)(mg/L)": "grnteWqTn",
"보증수질_총인(T-P)(mg/L)": "grnteWqTp",
"보증수질_수소이온농도(pH)": "grnteWqPh",
"보증수질_염화물": "grnteWqClIonQnt",
"보증수질_전기전도도": "grnteWqCndctStat",
"보증수질_비고": "grnteWqRmrk",
보증수질_염화물: "grnteWqClIonQnt",
보증수질_전기전도도: "grnteWqCndctStat",
보증수질_비고: "grnteWqRmrk",
"소요사업비_총(백만원)": "reqBizGrssCst",
"소요사업비_토목_재이용시설(백만원)": "reqBizReuseFcltCst",
"소요사업비_토목_관로(백만원)": "reqBizPplCst",
@@ -58,52 +111,189 @@ export const reUseCaseNames: nameMap = {
"사업비 정산_국비(백만원)": "clclnNlep",
"사업비 정산_지방비(백만원)": "clclnLcep",
"사업비 정산_원인자부담(백만원)": "clclnBrdnCst",
"사업비 정산_국고비율(백만원)": "clclnEcqrRt"
}
"사업비 정산_국고비율(백만원)": "clclnEcqrRt",
};
// interface PbnstSttnItem {
// rvbsn: string;
// lgbsn: number;
// mdmbsn?: string;
// rgnNm?: string;
// sido?: number;
// gugun?: number;
// bscFctNm?: string;
// efflWqltStndrdNm?: string;
// fctCpc?: number;
// tel?: string;
// ruseUtztnFcltNm?: string;
// fcltCpct?: number;
// splyExtraCpct?: number;
// ruseCpct?: number;
// usageTotal?: number;
// inUsage?: number;
// outUsage?: number;
// }
type FieldRule =
| { kind: "string"; max: number; required?: boolean; pattern?: RegExp }
| { kind: "int"; digits: number; required?: boolean }
| {
kind: "decimal";
intDigits: number;
fracDigits: number;
required?: boolean;
};
// export const excelPbnstSttnParsing = (data: PbnstSttnItem[]): string[][] => {
// const excelExportData: string[][] = [];
export const ruseExcelRules: Record<keyof ruseCaseMngExcelProps, FieldRule> = {
ruseBizNm: { kind: "string", max: 150, required: true },
bizBgngDate: { kind: "string", max: 6, required: true },
bizEndDate: { kind: "string", max: 6 },
splyFcltKnd: { kind: "string", max: 50 },
splyFcltNm: { kind: "string", max: 150, required: true },
splyFcltCpct: { kind: "int", digits: 14 },
dmdFcltNm: { kind: "string", max: 150, required: true },
dmdFcltAddr: { kind: "string", max: 1000 },
ruseFcltCpct: { kind: "int", digits: 14 },
ruseFcltPplLen: { kind: "string", max: 50 },
ruseFcltUsg: { kind: "string", max: 150 },
ruseFcltSplyCpct: { kind: "int", digits: 14 },
ruseFcltPrcsMsrc: { kind: "string", max: 50 },
grnteWqUsg: { kind: "string", max: 50 },
grnteWqGrssClfg: { kind: "int", digits: 10 },
grnteWqCombResdch: { kind: "decimal", intDigits: 5, fracDigits: 3 },
grnteWqTrbt: { kind: "string", max: 10 },
grnteWqBod: { kind: "string", max: 10 },
grnteWqSmll: { kind: "string", max: 10 },
grnteWqCrmty: { kind: "string", max: 10 },
grnteWqTn: { kind: "string", max: 10 },
grnteWqTp: { kind: "string", max: 10 },
grnteWqPh: { kind: "string", max: 10 },
grnteWqClIonQnt: { kind: "string", max: 10 },
grnteWqCndctStat: { kind: "string", max: 10 },
grnteWqRmrk: { kind: "string", max: 1000 },
reqBizReuseFcltCst: { kind: "int", digits: 14 },
reqBizPplCst: { kind: "int", digits: 14 },
reqBizArchCst: { kind: "int", digits: 14 },
reqBizMchnCst: { kind: "int", digits: 14 },
reqBizMtrngCst: { kind: "int", digits: 14 },
reqBizGisCst: { kind: "int", digits: 14 },
reqBizCctvCst: { kind: "int", digits: 14 },
reqBizCmcnCst: { kind: "int", digits: 14 },
reqBizWstCst: { kind: "int", digits: 14 },
reqBizDesignCst: { kind: "int", digits: 14 },
reqBizSpvsCst: { kind: "int", digits: 14 },
reqBizSbsdCst: { kind: "int", digits: 14 },
reqBizEtc: { kind: "string", max: 1000 },
reqBizGrssCst: { kind: "int", digits: 14 },
prcmPlanNlep: { kind: "int", digits: 14 },
prcmPlanLcep: { kind: "int", digits: 14 },
prcmPlanBrdnCst: { kind: "int", digits: 14 },
prcmPlanEcqrRt: { kind: "decimal", intDigits: 3, fracDigits: 2 },
prcmPlanGrssCst: { kind: "int", digits: 14 },
clclnNlep: { kind: "int", digits: 14 },
clclnLcep: { kind: "int", digits: 14 },
clclnBrdnCst: { kind: "int", digits: 14 },
clclnEcqrRt: { kind: "decimal", intDigits: 3, fracDigits: 2 },
clclnGrssCst: { kind: "int", digits: 14 },
};
// data.forEach((item) => {
// excelExportData.push([
// item.rvbsn,
// String(item.lgbsn),
// item.mdmbsn ?? "",
// item.rgnNm ?? "",
// String(item.sido ?? ""),
// String(item.gugun ?? ""),
// String(item.bscFctNm ?? ""),
// String(item.efflWqltStndrdNm ?? ""),
// String(item.fctCpc ?? ""),
// String(item.tel ?? ""),
// String(item.ruseUtztnFcltNm ?? ""),
// String(item.fcltCpct ?? ""),
// String(item.splyExtraCpct ?? ""),
// String(item.ruseCpct ?? ""),
// String(item.usageTotal ?? ""),
// String(item.inUsage ?? ""),
// String(item.outUsage ?? ""),
// ])
// })
// return excelExportData;
// };
const notStringTrim = (str: any) => (str == null ? "" : String(str)).trim();
const stripNum = (str: string) => str.replace(/,/g, "").replace(/%/g, "");
// 2024.9 / 2024.09 / 202409 → YYYYMM
const isDateCheck = (raw: any) => {
const isDate = notStringTrim(raw);
return stripNum(isDate)
.replace(/[^0-9]/g, "")
.slice(0, 6);
};
const parseIntLike = (raw: any, digits: number): number | "" => {
const s = stripNum(notStringTrim(raw));
if (!s || !/^\d+$/.test(s) || s.length > digits) return "";
return Number(s);
};
const parseDecLike = (
raw: any,
intDigits: number,
fracDigits: number
): number | "" => {
const isData = stripNum(String(raw ?? "").trim());
// 빈값은 실패
if (!isData) return "";
// 허용: "123" / "123.45" / ".45" / "0.45"
if (!/^\d*(\.\d+)?$/.test(isData)) return "";
let num = isData.split(".")[0];
if (num === "") num = "0";
const fraction = isData.split(".")[1] ?? "";
// 정수부가 비었으면 0으로 보정 → ".123" → "0.123"
// 자리수 체크
if ((num && num.length > intDigits) || fraction.length > fracDigits)
return "";
return Number(`${num}${fraction ? "." + fraction : ""}`);
};
export function mapAndValidateRow(
item: Record<string, unknown>,
base: ruseCaseMngExcelProps
): { ok: boolean; errors: string[]; value?: ruseCaseMngExcelProps } {
const out: ruseCaseMngExcelProps = { ...base };
const errors: string[] = [];
const bizNm = notStringTrim(item?.["사업명"]); //에러 메시지 표출용
for (const [header, dest] of Object.entries(reUseCaseNames) as [
string,
keyof ruseCaseMngExcelProps,
][]) {
const rule = ruseExcelRules[dest];
let raw = item?.[header];
// 빈/대시 → ''
if (
raw == null ||
notStringTrim(raw) === "" ||
notStringTrim(raw) === "-"
) {
(out as any)[dest] = "";
if (rule.required) errors.push(`${bizNm} =>[${header}] 필수값 누락`);
continue;
}
if (dest === "bizBgngDate" || dest === "bizEndDate") {
raw = isDateCheck(raw);
}
if (rule.kind === "string") {
const value = notStringTrim(raw);
if (rule.required && !value) errors.push(`${bizNm} =>[${header}] 필수값`);
if (value && rule.max && value.length > rule.max)
errors.push(`${bizNm} =>[${header}] 최대 ${rule.max}자 초과`);
if (value && rule.pattern && !rule.pattern.test(value))
errors.push(`${bizNm} =>[${header}] 형식 오류`);
(out as any)[dest] = value;
} else if (rule.kind === "int") {
const n = parseIntLike(raw, rule.digits);
if (n === "") {
if (rule.required)
errors.push(`${bizNm} =>[${header}] 정수 ${rule.digits}자리 이내 필요`);
(out as any)[dest] = "";
} else {
(out as any)[dest] = n;
}
} else {
// decimal
const n = parseDecLike(raw, rule.intDigits, rule.fracDigits);
if (n === "") {
if (rule.required)
errors.push(
`[${header}] 소수(정수부≤${rule.intDigits}, 소수부≤${rule.fracDigits}) 필요`
);
(out as any)[dest] = "";
} else {
(out as any)[dest] = n;
}
}
}
// // PK 필수(가독성용)
// (["bizBgngDate", "ruseBizNm", "splyFcltNm", "dmdFcltNm"] as const).forEach(
// (key) => {
// const objValue=Object.entries(reUseCaseNames).find(([,val]) => val === key);
// if (!notStringTrim(out[key]) &&objValue) errors.push(`${bizNm} =>[PK] ${objValue[0]} 필수`);
// }
// );
return {
ok: errors.length === 0,
errors,
value: errors.length ? undefined : out,
};
}
+82 -74
View File
@@ -2,6 +2,7 @@ import { fetchType, getFetchWithCount, postFectch } from "@/components/lib/api";
import { toast } from "react-hot-toast";
import type { TableUpperProps } from "../../common/type/header";
import { ruseCaseHeadListExport } from "./headList";
import type { ruseCaseMngExcelProps } from "./excel";
export type ruseCaseState = {
ruseCaseMng: {
ruseHeadList: TableUpperProps[];
@@ -12,73 +13,53 @@ export type ruseCaseState = {
};
};
export interface ruseCaseMngExcelProps {
ruseBizNm: string,
bizBgngDate: string,
bizEndDate: string,
splyFcltKnd: string,
splyFcltNm: string,
splyFcltCpct: number | 0 | '',
dmdFcltNm: string,
dmdFcltAddr: string,
ruseFcltCpct: number | 0 | '',
ruseFcltPplLen: string,
ruseFcltUsg: string,
ruseFcltSplyCpct: number | 0 | '',
ruseFcltPrcsMsrc: string,
grnteWqUsg: string,
grnteWqGrssClfg: number | 0 | '',
grnteWqCombResdch: number | 0 | '',
grnteWqTrbt: string,
grnteWqBod: string,
grnteWqSmll: string,
grnteWqCrmty: string,
grnteWqTn: string,
grnteWqTp: string,
grnteWqPh: string,
grnteWqClIonQnt: string,
grnteWqCndctStat: string,
grnteWqRmrk: string,
reqBizReuseFcltCst: number | 0 | '',
reqBizPplCst: number | 0 | '',
reqBizArchCst: number | 0 | '',
reqBizMchnCst: number | 0 | '',
reqBizMtrngCst: number | 0 | '',
reqBizGisCst: number | 0 | '',
reqBizCctvCst: number | 0 | '',
reqBizCmcnCst: number | 0 | '',
reqBizWstCst: number | 0 | '',
reqBizDesignCst: number | 0 | '',
reqBizSpvsCst: number | 0 | '',
reqBizSbsdCst: number | 0 | '',
reqBizEtc: string,
reqBizGrssCst: number | 0 | '',
prcmPlanNlep: number | 0 | '',
prcmPlanLcep: number | 0 | '',
prcmPlanBrdnCst: number | 0 | '',
prcmPlanEcqrRt: number | 0 | '',
prcmPlanGrssCst: number | 0 | '',
clclnNlep: number | 0 | '',
clclnLcep: number | 0 | '',
clclnBrdnCst: number | 0 | '',
clclnEcqrRt: number | 0 | '',
clclnGrssCst: number | 0 | '',
}
type ruseCaseActions = {
ruseCaseMngInit: () => void;
selectRuseCaseMng: (isExcel: boolean, pageNo: number, pageSize: number, sidoCd: string, sigunguCd: string, usageCd: string, name: string) => Promise<void>;
selectRuseCaseMng: (
isExcel: boolean,
pageNo: number,
pageSize: number,
sidoCd: string,
sigunguCd: string,
usageCd: string,
name: string
) => Promise<void>;
insertRuseCaseMng: (data: any) => Promise<void>;
};
export const fetchSearchRuseCaseMng = async (pageNo: number, pageSize: number, sidoCd: string, sigunguCd: string, usageCd: string, name: string) => {
const tailUrl = '/' + pageNo + '/' + pageSize + ((sigunguCd !== '전체' && sigunguCd !== '') ? `/${sigunguCd}` : (sidoCd != '전체' && sidoCd != '') ? `/${sidoCd}` : `/0`) + (usageCd ? `/${usageCd}` : '/0') + (name ? `/${name}` : '');
const resp = await getFetchWithCount(fetchType.WR, `/reuseFc/pubFcRuseCase${tailUrl}`);
const fetchSearchRuseCaseMng = async (
pageNo: number,
pageSize: number,
sidoCd: string,
sigunguCd: string,
usageCd: string,
name: string
) => {
const tailUrl =
"/" +
pageNo +
"/" +
pageSize +
(sigunguCd !== "전체" && sigunguCd !== ""
? `/${sigunguCd}`
: sidoCd != "전체" && sidoCd != ""
? `/${sidoCd}`
: `/0`) +
(usageCd ? `/${usageCd}` : "/0") +
(name ? `/${name}` : "");
const resp = await getFetchWithCount(
fetchType.WR,
`/reuseFc/pubFcRuseCase${tailUrl}`
);
return { data: resp.data, count: resp.count };
};
export const fetchInsertRuseCaseMng = async (data: any) => {
const response = await postFectch('/reuseFc/pubFcRuseCase', data, fetchType.WR);
const fetchInsertRuseCaseMng = async (data: any) => {
const response = await postFectch(
"/reuseFc/pubFcRuseCase",
data,
fetchType.WR
);
return { data: response.data, status: response.status };
};
@@ -99,7 +80,10 @@ export const ruseCaseActionsExport: (set: any) => ruseCaseActions = (
) => {
return {
ruseCaseMngInit: async () => {
const res = await getFetchWithCount(fetchType.WR, `/reuseFc/pubFcRuseCase/1/10/0/0`);
const res = await getFetchWithCount(
fetchType.WR,
`/reuseFc/pubFcRuseCase/1/10/0/0`
);
set((state: ruseCaseState) => ({
ruseCaseMng: {
...state.ruseCaseMng,
@@ -108,8 +92,23 @@ export const ruseCaseActionsExport: (set: any) => ruseCaseActions = (
},
}));
},
selectRuseCaseMng: async (isExcel: boolean, pageNo: number, pageSize: number, sidoCd: string, sigunguCd: string, usageCd: string, name: string) => {
const res = await fetchSearchRuseCaseMng(pageNo, pageSize, sidoCd, sigunguCd, usageCd, name);
selectRuseCaseMng: async (
isExcel: boolean,
pageNo: number,
pageSize: number,
sidoCd: string,
sigunguCd: string,
usageCd: string,
name: string
) => {
const res = await fetchSearchRuseCaseMng(
pageNo,
pageSize,
sidoCd,
sigunguCd,
usageCd,
name
);
if (isExcel) {
set((state: ruseCaseState) => {
return {
@@ -117,7 +116,7 @@ export const ruseCaseActionsExport: (set: any) => ruseCaseActions = (
ruseCaseMng: {
...state.ruseCaseMng,
count: res.count,
excelList: res.data
excelList: res.data,
},
};
});
@@ -133,18 +132,27 @@ export const ruseCaseActionsExport: (set: any) => ruseCaseActions = (
},
insertRuseCaseMng: async (data: any) => {
const response = await fetchInsertRuseCaseMng(data);
if (response.status === 200) {
toast.success("물재이용사례 등록이 완료되었습니다. ");
} else {
try {
const response = await fetchInsertRuseCaseMng(data);
if (response.status === 200) {
toast.success("물재이용사례 등록이 완료되었습니다. ");
const res = await getFetchWithCount(
fetchType.WR,
`/reuseFc/pubFcRuseCase/1/10/0/0`
);
set((state: ruseCaseState) => ({
ruseCaseMng: {
...state.ruseCaseMng,
ruseCaseMngList: res.data,
count: res.count,
},
}));
} else {
}
} catch (e) {
console.log(e);
toast.error("물재이용사례 등록에 실패하였습니다. ");
}
set((state: ruseCaseState) => ({
ruseCaseMng: {
...state.ruseCaseMng,
status: response.status,
},
}));
},
};
};
};
@@ -158,7 +158,6 @@ export default function ChkgdExcel({
];
const issues: string[] = [];
console.log('listTotalData ', listTotalData)
rswtExcelDataCheck(listTotalData,issues)
if (issues && issues.length > 0) {
@@ -1,7 +1,7 @@
import { useWrStore } from "@/app/store";
import { coRegExcel } from "@/app/store/matDmd/coSttn/headList";
import { reUseCaseNames } from "@/app/store/matDmd/ruseCaseMng/excel";
import type { ruseCaseMngExcelProps } from "@/app/store/matDmd/ruseCaseMng/index";
import { mapAndValidateRow, type ruseCaseMngExcelProps } from "@/app/store/matDmd/ruseCaseMng/excel";
import { showConfirmToast } from "@/components/lib/func/commom";
import { filePicked, Title } from "@common/business_components/ui";
import { Form } from "@common/business_components/ui/form";
import { Button, Input } from "@common/components/ui";
@@ -10,7 +10,7 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import type { z } from "zod";
export default function RuseCaseMngExcel() {
export default function RuseCaseMngExcel({setIsExcelOpen}:{setIsExcelOpen: (isOpen:boolean)=>void}) {
const { insertRuseCaseMng } = useWrStore((state) => state);
const form = useForm<z.infer<typeof coRegExcel>>({
resolver: zodResolver(coRegExcel),
@@ -23,6 +23,7 @@ export default function RuseCaseMngExcel() {
filePicked(file, 0).then((res: unknown) => {
try {
const newArray: ruseCaseMngExcelProps[] = []
const isError:string[][] = []
const dataArray = Array.isArray(res) ? res : [];
dataArray.forEach((item: Record<string, unknown>) => {
const object: ruseCaseMngExcelProps = {
@@ -78,49 +79,74 @@ export default function RuseCaseMngExcel() {
clclnGrssCst: 0,
};
Object.keys(reUseCaseNames).forEach((key: string) => {
if (item && reUseCaseNames[key] !== undefined) {
const mappedKey = reUseCaseNames[key];
if (mappedKey && object.hasOwnProperty(mappedKey)) {
let value;
if(value === "-"){
value = '';
}
if(key === "사업시작일" || key === "사업종료일"){
if(item[key]?.toString().split(".")[1]?.length === 1){
value = `${item[key]?.toString().split(".")[0] || ""}${item[key]?.toString().split(".")[1]?.length === 1 ? "0" : ""}${item[key]?.toString().split(".")[1] || ""}`
}else{
value = `${item[key]?.toString().split(".")[0] || ""}${item[key]?.toString().split(".")[1] || ""}`
}
}else if(key === "재이용시설_관로(km)" || key?.toString().includes("보증수질_")){
value = item[key]?.toString().trim() || '';
}else{
if(item[key]?.toString().includes("%") && Number(item[key]?.toString().replaceAll("%", ""))){
value = item[key]?.toString().replaceAll("%", "");
}else{
value = item[key];
}
}
const parsingExcel= mapAndValidateRow(item, object);
// Object.keys(reUseCaseNames).forEach((key: string) => {
// if (item && reUseCaseNames[key] !== undefined) {
// const mappedKey = reUseCaseNames[key];
// if (mappedKey && object.hasOwnProperty(mappedKey)) {
// let value;
if(value === null || value?.toString().trim() === "-" || value?.toString().trim() === ""){
if (typeof object[mappedKey as keyof ruseCaseMngExcelProps] === 'number') {
(object as unknown as Record<string, unknown>)[mappedKey] = '';
} else {
(object as unknown as Record<string, unknown>)[mappedKey] = '';
}
}else if (typeof object[mappedKey as keyof ruseCaseMngExcelProps] === 'number' && Number(value?.toString().replaceAll("%", "").replaceAll(",", ""))) {
(object as unknown as Record<string, unknown>)[mappedKey] = Number(value?.toString().replaceAll("%", "").replaceAll(",", ""));
}else{
(object as unknown as Record<string, unknown>)[mappedKey] = value?.toString().trim() || '';
}
}
}
});
newArray.push(object);
// if(value === "-"){
// value = '';
// }
// if(key === "사업시작일" || key === "사업종료일"){
// if(item[key]?.toString().split(".")[1]?.length === 1){
// value = `${item[key]?.toString().split(".")[0] || ""}${item[key]?.toString().split(".")[1]?.length === 1 ? "0" : ""}${item[key]?.toString().split(".")[1] || ""}`
// }else{
// value = `${item[key]?.toString().split(".")[0] || ""}${item[key]?.toString().split(".")[1] || ""}`
// }
// }else if(key === "재이용시설_관로(km)" || key?.toString().includes("보증수질_")){
// value = item[key]?.toString().trim() || '';
// }else{
// if(item[key]?.toString().includes("%") && Number(item[key]?.toString().replaceAll("%", ""))){
// value = item[key]?.toString().replaceAll("%", "");
// }else{
// value = item[key];
// }
// }
// if(value === null || value?.toString().trim() === "-" || value?.toString().trim() === ""){
// if (typeof object[mappedKey as keyof ruseCaseMngExcelProps] === 'number') {
// (object as unknown as Record<string, unknown>)[mappedKey] = '';
// } else {
// (object as unknown as Record<string, unknown>)[mappedKey] = '';
// }
// }else if (typeof object[mappedKey as keyof ruseCaseMngExcelProps] === 'number' && Number(value?.toString().replaceAll("%", "").replaceAll(",", ""))) {
// (object as unknown as Record<string, unknown>)[mappedKey] = Number(value?.toString().replaceAll("%", "").replaceAll(",", ""));
// }else{
// (object as unknown as Record<string, unknown>)[mappedKey] = value?.toString().trim() || '';
// }
// }
// }
// });
if(parsingExcel.ok&&parsingExcel.value){
newArray.push(parsingExcel.value);
}else if(parsingExcel.errors){
isError.push(parsingExcel.errors);
}
});
console.log("newArray : ", newArray);
insertRuseCaseMng(newArray);
if(isError.length > 0){
const errorLength = 10;
let isErrorCount=0;
isError.forEach((err) => {
err.forEach((error) =>{
isErrorCount++;
if(isErrorCount < errorLength){
showConfirmToast(error)
}
})
})
return;
}else if(newArray.length === 0){
alert("업로드할 데이터가 없습니다.");
return;
}else if(newArray.length > 0){
insertRuseCaseMng(newArray);
setIsExcelOpen(false);
}
} catch (e) {
console.log(e)
}