diff --git a/.gitignore b/.gitignore index 3327a728e..c5082bba3 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ pnpm-lock.yaml settings.json .npmrc .cursor + +apps/**/tsconfig.tsbuildinfo diff --git a/apps/dm_new/app/(landing)/reuse/_components/PostcodeModal.tsx b/apps/dm_new/app/(landing)/reuse/_components/PostcodeModal.tsx new file mode 100644 index 000000000..83fe82aea --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/_components/PostcodeModal.tsx @@ -0,0 +1,298 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +export type AddrItem = { + zipNo: string; // 우편번호 + roadAddr: string; // 도로명주소(전체 문장) + jibunAddr: string; // 지번주소 +}; + +type Props = { + open: boolean; + onClose: () => void; + onSelect: (item: AddrItem) => void; +}; + +type PostcodeApiResponse = { + totalCount?: number; + items?: AddrItem[]; +}; + +function useDebouncedValue(value: T, delay = 250) { + const [v, setV] = useState(value); + useEffect(() => { + const t = setTimeout(() => setV(value), delay); + return () => clearTimeout(t); + }, [value, delay]); + return v; +} + +// A: 추천은 roadAddr 전체 문장 +const normalize = (s: string) => s.replace(/\s+/g, " ").trim(); + +const makeSuggestionsA = (list: AddrItem[], keyword: string, limit = 7) => { + const key = normalize(keyword); + + const roadAddrs = list + .map((x) => x.roadAddr) + .filter((v): v is string => typeof v === "string" && v.trim().length > 0) + .map(normalize); + + const uniq = Array.from(new Set(roadAddrs)); + + // 입력어 포함 우선 + 짧은 주소 우선 + uniq.sort((a, b) => { + const ai = a.includes(key) ? 0 : 1; + const bi = b.includes(key) ? 0 : 1; + if (ai !== bi) return ai - bi; + return a.length - b.length; + }); + + return uniq.slice(0, limit); +}; + +export default function PostcodeModal({ open, onClose, onSelect }: Props) { + const [mounted, setMounted] = useState(false); + + const [q, setQ] = useState(""); + const dq = useDebouncedValue(q, 250); + + const [suggestions, setSuggestions] = useState([]); + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + + const inputRef = useRef(null); + + useEffect(() => setMounted(true), []); + + // open 시: body scroll lock + focus + ESC + useEffect(() => { + if (!open) return; + + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + + setTimeout(() => inputRef.current?.focus(), 50); + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKeyDown); + + return () => { + document.body.style.overflow = prevOverflow; + window.removeEventListener("keydown", onKeyDown); + }; + }, [open, onClose]); + + // 검색/추천/리스트 갱신 + useEffect(() => { + if (!open) return; + + const keyword = dq.trim(); + + if (!keyword) { + setSuggestions([]); + setItems([]); + setTotal(0); + setLoading(false); + return; + } + + let alive = true; + + (async () => { + setLoading(true); + try { + const res = await fetch(`/api/postcode?query=${encodeURIComponent(keyword)}&page=1`, { + cache: "no-store", + }); + + const data = (await res.json()) as PostcodeApiResponse; + + if (!alive) return; + + const list: AddrItem[] = data.items ?? []; + const totalCount: number = data.totalCount ?? 0; + + setItems(list); + setTotal(totalCount); + + // ✅ A: roadAddr 전체 문장 추천 + const sug = makeSuggestionsA(list, keyword, 7); + setSuggestions(sug); + } catch { + if (!alive) return; + setItems([]); + setTotal(0); + setSuggestions([]); + } finally { + if (alive) setLoading(false); + } + })(); + + return () => { + alive = false; + }; + }, [dq, open]); + + const showSuggest = open && q.trim().length > 0 && suggestions.length > 0; + + if (!mounted || !open) return null; + + return createPortal( +
+ {/* overlay */} + +
+ + {/* search input */} +
+
+ setQ(e.target.value)} + placeholder="주소검색" + className={[ + "w-full rounded-md border px-4 py-3 pr-12 text-[14px] outline-none", + "border-slate-300 focus:border-sky-500 focus:ring-2 focus:ring-sky-200", + ].join(" ")} + /> + + + + {/* suggestions dropdown */} + {showSuggest && ( +
+ {suggestions.map((s) => ( + + ))} +
+ )} +
+
+ + {/* results */} +
+
+ {q.trim() + ? loading + ? "검색 중…" + : `총 ${total.toLocaleString()}개의 검색결과` + : "주소를 입력하면 검색결과가 표시됩니다."} +
+ +
+ {!loading && q.trim() && items.length === 0 && ( +
+ 검색 결과가 없습니다. +
+ )} + +
+ {items.map((it) => ( + + ))} +
+
+
+ + + , + document.body + ); +} \ No newline at end of file diff --git a/apps/dm_new/app/(landing)/reuse/_components/SubPageHeader.tsx b/apps/dm_new/app/(landing)/reuse/_components/SubPageHeader.tsx index e4518c478..b18ee28a6 100644 --- a/apps/dm_new/app/(landing)/reuse/_components/SubPageHeader.tsx +++ b/apps/dm_new/app/(landing)/reuse/_components/SubPageHeader.tsx @@ -19,10 +19,10 @@ export default function SubPageHeader({ title, crumbs, note = "* 는 필수 값입니다.", - underlineWidthClass = "w-40", + // underlineWidthClass = "w-40", className = "", wrapClassName = "", - hideFooterLine = false, + // hideFooterLine = false, hideGuideText = false, }: Props) { return ( diff --git a/apps/dm_new/app/(landing)/reuse/_components/TopNav.tsx b/apps/dm_new/app/(landing)/reuse/_components/TopNav.tsx index 3d9d38ccc..8174da2cf 100644 --- a/apps/dm_new/app/(landing)/reuse/_components/TopNav.tsx +++ b/apps/dm_new/app/(landing)/reuse/_components/TopNav.tsx @@ -80,7 +80,8 @@ function DemandLoginModal({ open, onClose, onSuccess }: LoginModalProps) { if (noWorker) { setWorkerNo(""); setErrors((p) => { - const { workerNo: _w, ...rest } = p; + const { ...rest } = p; + delete rest.workerNo; return rest; }); } 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/(landing)/reuse/apply/modal/CandidateSiteModal.tsx b/apps/dm_new/app/(landing)/reuse/apply/modal/CandidateSiteModal.tsx new file mode 100644 index 000000000..aa5a9c0ee --- /dev/null +++ b/apps/dm_new/app/(landing)/reuse/apply/modal/CandidateSiteModal.tsx @@ -0,0 +1,327 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +export type CandidateSite = { + id: string; + name: string; + sewageM3d: number; // 하수처리량 + reuseM3d: number; // 재이용량 + // 지도 연동 시 사용(현재는 UI/구조용) + lat?: number; + lng?: number; +}; + +type Props = { + open: boolean; + onClose: () => void; + onApply: (selected: CandidateSite[]) => void; + initialSelected?: CandidateSite[]; + maxSelect?: number; // 기본 3개 +}; + +const MOCK_SITES: CandidateSite[] = [ + { id: "1", name: "서호생태 수자원 재이용1", sewageM3d: 1300, reuseM3d: 1300, lat: 35.0, lng: 128.0 }, + { id: "2", name: "서호생태 수자원 재이용2", sewageM3d: 1300, reuseM3d: 1300, lat: 35.01, lng: 128.01 }, + { id: "3", name: "서호생태 수자원 재이용3", sewageM3d: 1300, reuseM3d: 1300, lat: 35.02, lng: 128.02 }, +]; + +export default function CandidateSiteModal({ + open, + onClose, + onApply, + initialSelected = [], + maxSelect = 3, +}: Props) { + const [q, setQ] = useState(""); + const [selected, setSelected] = useState(initialSelected); + + // 지도 마커 컴포넌트 + function MapMarker({ label }: { label?: string }) { + return ( +
+ + + + + + + + + + + + + {/* pin body */} + + + {/* 내부 흰 원 */} + + + {label && ( + + {label} + + )} + +
+ ); + } + + // 모달 열릴 때마다 초기 선택값 동기화 + useEffect(() => { + if (open) setSelected(initialSelected); + if (open) setQ(""); + }, [open, initialSelected]); + + // ESC 닫기 + useEffect(() => { + if (!open) return; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKeyDown); + return () => window.removeEventListener("keydown", onKeyDown); + }, [open, onClose]); + + const filtered = useMemo(() => { + const keyword = q.trim(); + if (!keyword) return MOCK_SITES; + return MOCK_SITES.filter((s) => s.name.includes(keyword)); + }, [q]); + + const isSelected = (id: string) => selected.some((s) => s.id === id); + + const toggleSelect = (item: CandidateSite) => { + setSelected((prev) => { + const exists = prev.some((p) => p.id === item.id); + if (exists) return prev.filter((p) => p.id !== item.id); + + if (prev.length >= maxSelect) return prev; // 최대 개수 초과 방지 + return [...prev, item]; + }); + }; + + const removeChip = (id: string) => setSelected((prev) => prev.filter((p) => p.id !== id)); + + if (!open) return null; + + return ( +
+ {/* overlay */} + +
+
+ + {/* body */} +
+
+ {/* left: search + table */} +
+
후보지 검색
+ +
+ setQ(e.target.value)} + placeholder="주소검색" + className="h-12 w-full rounded-sm border border-neutral-300 bg-white px-4 pr-12 text-sm outline-none focus:border-sky-500" + /> + +
+ + {/* table header */} +
+
+
시설명
+
+ 하수처리량
(m³/일) +
+
+ 재이용량
(m³/일) +
+
선택
+
+ + {/* rows */} +
+ {filtered.length === 0 ? ( +
+ 검색 결과가 없습니다. +
+ ) : ( + filtered.map((s, idx) => { + const picked = isSelected(s.id); + const disabled = !picked && selected.length >= maxSelect; + return ( +
+
+ {idx + 1} + {s.name} +
+
{s.sewageM3d.toLocaleString()}
+
{s.reuseM3d.toLocaleString()}
+
+ +
+
+ ); + }) + )} +
+
+ + {/*
+ 최대 {maxSelect}개까지 선택 가능합니다. +
*/} +
+ + {/* right: map */} +
+
+ {/* TODO: 실제 지도 SDK 연동 시 이 영역 교체 */} +
+ 지도 영역(추후 지도 SDK 연동) +
+ + {/* 선택된 항목의 "가짜 마커" */} + {selected.map((s, i) => { + const POS = [ + { top: "40%", left: "45%" }, + { top: "25%", left: "75%" }, + { top: "70%", left: "80%" }, + ] as const; + + const pos = POS[i % POS.length] ?? POS[0]; + + return ( +
+ +
+ ); + })} +
+
+
+ + {/* selected chips */} +
+
+ 선택된 시설 (시설명/하수처리량(m³/일)/재이용량(m³/일)) +
+ +
+ {selected.length === 0 ? ( +
선택된 시설이 없습니다.
+ ) : ( +
+ {selected.map((s) => ( +
+ + {s.name} / {s.sewageM3d.toLocaleString()}(m³/일) / {s.reuseM3d.toLocaleString()}(m³/일) + + +
+ ))} +
+ )} +
+
+ + {/* footer button */} +
+ +
+
+
+ + ); +} \ No newline at end of file diff --git a/apps/dm_new/app/(landing)/reuse/apply/place/page.tsx b/apps/dm_new/app/(landing)/reuse/apply/place/page.tsx index 2deb0334e..25591d9e6 100644 --- a/apps/dm_new/app/(landing)/reuse/apply/place/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/apply/place/page.tsx @@ -1,8 +1,11 @@ // apps/dm/app/(landing)/reuse/apply/place/page.tsx "use client"; +import React from "react"; import { useMemo, useState } from "react"; import SubPageHeader from "../../_components/SubPageHeader"; +import PostcodeModal from "../../_components/PostcodeModal"; +import type { AddrItem } from "../../_components/PostcodeModal"; type Option = { value: string; label: string }; type SidoValue = "" | "seoul" | "busan" | "daegu" | "incheon" | "gyeonggi"; @@ -75,9 +78,7 @@ function FormRow({ {/* 입력(우측) */} -
- {children} -
+
{children}
); } @@ -145,6 +146,25 @@ function SelectBox({ } export default function DemandPlaceRegisterPage() { + /** ✅ 모달 + 주소 상태 (반드시 컴포넌트 내부!) */ + const [open, setOpen] = useState(false); + + // 모달에서 선택한 값은 이 3개에 반영 (기존 UI zip/addr1/addr2와 맞춤) + const [zip, setZip] = useState(""); + const [addr1, setAddr1] = useState(""); // 도로명 주소 넣을 곳 + const [addr2, setAddr2] = useState(""); // 상세주소(사용자 입력) + + // (필요하면 유지) 지번도 따로 보관하고 싶을 때 + // const [jibun, setJibun] = useState(""); + + const handleSelect = (it: AddrItem) => { + setZip(it.zipNo); + setAddr1(it.roadAddr); // ✅ 주소칸에 도로명 자동 입력 + // setJibun(it.jibunAddr); // ✅ 지번도 상태로 보관(원하면 UI에 표시) + setOpen(false); + }; + + /** 기존 폼 상태들 */ const [sido, setSido] = useState(""); const [sigungu, setSigungu] = useState(""); @@ -153,10 +173,6 @@ export default function DemandPlaceRegisterPage() { const [workerNo, setWorkerNo] = useState(""); const [noWorkerNo, setNoWorkerNo] = useState(false); - const [zip, setZip] = useState(""); - const [addr1, setAddr1] = useState(""); - const [addr2, setAddr2] = useState(""); - const [tel, setTel] = useState(""); const [pw, setPw] = useState(""); const [pw2, setPw2] = useState(""); @@ -222,11 +238,7 @@ export default function DemandPlaceRegisterPage() {
- +
@@ -234,11 +246,7 @@ export default function DemandPlaceRegisterPage() {
- +
@@ -278,21 +286,24 @@ export default function DemandPlaceRegisterPage() {
+ + {/* ✅ 선택된 도로명주소가 자동으로 들어감 */} - + + {/* ✅ 상세주소는 사용자 입력 */} + + + {/* (선택) 지번주소도 보여주고 싶으면 */} + {/* {jibun ?

지번: {jibun}

: null} */} @@ -300,11 +311,7 @@ export default function DemandPlaceRegisterPage() {
- +
@@ -312,12 +319,7 @@ export default function DemandPlaceRegisterPage() {
- +
@@ -345,6 +347,9 @@ export default function DemandPlaceRegisterPage() { 수요처 등록 + + {/* ✅ 모달 */} + setOpen(false)} onSelect={handleSelect} /> ); -} +} \ No newline at end of file diff --git a/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx b/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx index 27883a981..a8a7297d3 100644 --- a/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/apply/register/page.tsx @@ -4,6 +4,7 @@ import { useMemo, useState, useRef } from "react"; import SubPageHeader from "../../_components/SubPageHeader"; import WaterQualityRefModal from "../modal/WaterQualityRefModal"; +import CandidateSiteModal, { type CandidateSite } from "../modal/CandidateSiteModal"; type PHRange = { min: string; max: string }; @@ -18,6 +19,9 @@ export default function DemandRegisterPage() { const [openRef, setOpenRef] = useState(false); + const [openCand, setOpenCand] = useState(false); + const [selectedCands, setSelectedCands] = useState([]); + // 기본정보 const [reqDate, setReqDate] = useState(today); const [reqQty, setReqQty] = useState(""); @@ -251,10 +255,7 @@ export default function DemandRegisterPage() { @@ -290,6 +291,22 @@ export default function DemandRegisterPage() { + + setOpenCand(false)} + initialSelected={selectedCands} + onApply={(list) => { + setSelectedCands(list); + + // ✅ cand1~3 입력칸에 반영(시설명만 넣는 버전) + setCand1(list[0]?.name ?? ""); + setCand2(list[1]?.name ?? ""); + setCand3(list[2]?.name ?? ""); + + setOpenCand(false); + }} + /> ); } diff --git a/apps/dm_new/app/(landing)/reuse/apply/status/page.tsx b/apps/dm_new/app/(landing)/reuse/apply/status/page.tsx index 1a2493668..b7493f844 100644 --- a/apps/dm_new/app/(landing)/reuse/apply/status/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/apply/status/page.tsx @@ -262,7 +262,16 @@ export default function DemandStatusPage() { {/* 상세현황 */}
-
수요신청 상세현황
+ +
+
수요신청 상세현황
+ +
{!selected ? (
diff --git a/apps/dm_new/app/(landing)/reuse/business/globalCase/australia/page.tsx b/apps/dm_new/app/(landing)/reuse/business/globalCase/australia/page.tsx index aca82a23d..fa9eaad72 100644 --- a/apps/dm_new/app/(landing)/reuse/business/globalCase/australia/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/globalCase/australia/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/globalCase/eu/page.tsx b/apps/dm_new/app/(landing)/reuse/business/globalCase/eu/page.tsx index e18af7fed..9e5300b39 100644 --- a/apps/dm_new/app/(landing)/reuse/business/globalCase/eu/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/globalCase/eu/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/globalCase/overview/page.tsx b/apps/dm_new/app/(landing)/reuse/business/globalCase/overview/page.tsx index c9b2f1c29..b8ad773af 100644 --- a/apps/dm_new/app/(landing)/reuse/business/globalCase/overview/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/globalCase/overview/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/globalCase/singapore/page.tsx b/apps/dm_new/app/(landing)/reuse/business/globalCase/singapore/page.tsx index 175c4da57..a876a3180 100644 --- a/apps/dm_new/app/(landing)/reuse/business/globalCase/singapore/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/globalCase/singapore/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/globalCase/taiwan/page.tsx b/apps/dm_new/app/(landing)/reuse/business/globalCase/taiwan/page.tsx index aec2a63de..b296ae298 100644 --- a/apps/dm_new/app/(landing)/reuse/business/globalCase/taiwan/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/globalCase/taiwan/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/rain/suwon/page.tsx b/apps/dm_new/app/(landing)/reuse/business/rain/suwon/page.tsx index aec0b6f6a..3acba860b 100644 --- a/apps/dm_new/app/(landing)/reuse/business/rain/suwon/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/rain/suwon/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/sewage/bolyeong/page.tsx b/apps/dm_new/app/(landing)/reuse/business/sewage/bolyeong/page.tsx index be4cafab2..04569f264 100644 --- a/apps/dm_new/app/(landing)/reuse/business/sewage/bolyeong/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/sewage/bolyeong/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/sewage/cheongju/page.tsx b/apps/dm_new/app/(landing)/reuse/business/sewage/cheongju/page.tsx index 885315b86..1cddcae80 100644 --- a/apps/dm_new/app/(landing)/reuse/business/sewage/cheongju/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/sewage/cheongju/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/sewage/guri/page.tsx b/apps/dm_new/app/(landing)/reuse/business/sewage/guri/page.tsx index 5b40fdd2a..638a8c8b6 100644 --- a/apps/dm_new/app/(landing)/reuse/business/sewage/guri/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/sewage/guri/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/sewage/hwanggu/page.tsx b/apps/dm_new/app/(landing)/reuse/business/sewage/hwanggu/page.tsx index a3d9164c2..ef0baa15d 100644 --- a/apps/dm_new/app/(landing)/reuse/business/sewage/hwanggu/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/sewage/hwanggu/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/sewage/paju/page.tsx b/apps/dm_new/app/(landing)/reuse/business/sewage/paju/page.tsx index d620165cd..7c76f0fa9 100644 --- a/apps/dm_new/app/(landing)/reuse/business/sewage/paju/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/sewage/paju/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/business/sewage/yeosu/page.tsx b/apps/dm_new/app/(landing)/reuse/business/sewage/yeosu/page.tsx index aa2dc0200..e86d61b92 100644 --- a/apps/dm_new/app/(landing)/reuse/business/sewage/yeosu/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/business/sewage/yeosu/page.tsx @@ -21,22 +21,22 @@ function Hr({ className = "" }: HrProps) { return
; } -type ImgCardProps = { - src: string; - alt: string; -}; +// type ImgCardProps = { +// src: string; +// alt: string; +// }; -function ImgCard({ src, alt }: ImgCardProps) { - return ( -
- {alt} -
- ); -} +// function ImgCard({ src, alt }: ImgCardProps) { +// return ( +//
+// {alt} +//
+// ); +// } export default function GreywaterPage() { return ( diff --git a/apps/dm_new/app/(landing)/reuse/intro/rain/page.tsx b/apps/dm_new/app/(landing)/reuse/intro/rain/page.tsx index 5d9e7e0e3..bb9dc37db 100644 --- a/apps/dm_new/app/(landing)/reuse/intro/rain/page.tsx +++ b/apps/dm_new/app/(landing)/reuse/intro/rain/page.tsx @@ -65,12 +65,33 @@ export default function RainwaterReusePage() {
{/* 2) 빗물이용시설 설치 관리 법적 의무 대상 */} -
+
빗물이용시설 설치 관리 법적 의무 대상
+ +
+ + {/* 3) 빗물이용시설 설계 방안 */} +
+ 빗물이용시설 설계 방안 +
+ +
+
+ +
+ + {/* 4) 빗물이용시설 시설기준 및 관리기준 */} +
+ 빗물이용시설 시설기준 및 관리기준 +
+ +
+
+
); } diff --git a/apps/dm_new/app/(landing)/reuse/layout.tsx b/apps/dm_new/app/(landing)/reuse/layout.tsx index 0f0e274cd..d003de516 100644 --- a/apps/dm_new/app/(landing)/reuse/layout.tsx +++ b/apps/dm_new/app/(landing)/reuse/layout.tsx @@ -1,6 +1,6 @@ // apps/dm/app/(landing)/reuse/layout.tsx import "./reuse.module.css"; -import type { ReactNode } from "react"; +// import type { ReactNode } from "react"; import TopNav from "./_components/TopNav"; export default function ReuseLandingLayout({ children }: { children: React.ReactNode }) { 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 c150ee1f7..9422cd1ce 100644 --- a/apps/dm_new/app/(with-layout)/apply/dmdco/page.tsx +++ b/apps/dm_new/app/(with-layout)/apply/dmdco/page.tsx @@ -1,7 +1,9 @@ "use client"; -import { useEffect, useMemo, useState, type ReactNode } from "react"; +import { useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react"; import SubPageHeader from "@/components/SubPageHeader"; +import PostcodeModal from "@/components/PostcodeModal"; +import type { AddrItem } from "@/app/store/sdmMng/postcode-service"; import { useDmStore } from "@/app/store"; import { fetchType, getFetch, postFetch, putFetch } from "@/components/ui/api"; import { getCookie } from "@common/components/lib"; @@ -11,6 +13,7 @@ type Option = { value: string; label: string }; type SessionUser = { dmdInfoId?: string; dmdCoNm?: string; + zip?: string; daddr?: string; telno?: string; ctpvCd?: string; @@ -21,6 +24,42 @@ type SessionUser = { const TOKEN_ID_COOKIE = "token-id"; const FALLBACK_UUID = "c475824c-0e82-4a06-a282-2b55a20df265"; +const normalizeRegionName = (value: string) => value.replace(/\s+/g, "").trim(); +const isSameRegionName = (left: string, right: string) => { + const a = normalizeRegionName(left); + const b = normalizeRegionName(right); + return Boolean(a && b && (a === b || a.includes(b) || b.includes(a))); +}; +const formatKoreanPhoneNumber = (value: string) => { + const digits = value.replace(/\D/g, "").slice(0, 11); // 숫자만 추출하고 최대 11자리까지만 사용 + if (!digits) return ""; // 입력이 비어 있으면 빈 문자열 반환 + + if (/^1\d{0,7}$/.test(digits)) { // 15xx/16xx/18xx 같은 대표번호 패턴 + if (digits.length <= 4) return digits; // 아직 뒷자리가 부족하면 원문 유지 + return `${digits.slice(0, 4)}-${digits.slice(4, 8)}`; // xxxx-xxxx + } + + if (digits.startsWith("02")) { // 서울 지역번호(02) + const rest = digits.slice(2); // 국번/가입자번호 구간 + if (!rest) return "02"; // 02만 입력된 상태 + if (rest.length <= 3) return `02-${rest}`; // 02-xxx + if (rest.length <= 7) return `02-${rest.slice(0, 3)}-${rest.slice(3)}`; // 02-xxx-xxxx + return `02-${rest.slice(0, 4)}-${rest.slice(4, 8)}`; // 02-xxxx-xxxx + } + + if (digits.startsWith("0")) { // 02 제외 일반 지역번호/휴대폰(010 포함) + const area = digits.slice(0, 3); // 앞 3자리 식별자 + const rest = digits.slice(3); // 나머지 번호 + if (!rest) return area; // 식별자만 입력된 상태 + if (rest.length <= 3) return `${area}-${rest}`; // 0xx-xxx + if (rest.length <= 7) return `${area}-${rest.slice(0, 3)}-${rest.slice(3)}`; // 0xx-xxx-xxxx + return `${area}-${rest.slice(0, 4)}-${rest.slice(4, 8)}`; // 0xx-xxxx-xxxx + } + + if (digits.length <= 4) return digits; // 예외 입력: 앞 4자리까지 + if (digits.length <= 8) return `${digits.slice(0, 4)}-${digits.slice(4)}`; // xxxx-xxxx + return `${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8, 11)}`; // xxxx-xxxx-xxx +}; function Divider() { return
; @@ -55,6 +94,7 @@ function TextInput({ placeholder, type = "text", disabled, + inputRef, className = "", }: { value: string; @@ -62,10 +102,12 @@ function TextInput({ placeholder?: string; type?: string; disabled?: boolean; + inputRef?: RefObject; className?: string; }) { return ( (null); const [tel, setTel] = useState(""); const [pw, setPw] = useState(""); @@ -167,9 +212,10 @@ export default function DemandPlaceRegisterPage() { } setCompany(String(user.dmdCoNm ?? "")); + setZip(String(user.zip ?? "")); setAddr1(String(user.daddr ?? "")); setAddr2(""); - setTel(String(user.telno ?? "").replace(/\D/g, "")); + setTel(formatKoreanPhoneNumber(String(user.telno ?? ""))); setSido(String(user.ctpvCd ?? "")); setSigungu(String(user.sggCd ?? "")); } catch { @@ -178,6 +224,22 @@ export default function DemandPlaceRegisterPage() { } }, []); + useEffect(() => { + if (!focusDetailAddrOnClose || isPostcodeModalOpen) return; + + if (!addr1.trim()) { + setFocusDetailAddrOnClose(false); + return; + } + + const timer = setTimeout(() => { + detailAddrRef.current?.focus(); + setFocusDetailAddrOnClose(false); + }, 0); + + return () => clearTimeout(timer); + }, [addr1, focusDetailAddrOnClose, isPostcodeModalOpen]); + const sidoOptions = useMemo(() => { const mapped = selectSidoData .filter((item) => item.val !== "0") @@ -274,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, @@ -311,6 +374,30 @@ export default function DemandPlaceRegisterPage() { } }; + const handleSelectAddress = (item: AddrItem) => { + setZip(item.zipNo); + setAddr1(item.roadAddr); + setFocusDetailAddrOnClose(true); + const matchedSido = selectSidoData.find( + (region) => + region.val !== "0" && + isSameRegionName(region.text ?? "", item.ctpvNm ?? ""), + ); + + if (matchedSido) { + setSido(matchedSido.val); + const matchedSigungu = selectSigunData.find( + (region) => + region.val !== "0" && + region.val.startsWith(matchedSido.val) && + isSameRegionName(region.text ?? "", item.sggNm ?? ""), + ); + setSigungu(matchedSigungu?.val ?? ""); + } + + setIsPostcodeModalOpen(false); + }; + const pageTitle = editMode ? "수요처수정" : "수요처등록"; return ( @@ -414,7 +501,7 @@ export default function DemandPlaceRegisterPage() {
@@ -436,7 +524,7 @@ export default function DemandPlaceRegisterPage() {
setTel(v.replace(/\D/g, "").slice(0, 11))} + onChange={(v) => setTel(formatKoreanPhoneNumber(v))} placeholder="전화번호를 입력하세요." />
@@ -478,6 +566,12 @@ export default function DemandPlaceRegisterPage() { {editMode ? "수요처 수정" : "수요처 등록"}
+ + setIsPostcodeModalOpen(false)} + onSelect={handleSelectAddress} + /> ); } 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..c37912b97 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 { Suspense, 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; + } +}; + +function DemandRegisterPageContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { insertSdmInfoData, Sdm } = useDmStore((state) => state); -export default function DemandRegisterPage() { const today = useMemo(() => { const d = new Date(); const yyyy = d.getFullYear(); @@ -17,283 +86,486 @@ 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={ -