From b6ca669c2e542988e48a4aa357d549bc3fa5f518 Mon Sep 17 00:00:00 2001 From: ljpark Date: Wed, 25 Feb 2026 18:15:25 +0900 Subject: [PATCH] =?UTF-8?q?dm=5Fnew:=20=EC=9A=B0=ED=8E=B8=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(landing)/reuse/_constants/lnb.ts | 2 +- .../app/(with-layout)/apply/dmdco/page.tsx | 3 + .../app/(with-layout)/apply/register/page.tsx | 742 ++++++++++++------ .../app/(with-layout)/intro/rain/page.tsx | 22 +- apps/dm_new/app/store/sdmMng/index.tsx | 2 + apps/dm_new/components/LoginModal.tsx | 6 +- .../ui/sdmInfoInsertForm/component.tsx | 10 +- apps/dm_new/public/favicon.ico | Bin 0 -> 4286 bytes 8 files changed, 538 insertions(+), 249 deletions(-) create mode 100644 apps/dm_new/public/favicon.ico diff --git a/apps/dm_new/app/(landing)/reuse/_constants/lnb.ts b/apps/dm_new/app/(landing)/reuse/_constants/lnb.ts index 49c9ae1ae..671dabd65 100644 --- a/apps/dm_new/app/(landing)/reuse/_constants/lnb.ts +++ b/apps/dm_new/app/(landing)/reuse/_constants/lnb.ts @@ -2,7 +2,7 @@ import type { LnbItem } from "../_components/Lnb"; export const LNB_DEMAND: LnbItem[] = [ - { href: "/reuse/apply/place", label: "수요처등록" }, + { href: "/reuse/apply/place", label: "수요처" }, { href: "/reuse/apply/register", label: "수요정보등록" }, { href: "/reuse/apply/status", label: "수요신청현황" }, ]; diff --git a/apps/dm_new/app/(with-layout)/apply/dmdco/page.tsx b/apps/dm_new/app/(with-layout)/apply/dmdco/page.tsx index 9c403c4bd..9422cd1ce 100644 --- a/apps/dm_new/app/(with-layout)/apply/dmdco/page.tsx +++ b/apps/dm_new/app/(with-layout)/apply/dmdco/page.tsx @@ -13,6 +13,7 @@ type Option = { value: string; label: string }; type SessionUser = { dmdInfoId?: string; dmdCoNm?: string; + zip?: string; daddr?: string; telno?: string; ctpvCd?: string; @@ -211,6 +212,7 @@ export default function DemandPlaceRegisterPage() { } setCompany(String(user.dmdCoNm ?? "")); + setZip(String(user.zip ?? "")); setAddr1(String(user.daddr ?? "")); setAddr2(""); setTel(formatKoreanPhoneNumber(String(user.telno ?? ""))); @@ -334,6 +336,7 @@ export default function DemandPlaceRegisterPage() { dmdCoNm: company.trim(), dmdInfoId: fullDmdInfoId, bzmnNo: bizDigits, + zip: zip.trim(), daddr: mergedAddress, telno: telDigits, rgtr: (sessionUser?.rgtr as string) || myUUID, diff --git a/apps/dm_new/app/(with-layout)/apply/register/page.tsx b/apps/dm_new/app/(with-layout)/apply/register/page.tsx index 1b0d684eb..2269ecb54 100644 --- a/apps/dm_new/app/(with-layout)/apply/register/page.tsx +++ b/apps/dm_new/app/(with-layout)/apply/register/page.tsx @@ -1,13 +1,82 @@ -// apps/dm/app/(landing)/reuse/apply/register/page.tsx +// app/(with-layout)/apply/register/page.tsx +/** + * 수요정보등록(수정) 화면 + * + * 🔗 사용 API + * - 수요정보등록: POST /reuseFc/request/demandInfo + * - 수요정보조회: GET /reuseFc/request/' + dmdInfoId - getSdmInfoStatusList + */ "use client"; -import { useMemo, useState, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useDmStore } from "@/app/store"; +import type { DemandCompBizType } from "@/app/store/sdmMng"; +import { putFetch } from "@/components/ui/api"; import SubPageHeader from "@/components/SubPageHeader"; import WaterQualityRefModal from "../modal/WaterQualityRefModal"; type PHRange = { min: string; max: string }; +type SessionUser = { + dmdInfoId?: string; + dmdCoNm?: string; + ctpvCd?: string; + sggCd?: string; + zip?: string; + daddr?: string; + telno?: string; + rgtr?: string; +}; + +const toNumberOrNull = (value: string, disabled = false) => { + if (disabled) return null; + const trimmed = value.trim(); + if (!trimmed) return null; + const parsed = Number(trimmed); + return Number.isNaN(parsed) ? null : parsed; +}; + +const toNumberOrEmpty = (value: string) => { + const trimmed = value.trim(); + if (!trimmed) return "" as const; + const parsed = Number(trimmed); + return Number.isNaN(parsed) ? ("" as const) : parsed; +}; + +const isNumeric = (value: string) => /^\d+(\.\d+)?$/.test(value.trim()); + +const formatDateForInput = (raw?: string | null) => { + if (!raw) return ""; + const parsed = new Date(raw); + if (Number.isNaN(parsed.getTime())) return ""; + const yyyy = parsed.getFullYear(); + const mm = String(parsed.getMonth() + 1).padStart(2, "0"); + const dd = String(parsed.getDate()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; +}; + +const parsePhRange = (raw?: string | null): PHRange => { + if (!raw) return { min: "", max: "" }; + const [min = "", max = ""] = raw.split("~").map((v) => v.trim()); + return { min, max }; +}; + +const getSessionUser = (): SessionUser | null => { + if (typeof window === "undefined") return null; + const raw = sessionStorage.getItem("user"); + if (!raw) return null; + try { + return JSON.parse(raw) as SessionUser; + } catch { + return null; + } +}; export default function DemandRegisterPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { insertSdmInfoData, Sdm } = useDmStore((state) => state); + const today = useMemo(() => { const d = new Date(); const yyyy = d.getFullYear(); @@ -17,279 +86,474 @@ export default function DemandRegisterPage() { }, []); const [openRef, setOpenRef] = useState(false); + const [editMode, setEditMode] = useState(false); + const [updateAllowed, setUpdateAllowed] = useState(true); + const [dmdInfoSn, setDmdInfoSn] = useState(0); + const [submitting, setSubmitting] = useState(false); // 기본정보 - const [reqDate, setReqDate] = useState(today); - const [reqQty, setReqQty] = useState(""); - const [usePurpose, setUsePurpose] = useState(""); - const [hopePrice, setHopePrice] = useState(""); + const [reqDate, setReqDate] = useState(today); // 요구시기 + const [reqQty, setReqQty] = useState(""); // 요구수량(m²/d) + const [usePurpose, setUsePurpose] = useState(""); // 사용용도 + const [hopePrice, setHopePrice] = useState(""); // 희망단가 + // 요구수질 - const [coliform, setColiform] = useState(""); - const [noDetect, setNoDetect] = useState(false); + const [coliform, setColiform] = useState(""); // 총대장균군수(개/mg) + const [noDetect, setNoDetect] = useState(false); // 총대장균군수 불검출 + + const [tn, setTn] = useState(""); // 총질소(T-N)(mg/L) + const [residualCl, setResidualCl] = useState(""); // 결합잔류염소(mg/L) + const [tp, setTp] = useState(""); // 총인(T-P)(mg/L) + const [turbidity, setTurbidity] = useState(""); // + const [ph, setPh] = useState({ min: "", max: "" }); // 수소이온농도(pH) + const [bod, setBod] = useState(""); // 생물화학적산소요구량(BOD)(mg/L) + const [chloride, setChloride] = useState(""); // 염화물(mgCL/L) + const [odor, setOdor] = useState("불쾌하지 않을 것"); // 냄새 + const [ec, setEc] = useState(""); // 전기전도도(µs/cm) + const [color, setColor] = useState(""); // 색도(도) + const [remark, setRemark] = useState(""); // 비고 - const [tn, setTn] = useState(""); - const [residualCl, setResidualCl] = useState(""); - const [tp, setTp] = useState(""); - const [turbidity, setTurbidity] = useState(""); - const [ph, setPh] = useState({ min: "", max: "" }); - const [bod, setBod] = useState(""); - const [chloride, setChloride] = useState(""); - const [odor, setOdor] = useState("불쾌하지 않을 것"); - const [ec, setEc] = useState(""); - const [color, setColor] = useState(""); - const [remark, setRemark] = useState(""); // 공급처 후보지 선택(필수아님) const [cand1, setCand1] = useState(""); const [cand2, setCand2] = useState(""); const [cand3, setCand3] = useState(""); + useEffect(() => { + const isEditParam = + searchParams.get("editMode") === "true" || + Boolean(searchParams.get("dmdInfoSn")); + setEditMode(isEditParam); + + const status = (searchParams.get("status") || "").trim(); + setUpdateAllowed(!isEditParam || status === "신청"); + + if (!isEditParam) return; + + const parsedSn = Number(searchParams.get("dmdInfoSn") || "0"); + setDmdInfoSn(Number.isNaN(parsedSn) ? 0 : parsedSn); + + setReqDate(formatDateForInput(searchParams.get("dmndTmng")) || today); + setReqQty(searchParams.get("dmndQty") || ""); + setUsePurpose(searchParams.get("useUsgCd") || ""); + setHopePrice(searchParams.get("wntUntprc") || ""); + + const rawColiform = searchParams.get("grssClfg") || ""; + const isNoDetect = rawColiform === "-1"; + setNoDetect(isNoDetect); + setColiform(isNoDetect ? "" : rawColiform); + + setTn(searchParams.get("tn") || ""); + setResidualCl(searchParams.get("combResdch") || ""); + setTp(searchParams.get("tph") || ""); + setTurbidity(searchParams.get("trbt") || ""); + setBod(searchParams.get("blgcOxygDmndQnt") || ""); + setChloride(searchParams.get("clIonQnt") || ""); + setEc(searchParams.get("cdtnv") || ""); + setColor(searchParams.get("crmty") || ""); + setRemark(searchParams.get("rmrk") || ""); + + const phRange = parsePhRange(searchParams.get("ph")); + setPh(phRange); + setOdor(searchParams.get("smll") || "불쾌하지 않을 것"); + }, [searchParams, today]); + + const validate = () => { + const disabledFields = Sdm.disableRules[usePurpose] || []; + const isDisabled = (field: string) => disabledFields.includes(field); + + if (!reqDate) { + alert("요구시기를 입력해주세요."); + return false; + } + if (!reqQty.trim()) { + alert("요구수량을 입력해주세요."); + return false; + } + if (!/^\d+$/.test(reqQty.trim())) { + alert("요구수량은 숫자만 입력 가능합니다."); + return false; + } + if (!usePurpose.trim()) { + alert("사용용도를 입력해주세요."); + return false; + } + if (!hopePrice.trim()) { + alert("희망단가를 입력해주세요."); + return false; + } + if (!/^\d+$/.test(hopePrice.trim())) { + alert("희망단가는 숫자만 입력 가능합니다."); + return false; + } + if (!noDetect && coliform.trim() && !isNumeric(coliform)) { + alert("총대장균군수는 숫자만 입력 가능합니다."); + return false; + } + + const requiredFields: Array<{ key: string; label: string; value: string }> = [ + { key: "tn", label: "총질소(T-N)(mg/L)", value: tn }, + { key: "combResdch", label: "결합잔류염소(mg/L)", value: residualCl }, + { key: "tph", label: "총인(T-P)(mg/L)", value: tp }, + { key: "trbt", label: "탁도(NTU)", value: turbidity }, + { key: "bod", label: "생물학적 산소요구량(BOD)(mg/L)", value: bod }, + { key: "clIonQnt", label: "염화물(mgCL/L)", value: chloride }, + { key: "smll", label: "냄새", value: odor }, + { key: "cdtnv", label: "전기전도도(㎲/cm)", value: ec }, + { key: "crmty", label: "색도(도)", value: color }, + ]; + + for (const field of requiredFields) { + if (isDisabled(field.key)) continue; + if (!field.value.trim()) { + alert(`${field.label} 을 입력해주세요.`); + return false; + } + if (field.key !== "smll" && !isNumeric(field.value)) { + alert(`${field.label} 은 숫자만 입력 가능합니다.`); + return false; + } + } + + if (!isDisabled("ph") && (!ph.min.trim() || !ph.max.trim())) { + alert("수소이온농도(pH) 을 입력해주세요."); + return false; + } + + return true; + }; + + const onSubmit = async () => { + if (submitting) return; + if (!validate()) return; + + if (editMode && !updateAllowed) { + alert("진행상태가 신청인 경우에만 수정할 수 있습니다."); + return; + } + + const sessionUser = getSessionUser(); + const idFromQuery = + (searchParams.get("dmdInfoId") || "") + (searchParams.get("bznmTotNo") || ""); + const dmdInfoId = String(sessionUser?.dmdInfoId || idFromQuery || "").trim(); + + if (!dmdInfoId) { + alert("수요처 정보가 없어 저장할 수 없습니다."); + return; + } + + const disabledFields = Sdm.disableRules[usePurpose] || []; + const isDisabled = (field: string) => disabledFields.includes(field); + const parsedPh = isDisabled("ph") ? "" : `${ph.min.trim()}~${ph.max.trim()}`; + const cands = [cand1, cand2, cand3] + .map((name) => name.trim()) + .filter(Boolean) + .map((name) => ({ + ruseUtztnFcltNm: name, + ruseUtztnFcltCd: "", + })) + .filter((item) => item.ruseUtztnFcltNm && item.ruseUtztnFcltCd); + + const payload: DemandCompBizType = { + index: 0, + dmdInfoId, + dmdInfoFk: dmdInfoId, + bzmnNo: "", + dmdCoNm: String(sessionUser?.dmdCoNm || searchParams.get("dmdCoNm") || ""), + zip: String(sessionUser?.zip || searchParams.get("zip") || ""), + daddr: String(sessionUser?.daddr || searchParams.get("daddr") || ""), + telno: String(sessionUser?.telno || searchParams.get("telno") || ""), + rgtr: String(sessionUser?.rgtr || searchParams.get("rgtr") || ""), + useUsgCd: usePurpose.trim(), + ctpvCd: String(sessionUser?.ctpvCd || searchParams.get("ctpvCd") || ""), + sggCd: String(sessionUser?.sggCd || searchParams.get("sggCd") || ""), + dmdAplyPswd: "", + wntUntprc: toNumberOrEmpty(hopePrice), + dmndTmng: reqDate ? new Date(reqDate) : "", + dmndQty: toNumberOrEmpty(reqQty), + grssClfg: noDetect ? -1 : toNumberOrNull(coliform), + tn: toNumberOrNull(tn, isDisabled("tn")), + combResdch: toNumberOrNull(residualCl, isDisabled("combResdch")), + tph: toNumberOrNull(tp, isDisabled("tph")), + trbt: toNumberOrNull(turbidity, isDisabled("trbt")), + ph: parsedPh, + blgcOxygDmndQnt: toNumberOrNull(bod, isDisabled("bod")), + clIonQnt: toNumberOrNull(chloride, isDisabled("clIonQnt")), + smll: isDisabled("smll") ? "" : odor.trim(), + cdtnv: toNumberOrNull(ec, isDisabled("cdtnv")), + crmty: toNumberOrNull(color, isDisabled("crmty")), + rmrk: remark.trim(), + reqDt: "", + dmdInfoSn: editMode ? dmdInfoSn : 0, + status: String(searchParams.get("status") || ""), + splyCndtList: cands, + }; + + try { + setSubmitting(true); + if (editMode) { + const updatePayload = { + ...payload, + mdfr: String(sessionUser?.rgtr || payload.rgtr || ""), + }; + await putFetch("/reuseFc/request/demandInfo", updatePayload); + alert("수요정보 수정 완료!"); + } else { + await insertSdmInfoData(payload); + } + router.push("/apply/status"); + } catch (error) { + console.error("Failed to submit demand info", error); + alert(editMode ? "수요정보 수정에 실패하였습니다." : "수요정보 등록에 실패하였습니다."); + } finally { + setSubmitting(false); + } + }; + return ( <> -
+
- {/* 서브 타이틀 + 브레드크럼 */} - + {/* 서브 타이틀 + 브레드크럼 */} + - {/* 컨텐츠 영역 */} -
+ {/* 컨텐츠 영역 */} +
- {/* 기본정보 테이블 */} -
- - - + {/* 기본정보 테이블 */} +
+ + + - + - - - + + + - + - - - + + + - + - - - + + + +
+ + {/* 요구수질 섹션 타이틀 바 */} +
+
+ 요구수질 + + {/* 수질기준참고 모달 */} + setOpenRef(false)} + imageSrc="/images/landing/water_quality_ref_1.png" + />
+
- {/* 요구수질 섹션 타이틀 바 */} -
-
- 요구수질 - - {/* 수질기준참고 모달 */} - setOpenRef(false)} - imageSrc="/images/landing/water_quality_ref_1.png" + {/* 요구수질 테이블 */} +
+ {/* 1행 */} + + + +
+ } + rightLabel="총질소(T-N)(mg/L)" + rightControl={ + + } + /> + + + {/* 2행 */} + } + rightLabel="총인(T-P)(mg/L)" + rightControl={ + + } + /> + + + {/* 3행 */} + + } + rightLabel="수소이온농도(pH)" + rightControl={ +
+ setPh((p) => ({ ...p, min: v }))} + className="w-32" + /> + ~ + setPh((p) => ({ ...p, max: v }))} + className="w-32" />
-
+ } + /> + - {/* 요구수질 테이블 */} -
- {/* 1행 */} - - - -
- } - rightLabel="총질소(T-N)(mg/L)" - rightControl={ - - } - /> - - - {/* 2행 */} - } - rightLabel="총인(T-P)(mg/L)" - rightControl={ - - } - /> - - - {/* 3행 */} - - } - rightLabel="수소이온농도(pH)" - rightControl={ -
- setPh((p) => ({ ...p, min: v }))} - className="w-32" - /> - ~ - setPh((p) => ({ ...p, max: v }))} - className="w-32" - /> -
- } - /> - - - {/* 4행 */} - - 생물화학적산소요구량 -
- (BOD)(mg/L) - - } - leftRequired - leftControl={} - rightLabel="염화물(mgCL/L)" - rightControl={} - /> - - - {/* 5행 */} - setOdor(e.target.value)} - className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500" - > - - - - - } - rightLabel="전기전도도(µs/cm)" - rightControl={} - /> - - - {/* 6행 */} - } - rightLabel="비고" - rightControl={ -