diff --git a/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx b/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx index 813b4e23f..afea9d894 100644 --- a/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx +++ b/apps/wr/app/(with-layout)/matMng/coMat/ruseCaseMng/page.tsx @@ -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([]); - const [objSearch, setObjSearch] = useState({ sgg: "", city: "" }); + const { code, ruseCaseMng, selectRuseCaseMng, ruseCaseMngInit } = useWrStore( + (state) => state + ); + const { ruseCaseMngList, status, count, excelList } = ruseCaseMng; + const [selectSgg, setSelectSgg] = useState([]); + const [objSearch, setObjSearch] = useState({ + sgg: "", + city: "", + }); - const [sidoCd, setSidoCd] = useState(""); - const [sigunguCd, setSigunguCd] = useState(""); - // const [selectUtz, setSelectUtz] = useState(["조경용수"]); - const [selectUtz, setSelectUtz] = useState(""); - const [name, setName] = useState(""); - const [resetPage, isResetPage] = useState(false); - const { t } = useTranslation('base'); + const [sidoCd, setSidoCd] = useState(""); + const [sigunguCd, setSigunguCd] = useState(""); + // const [selectUtz, setSelectUtz] = useState(["조경용수"]); + const [selectUtz, setSelectUtz] = useState(""); + const [name, setName] = useState(""); + 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(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 ( - <> - -
-
- - <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 />} - /> - </> - ); -} \ No newline at end of file + </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} />} + /> + </> + ); +} diff --git a/apps/wr/app/store/matDmd/ruseCaseMng/excel.tsx b/apps/wr/app/store/matDmd/ruseCaseMng/excel.tsx index cd320cd06..40d2ce7d1 100644 --- a/apps/wr/app/store/matDmd/ruseCaseMng/excel.tsx +++ b/apps/wr/app/store/matDmd/ruseCaseMng/excel.tsx @@ -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; -// }; \ No newline at end of file +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, + }; +} \ No newline at end of file diff --git a/apps/wr/app/store/matDmd/ruseCaseMng/index.tsx b/apps/wr/app/store/matDmd/ruseCaseMng/index.tsx index 160e1f7f3..587e08fd9 100644 --- a/apps/wr/app/store/matDmd/ruseCaseMng/index.tsx +++ b/apps/wr/app/store/matDmd/ruseCaseMng/index.tsx @@ -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, - }, - })); }, }; -}; \ No newline at end of file +}; diff --git a/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx b/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx index 405061390..84dd7d8e3 100644 --- a/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx +++ b/apps/wr/components/ui/modal/chkgd/chkgdExcel/chkgdExcel.tsx @@ -158,7 +158,6 @@ export default function ChkgdExcel({ ]; const issues: string[] = []; - console.log('listTotalData ', listTotalData) rswtExcelDataCheck(listTotalData,issues) if (issues && issues.length > 0) { diff --git a/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx b/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx index 911dd2ed2..3d34155e1 100644 --- a/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx +++ b/apps/wr/components/ui/modal/coMat/ruseCaseMng/ruseCaseMngExcelModal.tsx @@ -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) }