diff --git a/apps/dm_new/.env.local b/apps/dm_new/.env.local index 88b6f8067..62c4be2cc 100644 --- a/apps/dm_new/.env.local +++ b/apps/dm_new/.env.local @@ -1,4 +1,4 @@ -NEXT_PUBLIC_SITE_ID = DM +NEXT_PUBLIC_SITE_ID = DN NEXT_PUBLIC_CLAIM_NAME = Authorization NEXT_PUBLIC_AUTH_USER_ID = token-id NEXT_PUBLIC_ACCESS_TOKEN = access-token diff --git a/apps/dm_new/app/(with-layout)/apply/layout.tsx b/apps/dm_new/app/(with-layout)/apply/layout.tsx new file mode 100644 index 000000000..49c8a6266 --- /dev/null +++ b/apps/dm_new/app/(with-layout)/apply/layout.tsx @@ -0,0 +1,11 @@ +import type { ReactNode } from "react"; +import Lnb from "@/components/Lnb"; + +export default function ApplyLayout({ children }: { children: ReactNode }) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/apps/dm_new/app/(with-layout)/apply/modal/WaterQualityRefModal.tsx b/apps/dm_new/app/(with-layout)/apply/modal/WaterQualityRefModal.tsx new file mode 100644 index 000000000..877eb7d8c --- /dev/null +++ b/apps/dm_new/app/(with-layout)/apply/modal/WaterQualityRefModal.tsx @@ -0,0 +1,99 @@ +// apps/dm/app/(landing)/reuse/apply/modal/WaterQualityRefModal.tsx (수질기준참고 모달) +"use client"; + +import { useEffect, useId } from "react"; +import Image from "next/image"; + +type Props = { + open: boolean; + onClose: () => void; + imageSrc: string; + title?: string; +}; + +export default function WaterQualityRefModal({ + open, + onClose, + imageSrc, + title = "수질 기준 참고", +}: Props) { + const titleId = useId(); + + useEffect(() => { + if (!open) return; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKeyDown); + + // 스크롤 잠금 + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + + return () => { + window.removeEventListener("keydown", onKeyDown); + document.body.style.overflow = prev; + }; + }, [open, onClose]); + + if (!open) return null; + + return ( +
+ {/* Backdrop */} + + +

+ {title} +

+ +
+
+ + {/* Body (스크롤 영역) */} +
+
+ {/* 이미지가 길어도 여기서 스크롤 */} + 수질 기준 참고 +
+
+
+ + + ); +} diff --git a/apps/dm_new/app/(with-layout)/apply/place/page.tsx b/apps/dm_new/app/(with-layout)/apply/place/page.tsx new file mode 100644 index 000000000..4e13f3449 --- /dev/null +++ b/apps/dm_new/app/(with-layout)/apply/place/page.tsx @@ -0,0 +1,350 @@ +// apps/dm/app/(landing)/reuse/apply/place/page.tsx +"use client"; + +import { useMemo, useState } from "react"; +import SubPageHeader from "@/components/SubPageHeader"; + +type Option = { value: string; label: string }; +type SidoValue = "" | "seoul" | "busan" | "daegu" | "incheon" | "gyeonggi"; + +const SIDO: { value: SidoValue; label: string }[] = [ + { value: "", label: "선택" }, + { value: "seoul", label: "서울특별시" }, + { value: "busan", label: "부산광역시" }, + { value: "daegu", label: "대구광역시" }, + { value: "incheon", label: "인천광역시" }, + { value: "gyeonggi", label: "경기도" }, +]; + +const SIGUNGU_MAP: Record = { + "": [{ value: "", label: "선택" }], + seoul: [ + { value: "", label: "선택" }, + { value: "gangnam", label: "강남구" }, + { value: "yeongdeungpo", label: "영등포구" }, + { value: "mapo", label: "마포구" }, + ], + busan: [ + { value: "", label: "선택" }, + { value: "haeundae", label: "해운대구" }, + { value: "suyeong", label: "수영구" }, + ], + daegu: [ + { value: "", label: "선택" }, + { value: "suseong", label: "수성구" }, + { value: "dalseo", label: "달서구" }, + ], + incheon: [ + { value: "", label: "선택" }, + { value: "namdong", label: "남동구" }, + { value: "yeonsu", label: "연수구" }, + ], + gyeonggi: [ + { value: "", label: "선택" }, + { value: "bundang", label: "성남시 분당구" }, + { value: "suji", label: "용인시 수지구" }, + { value: "ilsandong", label: "고양시 일산동구" }, + ], +}; + +/* --------------------------- + * register/page.tsx 스타일과 맞춘 공통 UI (FormRow/Divider) + * -------------------------- */ + +function Divider() { + return
; +} + +function FormRow({ + label, + required, + children, +}: { + label: React.ReactNode; + required?: boolean; + children: React.ReactNode; +}) { + return ( +
+ {/* 라벨(좌측) */} +
+
+ {label} + {required ? * : null} +
+
+ + {/* 입력(우측) */} +
+ {children} +
+
+ ); +} + +function TextInput({ + value, + onChange, + placeholder, + type = "text", + disabled, + className = "", +}: { + value: string; + onChange: (v: string) => void; + placeholder?: string; + type?: string; + disabled?: boolean; + className?: string; +}) { + return ( + onChange(e.target.value)} + placeholder={placeholder} + className={[ + "h-11 w-full rounded border border-neutral-300 bg-white px-3 text-sm text-neutral-800", + "placeholder:text-neutral-400 focus:border-neutral-400 focus:outline-none", + disabled ? "bg-neutral-100 text-neutral-400" : "", + className, + ].join(" ")} + /> + ); +} + +function SelectBox({ + value, + onChange, + options, + className = "", +}: { + value: T; + onChange: (v: T) => void; + options: { value: T; label: string }[]; + className?: string; +}) { + return ( + + ); +} + +export default function DemandPlaceRegisterPage() { + const [sido, setSido] = useState(""); + const [sigungu, setSigungu] = useState(""); + + const [company, setCompany] = useState(""); + const [bizNo, setBizNo] = useState(""); + 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(""); + + // ✅ undefined 불가(Record + sido가 SidoValue) + const sigunguOptions = useMemo(() => SIGUNGU_MAP[sido], [sido]); + + const onSubmit = () => { + if (!sido || !sigungu) return alert("시도/시군구를 선택해 주세요."); + if (!company) return alert("회사명을 입력해 주세요."); + if (!bizNo) return alert("사업자등록번호를 입력해 주세요."); + if (!noWorkerNo && !workerNo) + return alert("종사자등록번호를 입력하거나 '해당없음'을 선택해 주세요."); + if (!addr1) return alert("사업자주소를 입력해 주세요."); + if (!tel) return alert("전화번호를 입력해 주세요."); + if (!pw) return alert("비밀번호를 입력해 주세요."); + if (pw !== pw2) return alert("비밀번호가 일치하지 않습니다."); + + alert("수요처 등록(샘플) 완료!"); + }; + + return ( +
+ + + {/* register/page.tsx와 동일한 테이블 룩앤필 */} +
+ + + value={sido} + onChange={(v) => { + setSido(v); + setSigungu(""); // 시도 변경 시 초기화 + }} + options={SIDO} + /> + + + + + + + + + + + +
+ +
+
+ + + + +
+ +
+
+ + + + +
+
+ +
+ + +
+
+ + + + +
+
+
+ +
+ +
+ + + +
+
+ + + + +
+ +
+
+ + + + +
+ +
+
+ + + + +
+ +
+
+
+ + {/* register/page.tsx와 동일하게 하단 중앙 버튼 */} +
+ +
+
+ ); +} diff --git a/apps/dm_new/app/(with-layout)/apply/register/page.tsx b/apps/dm_new/app/(with-layout)/apply/register/page.tsx new file mode 100644 index 000000000..645dadd67 --- /dev/null +++ b/apps/dm_new/app/(with-layout)/apply/register/page.tsx @@ -0,0 +1,476 @@ +// apps/dm/app/(landing)/reuse/apply/register/page.tsx +"use client"; + +import { useMemo, useState, useRef } from "react"; +import SubPageHeader from "@/components/SubPageHeader"; +import WaterQualityRefModal from "../modal/WaterQualityRefModal"; + +type PHRange = { min: string; max: string }; + +export default function DemandRegisterPage() { + const today = useMemo(() => { + const d = new Date(); + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const dd = String(d.getDate()).padStart(2, "0"); + return `${yyyy}-${mm}-${dd}`; + }, []); + + const [openRef, setOpenRef] = useState(false); + + // 기본정보 + const [reqDate, setReqDate] = useState(today); + const [reqQty, setReqQty] = useState(""); + const [usePurpose, setUsePurpose] = useState(""); + const [hopePrice, setHopePrice] = useState(""); + + // 요구수질 + const [coliform, setColiform] = useState(""); + const [noDetect, setNoDetect] = useState(false); + + 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(""); + + return ( + <> + +
+ + {/* 서브 타이틀 + 브레드크럼 */} + + + {/* 컨텐츠 영역 */} +
+ + {/* 기본정보 테이블 */} +
+ + + + + + + + + + + + + + + + + + + + + +
+ + {/* 요구수질 섹션 타이틀 바 */} +
+
+ 요구수질 + + {/* 수질기준참고 모달 */} + 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" + /> +
+ } + /> + + + {/* 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={ +