dm_new: 수요처등록수정
This commit is contained in:
@@ -1,55 +1,26 @@
|
||||
// app/apply/dmdco/page.tsx
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import { useDmStore } from "@/app/store";
|
||||
import { fetchType, getFetch, postFetch, putFetch } from "@/components/ui/api";
|
||||
import { getCookie } from "@common/components/lib";
|
||||
|
||||
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<SidoValue, Option[]> = {
|
||||
"": [{ 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: "고양시 일산동구" },
|
||||
],
|
||||
type SessionUser = {
|
||||
dmdInfoId?: string;
|
||||
dmdCoNm?: string;
|
||||
daddr?: string;
|
||||
telno?: string;
|
||||
ctpvCd?: string;
|
||||
sggCd?: string;
|
||||
rgtr?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/* ---------------------------
|
||||
* register/page.tsx 스타일과 맞춘 공통 UI (FormRow/Divider)
|
||||
* -------------------------- */
|
||||
const TOKEN_ID_COOKIE = "token-id";
|
||||
const FALLBACK_UUID = "c475824c-0e82-4a06-a282-2b55a20df265";
|
||||
|
||||
function Divider() {
|
||||
return <div className="h-px w-full bg-neutral-200" />;
|
||||
@@ -60,13 +31,12 @@ function FormRow({
|
||||
required,
|
||||
children,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
label: ReactNode;
|
||||
required?: boolean;
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
{/* 라벨(좌측) */}
|
||||
<div className="col-span-12 lg:col-span-2 flex items-center justify-center text-center bg-emerald-50/70 px-6 py-6">
|
||||
<div className="flex items-center gap-1 text-lg font-semibold text-neutral-800">
|
||||
<span>{label}</span>
|
||||
@@ -74,10 +44,7 @@ function FormRow({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 입력(우측) */}
|
||||
<div className="col-span-12 lg:col-span-10 px-6 py-6">
|
||||
{children}
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-10 px-6 py-6">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -114,21 +81,21 @@ function TextInput({
|
||||
);
|
||||
}
|
||||
|
||||
function SelectBox<T extends string>({
|
||||
function SelectBox({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
className = "",
|
||||
}: {
|
||||
value: T;
|
||||
onChange: (v: T) => void;
|
||||
options: { value: T; label: string }[];
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
options: Option[];
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value as T)}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={[
|
||||
"h-11 w-[220px] rounded border border-neutral-300 bg-white px-3 text-sm text-neutral-800",
|
||||
"focus:border-neutral-400 focus:outline-none",
|
||||
@@ -145,8 +112,14 @@ function SelectBox<T extends string>({
|
||||
}
|
||||
|
||||
export default function DemandPlaceRegisterPage() {
|
||||
const [sido, setSido] = useState<SidoValue>("");
|
||||
const [sigungu, setSigungu] = useState<string>("");
|
||||
const { Common, initializeCommonData } = useDmStore((state) => state);
|
||||
const { selectSidoData, selectSigunData } = Common;
|
||||
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [sessionUser, setSessionUser] = useState<SessionUser | null>(null);
|
||||
|
||||
const [sido, setSido] = useState("");
|
||||
const [sigungu, setSigungu] = useState("");
|
||||
|
||||
const [company, setCompany] = useState("");
|
||||
const [bizNo, setBizNo] = useState("");
|
||||
@@ -161,44 +134,205 @@ export default function DemandPlaceRegisterPage() {
|
||||
const [pw, setPw] = useState("");
|
||||
const [pw2, setPw2] = useState("");
|
||||
|
||||
// ✅ undefined 불가(Record<SidoValue,...> + sido가 SidoValue)
|
||||
const sigunguOptions = useMemo<Option[]>(() => SIGUNGU_MAP[sido], [sido]);
|
||||
useEffect(() => {
|
||||
initializeCommonData();
|
||||
}, [initializeCommonData]);
|
||||
|
||||
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("비밀번호가 일치하지 않습니다.");
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
alert("수요처 등록(샘플) 완료!");
|
||||
const rawUser = sessionStorage.getItem("user");
|
||||
if (!rawUser) {
|
||||
setEditMode(false);
|
||||
setSessionUser(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = JSON.parse(rawUser) as SessionUser;
|
||||
setSessionUser(user);
|
||||
setEditMode(true);
|
||||
|
||||
const fullId = String(user.dmdInfoId ?? "").trim();
|
||||
const bizDigits = fullId.slice(0, 10);
|
||||
const workerDigits = fullId.slice(10, 14);
|
||||
|
||||
setBizNo(bizDigits);
|
||||
if (workerDigits && workerDigits !== "0000") {
|
||||
setNoWorkerNo(false);
|
||||
setWorkerNo(workerDigits);
|
||||
} else {
|
||||
setNoWorkerNo(true);
|
||||
setWorkerNo("");
|
||||
}
|
||||
|
||||
setCompany(String(user.dmdCoNm ?? ""));
|
||||
setAddr1(String(user.daddr ?? ""));
|
||||
setAddr2("");
|
||||
setTel(String(user.telno ?? "").replace(/\D/g, ""));
|
||||
setSido(String(user.ctpvCd ?? ""));
|
||||
setSigungu(String(user.sggCd ?? ""));
|
||||
} catch {
|
||||
setEditMode(false);
|
||||
setSessionUser(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sidoOptions = useMemo<Option[]>(() => {
|
||||
const mapped = selectSidoData
|
||||
.filter((item) => item.val !== "0")
|
||||
.map((item) => ({ value: item.val, label: item.text }));
|
||||
|
||||
return [{ value: "", label: "선택" }, ...mapped];
|
||||
}, [selectSidoData]);
|
||||
|
||||
const sigunguOptions = useMemo<Option[]>(() => {
|
||||
if (!sido) return [{ value: "", label: "선택" }];
|
||||
|
||||
const mapped = selectSigunData
|
||||
.filter((item) => item.val.substring(0, 2) === sido)
|
||||
.map((item) => ({ value: item.val, label: item.text }));
|
||||
|
||||
return [{ value: "", label: "선택" }, ...mapped];
|
||||
}, [sido, selectSigunData]);
|
||||
|
||||
const formatBusinessNumber = (value: string) => {
|
||||
const digitsOnly = value.replace(/\D/g, "").slice(0, 10);
|
||||
if (digitsOnly.length <= 3) return digitsOnly;
|
||||
if (digitsOnly.length <= 5) {
|
||||
return `${digitsOnly.slice(0, 3)}-${digitsOnly.slice(3)}`;
|
||||
}
|
||||
return `${digitsOnly.slice(0, 3)}-${digitsOnly.slice(3, 5)}-${digitsOnly.slice(5)}`;
|
||||
};
|
||||
|
||||
const checkDuplicate = async (fullDmdInfoId: string) => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.WR, `/reuseFc/request/${fullDmdInfoId}`);
|
||||
const responseData = (res?.data ?? res) as { dmdInfoId?: string } | undefined;
|
||||
return Boolean(responseData?.dmdInfoId);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const bizDigits = bizNo.replace(/\D/g, "").slice(0, 10);
|
||||
const workerDigits = workerNo.replace(/\D/g, "").slice(0, 4);
|
||||
const telDigits = tel.replace(/\D/g, "");
|
||||
const password = pw.trim();
|
||||
const passwordConfirm = pw2.trim();
|
||||
|
||||
if (!sido || sido === "0") return alert("시도/시군구를 선택해 주세요.");
|
||||
if (!sigungu || sigungu === "0") return alert("시도/시군구를 선택해 주세요.");
|
||||
if (!company.trim()) return alert("회사명을 입력해 주세요.");
|
||||
if (company.trim().length > 20) return alert("회사명은 20자 이내로 입력해주세요.");
|
||||
|
||||
if (!bizDigits) return alert("사업자등록번호를 입력해 주세요.");
|
||||
if (!/^\d{10}$/.test(bizDigits)) {
|
||||
return alert("사업자등록번호는 10자리 숫자만 입력 가능합니다.");
|
||||
}
|
||||
|
||||
if (!noWorkerNo) {
|
||||
if (!workerDigits) return alert("종사업자등록번호를 입력해 주세요.");
|
||||
if (!/^\d{4}$/.test(workerDigits)) {
|
||||
return alert("종사업자등록번호는 4자리 숫자만 입력 가능합니다.");
|
||||
}
|
||||
}
|
||||
|
||||
const mergedAddress = [addr1.trim(), addr2.trim()].filter(Boolean).join(" ");
|
||||
if (!mergedAddress) return alert("사업자주소를 입력해 주세요.");
|
||||
if (mergedAddress.length > 40) return alert("주소는 40자 이내로 입력해주세요.");
|
||||
|
||||
if (!telDigits) return alert("전화번호를 입력해 주세요.");
|
||||
if (!/^\d{8,11}$/.test(telDigits)) {
|
||||
return alert("전화번호는 8~11자리 숫자만 입력 가능합니다.");
|
||||
}
|
||||
|
||||
if (!editMode) {
|
||||
if (!password) return alert("비밀번호를 입력해 주세요.");
|
||||
if (password.length < 8 || password.length > 16) {
|
||||
return alert("비밀번호는 8자 이상 16자 이내로 입력해주세요.");
|
||||
}
|
||||
if (!passwordConfirm) return alert("비밀번호 확인을 입력해 주세요.");
|
||||
if (password !== passwordConfirm) return alert("비밀번호가 일치하지 않습니다.");
|
||||
} else if ((password || passwordConfirm) && password !== passwordConfirm) {
|
||||
return alert("비밀번호가 일치하지 않습니다.");
|
||||
}
|
||||
|
||||
const fullDmdInfoId = bizDigits + (noWorkerNo ? "0000" : workerDigits);
|
||||
|
||||
if (!editMode) {
|
||||
const isDuplicate = await checkDuplicate(fullDmdInfoId);
|
||||
if (isDuplicate) return alert("이미 등록된 사업자등록번호입니다.");
|
||||
}
|
||||
|
||||
const myUUID = getCookie(TOKEN_ID_COOKIE) || FALLBACK_UUID;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
ctpvCd: sido,
|
||||
sggCd: sigungu,
|
||||
dmdCoNm: company.trim(),
|
||||
dmdInfoId: fullDmdInfoId,
|
||||
bzmnNo: bizDigits,
|
||||
daddr: mergedAddress,
|
||||
telno: telDigits,
|
||||
rgtr: (sessionUser?.rgtr as string) || myUUID,
|
||||
};
|
||||
|
||||
if (password) {
|
||||
payload.dmdAplyPswd = password;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editMode) {
|
||||
payload.mdfr = myUUID;
|
||||
await putFetch("/reuseFc/request", payload);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const nextSessionUser = {
|
||||
...(sessionUser ?? {}),
|
||||
...payload,
|
||||
};
|
||||
delete nextSessionUser.dmdAplyPswd;
|
||||
sessionStorage.setItem("user", JSON.stringify(nextSessionUser));
|
||||
}
|
||||
|
||||
alert("수요처 수정 완료!");
|
||||
} else {
|
||||
await postFetch("/reuseFc/request", payload);
|
||||
alert("수요처 등록 완료!");
|
||||
}
|
||||
|
||||
setPw("");
|
||||
setPw2("");
|
||||
} catch (error) {
|
||||
console.error("Failed to submit demand place", error);
|
||||
alert(editMode ? "수요처 수정에 실패하였습니다." : "수요처 등록에 실패하였습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
const pageTitle = editMode ? "수요처수정" : "수요처등록";
|
||||
|
||||
return (
|
||||
<main className="bg-white">
|
||||
<SubPageHeader
|
||||
title="수요처등록"
|
||||
title={pageTitle}
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "수요신청", href: "/reuse/apply" },
|
||||
{ label: "수요처등록" },
|
||||
{ label: pageTitle },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* register/page.tsx와 동일한 테이블 룩앤필 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<FormRow label="시도" required>
|
||||
<SelectBox<SidoValue>
|
||||
<SelectBox
|
||||
value={sido}
|
||||
onChange={(v) => {
|
||||
setSido(v);
|
||||
setSigungu(""); // 시도 변경 시 초기화
|
||||
setSigungu("");
|
||||
}}
|
||||
options={SIDO}
|
||||
options={sidoOptions}
|
||||
/>
|
||||
</FormRow>
|
||||
|
||||
@@ -235,8 +369,8 @@ export default function DemandPlaceRegisterPage() {
|
||||
<FormRow label="사업자등록번호" required>
|
||||
<div className="max-w-[420px]">
|
||||
<TextInput
|
||||
value={bizNo}
|
||||
onChange={setBizNo}
|
||||
value={formatBusinessNumber(bizNo)}
|
||||
onChange={(v) => setBizNo(v.replace(/\D/g, "").slice(0, 10))}
|
||||
placeholder="사업자 등록번호를 입력해주세요."
|
||||
/>
|
||||
</div>
|
||||
@@ -249,7 +383,7 @@ export default function DemandPlaceRegisterPage() {
|
||||
<div className="w-[420px] max-w-full">
|
||||
<TextInput
|
||||
value={workerNo}
|
||||
onChange={setWorkerNo}
|
||||
onChange={(v) => setWorkerNo(v.replace(/\D/g, "").slice(0, 4))}
|
||||
placeholder="종사자등록번호를 입력해주세요."
|
||||
disabled={noWorkerNo}
|
||||
/>
|
||||
@@ -302,7 +436,7 @@ export default function DemandPlaceRegisterPage() {
|
||||
<div className="max-w-[420px]">
|
||||
<TextInput
|
||||
value={tel}
|
||||
onChange={setTel}
|
||||
onChange={(v) => setTel(v.replace(/\D/g, "").slice(0, 11))}
|
||||
placeholder="전화번호를 입력하세요."
|
||||
/>
|
||||
</div>
|
||||
@@ -310,7 +444,7 @@ export default function DemandPlaceRegisterPage() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="비밀번호" required>
|
||||
<FormRow label="비밀번호" required={!editMode}>
|
||||
<div className="max-w-[420px]">
|
||||
<TextInput
|
||||
value={pw}
|
||||
@@ -323,7 +457,7 @@ export default function DemandPlaceRegisterPage() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="비밀번호 확인" required>
|
||||
<FormRow label="비밀번호 확인" required={!editMode}>
|
||||
<div className="max-w-[420px]">
|
||||
<TextInput
|
||||
value={pw2}
|
||||
@@ -335,14 +469,13 @@ export default function DemandPlaceRegisterPage() {
|
||||
</FormRow>
|
||||
</div>
|
||||
|
||||
{/* register/page.tsx와 동일하게 하단 중앙 버튼 */}
|
||||
<div className="flex justify-center py-12">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
|
||||
>
|
||||
수요처 등록
|
||||
{editMode ? "수요처 수정" : "수요처 등록"}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// apps/dm/app/(landing)/reuse/business/globalCase/australia/page.tsx
|
||||
"use client";
|
||||
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import CaseImageSlider from "@/components/CaseImageSlider";
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="h-5 w-[3px] bg-sky-500" />
|
||||
<h3 className="text-2xl font-bold text-neutral-900">{children}</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type HrProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Hr({ className = "" }: HrProps) {
|
||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||
}
|
||||
|
||||
type ImgCardProps = {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
function ImgCard({ src, alt }: ImgCardProps) {
|
||||
return (
|
||||
<div className="w-full overflow-x-auto">
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="block mx-auto max-w-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GreywaterPage() {
|
||||
return (
|
||||
<div className="bg-white">
|
||||
<SubPageHeader
|
||||
title="[호주] 생활 속 물 재이용 / 광역 물 재이용수 공급"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "물 재이용 사례" },
|
||||
{ label: "해외 재이용 사례" },
|
||||
{ label: "호주", href: "/reuse/business/globalCase/australia" },
|
||||
]}
|
||||
hideFooterLine
|
||||
hideGuideText
|
||||
wrapClassName="!pb-0"
|
||||
/>
|
||||
|
||||
<Hr className="my-7" />
|
||||
|
||||
{/* 슬라이드 이미지 영역 */}
|
||||
<CaseImageSlider
|
||||
className="mt-8"
|
||||
maxWidthClassName="max-w-[760px]"
|
||||
slides={[
|
||||
{ src: "/images/landing/business/globalCase/australia_01.png", alt: "호주 사례 1" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 생활용수로 재이용수의 적극 활용 */}
|
||||
<section className="mt-8">
|
||||
<SectionTitle>생활용수로 재이용수의 적극 활용</SectionTitle>
|
||||
<div className="px-4 mt-6">
|
||||
<ul className="space-y-4 text-base text-[#4F4F4F]">
|
||||
<li>
|
||||
– 재이용수를 공원, 골프장 등의 관개용수로 공급(초록색 지역)
|
||||
</li>
|
||||
<li>
|
||||
– Dual pipe로 재이용수를 특정 주거단지로 공급(보라색 지역)
|
||||
</li>
|
||||
<li>
|
||||
– 현재 7,000여 가구가 재이용수를 사용
|
||||
</li>
|
||||
<li>
|
||||
– 2030년까지 43,000가구로 확대 예정
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Hr className="my-11" />
|
||||
|
||||
{/* 호주 재이용수 광역 활용(퀸즈랜드주 브리즈번) */}
|
||||
<section className="mt-6">
|
||||
<SectionTitle>호주 재이용수 광역 활용(퀸즈랜드주 브리즈번)</SectionTitle>
|
||||
<div className="px-4">
|
||||
<ul className="mt-6 space-y-4 text-base text-gray-600">
|
||||
<li>
|
||||
• SEQ Water Grid는 브리즈번 남동부지역의 식수 공급을 위해 설정한 광역 수로망
|
||||
</li>
|
||||
<li>
|
||||
• 이 광역수로망을 활용하여 물이 부족한 지역으로 물을 공급<br />
|
||||
<ul className="list-inside text-[#767676] ml-4 mt-2">
|
||||
<li>– 1,618억의 사업비 중 92% 국비, 지방비 8% 투입</li>
|
||||
<li>– 재이용수는 24km 관로로 타이난 TSMC로 공급</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
• 12개 댐, 32개 수처리 시설, 3개 재이용시설, 담수화 플랜트 1개, 저수지 28개와 600km 이상의 물 공급 관로를 건설
|
||||
</li>
|
||||
<li>
|
||||
• 지역적으로 강수량이 고르지 않아, 댐의 저수율이 불균등할 때 지역의 물 공급을 보충하는 중요한 역할을 수행
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Hr className="my-11" />
|
||||
|
||||
{/* 특이사항 */}
|
||||
<section className="pb-[200px]">
|
||||
<SectionTitle>특이사항</SectionTitle>
|
||||
<ul className="mt-6 space-y-4 pl-4 text-base text-gray-600">
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>호주는 2000~2009년 강수량 감소로 밀레니엄 가뭄이 발생하여 심각한 물 부족 현상이 발생함</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>호주는 해안 도시에 인구가 밀집하여 거주하고 있으며 인구 증가율이 타 선진국 대비 높아, 향후 도시에 공급할 수자원 확보가 필요함</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>호주정부는 1990년대부터 하·폐수가 방류될 경우의 환경 피해에 대해 인식하기 시작하고 배출규제와 위원회를 구성하기 시작</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>연방정부는 주에 인센티브를 주는 방식으로 물 정책을 장려하고 토지소유권과 물 권리를 구분하여 거래를 허용하고 물 재이용 활성화를 위한 Water Smart Australia 프로그램을 시행</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>호주의 용수공급 체계는 영국과 미국의 특성들이 존재하지만, 주의 특성과 주 정부의 정책 방향에 따라 주마다도 많은 차이가 있음. 특히 대부분의 주에서 수 자원 개발 기능과 수도 공급의 기능을 분리하여 주정부<br/>산하 기관에서 운영하는게 특징</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// apps/dm/app/(landing)/reuse/business/globalCase/eu/page.tsx
|
||||
"use client";
|
||||
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import CaseImageSlider from "@/components/CaseImageSlider";
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="h-5 w-[3px] bg-sky-500" />
|
||||
<h3 className="text-2xl font-bold text-neutral-900">{children}</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type HrProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Hr({ className = "" }: HrProps) {
|
||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||
}
|
||||
|
||||
// type ImgCardProps = {
|
||||
// src: string;
|
||||
// alt: string;
|
||||
// };
|
||||
|
||||
// function ImgCard({ src, alt }: ImgCardProps) {
|
||||
// return (
|
||||
// <div className="w-full overflow-x-auto">
|
||||
// <img
|
||||
// src={src}
|
||||
// alt={alt}
|
||||
// className="block mx-auto max-w-none"
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
export default function GreywaterPage() {
|
||||
return (
|
||||
<div className="bg-white">
|
||||
<SubPageHeader
|
||||
title="[EU] 법률을 제정, EU국가에서 물 재이용 유도"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "물 재이용 사례" },
|
||||
{ label: "해외 재이용 사례" },
|
||||
{ label: "EU", href: "/reuse/business/globalCase/eu" },
|
||||
]}
|
||||
hideFooterLine
|
||||
hideGuideText
|
||||
wrapClassName="!pb-0"
|
||||
/>
|
||||
|
||||
<Hr className="my-7" />
|
||||
|
||||
{/* 슬라이드 이미지 영역 */}
|
||||
<CaseImageSlider
|
||||
className="mt-8"
|
||||
maxWidthClassName="max-w-[760px]"
|
||||
slides={[
|
||||
{ src: "/images/landing/business/globalCase/eu_01.png", alt: "EU 사례 1" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 물 재이용에 대한 법률을 제정(2020 제정, 2023 시행) */}
|
||||
<section className="mt-8">
|
||||
<SectionTitle>물 재이용에 대한 법률을 제정(2020 제정, 2023 시행)</SectionTitle>
|
||||
<div className="px-4 mt-6">
|
||||
<ul className="mt-6 space-y-4 text-base text-gray-600">
|
||||
<li>
|
||||
• 유럽 지역의 물 부족 문제를 인식하고 ’20년 물 재이용 법률을 제정 (2020/741)
|
||||
</li>
|
||||
<li>
|
||||
• 2023년부터 모든 EU국가에서 시행
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Hr className="my-11" />
|
||||
|
||||
{/* 유럽연합 회원국 물 재이용 지원정책 */}
|
||||
<section className="mt-6">
|
||||
<SectionTitle>유럽연합 회원국 물 재이용 지원정책</SectionTitle>
|
||||
<div className="px-4">
|
||||
<ul className="mt-6 space-y-4 text-base text-gray-600">
|
||||
<li>
|
||||
• 유럽연합 차원에서 다양한 물 재이용 프로그램 자금 지원<br />
|
||||
<ul className="list-inside space-y-4 text-[#767676] text-sm ml-4 mt-2">
|
||||
<li>– 2021년부터 3년간 Life RECYCLIO 프로그램 지원 → 세부내용 물 재이용 프로그램<br/>
|
||||
<ul className="list-inside space-y-2 text-[#767676] ml-4 mt-2">
|
||||
<li>1. 환경 관련 사업에 자금을 지원하는 Life 프로그램의 일환으로 80만 유로를 지원</li>
|
||||
<li>2. 식품·농업 분야 물 재이용 확대를 위해 기술 이전 및 평가 실시</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>– 물 재이용 정책 수행을 위해 BOOST-INN 180만 유로를 지원<br/>
|
||||
<ul className="list-inside space-y-2 text-[#767676] ml-4 mt-2">
|
||||
<li> *프랑스, 이탈리아, 스페인 등 유럽 선진국 솔루션 공유</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
• EU산하 JRC(공동연구센터)에서 물 재이용 기술제공 지원<br />
|
||||
<ul className="list-inside space-y-2 text-[#767676] text-sm ml-4 mt-2">
|
||||
<li>‘물 재이용 회원 관리 실무 커뮤니티(Water Reuse Management Community of Practice)’ 시범사업 수행</li>
|
||||
<li>유럽 전역에 성공적인 모델 활용 및 혁신적인 물 재이용 솔루션을 확장할 수 있는 기술을 제공</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Hr className="my-11" />
|
||||
|
||||
{/* 특이사항 */}
|
||||
<section className="pb-[200px]">
|
||||
<SectionTitle>특이사항</SectionTitle>
|
||||
<ul className="mt-6 space-y-4 pl-4 text-base text-gray-600">
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>2019년 기준 EU 인구의 38%가 물 부족의 영향을 받을 것으로 예상됨. 이 흐름대로면 2030년까지 유럽 유역 전반에 물 부족 문제가 발생하게 될 것으로 예상. 인구 증가, 도시화의 확대, 염분화 및 기후변화들이 수자원 문제들의 주요 요인이며, 식습관 변화와 산업용수 수요 증가 등으로 악화되고 있는 상황</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>유럽의 물 부족 상황은 지역에 따라 다르게 나타나 스페인을 비롯한 남부유럽 에서 특히 심각함. 전반적으로 북유럽에 비해 남유럽의 물 스트레스가 심하며, 특히 스페인과 포르투갈, 프랑스 동부지역의 물 스트레스가 심각함</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>유럽 연합에서도 물 부족 문제를 인식하고 2000년 EU Water Framework Directive을 개발, 이후 2020년 재이용에 관한 법률을 제정</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// apps/dm/app/(landing)/reuse/business/globalCase/singapore/page.tsx
|
||||
"use client";
|
||||
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import CaseImageSlider from "@/components/CaseImageSlider";
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="h-5 w-[3px] bg-sky-500" />
|
||||
<h3 className="text-2xl font-bold text-neutral-900">{children}</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type HrProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Hr({ className = "" }: HrProps) {
|
||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||
}
|
||||
|
||||
type ImgCardProps = {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
function ImgCard({ src, alt }: ImgCardProps) {
|
||||
return (
|
||||
<div className="w-full overflow-x-auto">
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="block mx-auto max-w-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GreywaterPage() {
|
||||
return (
|
||||
<div className="bg-white">
|
||||
<SubPageHeader
|
||||
title="[싱가포르] 수요가 높은 산업의 물 재이용 의무화"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "물 재이용 사례" },
|
||||
{ label: "해외 재이용 사례" },
|
||||
{ label: "싱가포르", href: "/reuse/business/globalCase/singapore" },
|
||||
]}
|
||||
hideFooterLine
|
||||
hideGuideText
|
||||
wrapClassName="!pb-0"
|
||||
/>
|
||||
|
||||
<Hr className="my-7" />
|
||||
|
||||
{/* 슬라이드 이미지 영역 */}
|
||||
<CaseImageSlider
|
||||
className="mt-8"
|
||||
maxWidthClassName="max-w-[760px]"
|
||||
slides={[
|
||||
{ src: "/images/landing/business/globalCase/singapore_01.png", alt: "싱가포르 사례 1" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 싱가포르 NEWater 비중 확대 */}
|
||||
<section className="mt-8">
|
||||
<SectionTitle>싱가포르 NEWater 비중 확대</SectionTitle>
|
||||
<div className="px-4 mt-6">
|
||||
<ul className="mt-6 space-y-4 text-base text-gray-600">
|
||||
<li>
|
||||
• 2003년 공식 NEWater 공장을 가동했으며 신뢰확보를 위해 NEWater Visitor Center를 함께 건설
|
||||
</li>
|
||||
<li>
|
||||
• NEWater 전체파이프 공급 → 반도체 공장에서 가장 많이 사용
|
||||
</li>
|
||||
<li>
|
||||
• 2060년까지 NEWater 비중 55%까지 높이는 것이 싱가포르의 목표
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Hr className="my-11" />
|
||||
|
||||
{/* 싱가포르 재이용수 의무화 및 지원정책 */}
|
||||
<section className="mt-6">
|
||||
<SectionTitle>싱가포르 재이용수 의무화 및 지원정책</SectionTitle>
|
||||
<div className="px-4">
|
||||
<ul className="mt-6 space-y-4 text-base text-gray-600">
|
||||
<li>
|
||||
• 공업용수 수요가 높은 산업에 대해 물 재이용을 의무화<br />
|
||||
<ul className="list-inside text-[#767676] text-sm ml-4 mt-2">
|
||||
<li>– 반도체업계 전체 비 재이용수(non-domestic용수) 17% 차지 → 공업용수 수요 원인</li>
|
||||
<li>– 2024년 1월 1일부터 연간 최소 60,000㎥를 소비하는 사업자는 재이용수 사용 의무화</li>
|
||||
<li>– 반도체 공장의 경우 최소 50% 이상의 재이용수를 요구함(현재는 43% 정도)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
• 재정 지원을 통해 재이용 활성화를 지원<br />
|
||||
<ul className="list-inside text-[#767676] text-sm ml-4 mt-2">
|
||||
<li>– 재이용 설비를 기준으로 2025년까지 싱가포르 수자원공사의 2가지 금융상품 제공<br/>
|
||||
<ul className="list-inside text-[#767676] ml-4 mt-2">
|
||||
<li>1. Water Efficiency Fund</li>
|
||||
<li>2. Industrial Water Solutions Demonstration Fund</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li className="mt-2">– 2025년 기준 지원 한도 상향 → 물 재이용 활성 도모<br/>
|
||||
<ul className="list-inside text-[#767676] ml-4 mt-2">
|
||||
<li>투입비용 50% → 75% 지원 / WSDP: $400만 → $500만</li>
|
||||
<li>물 효율이 높은 장비를 도입하는 데에도 최대 $300만 지원 가능</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Hr className="my-11" />
|
||||
|
||||
{/* 특이사항 */}
|
||||
<section className="pb-[200px]">
|
||||
<SectionTitle>특이사항</SectionTitle>
|
||||
<ul className="mt-6 space-y-4 pl-4 text-base text-gray-600">
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>싱가포르는 천연 수자원이 많지 않아, 말레이시아 조호르와 1962년 맺은 협약으로 물을 수입해왔으며, 재이용수를 늘리기 위한 노력을 계속하고 있음<br/>
|
||||
<ul className="list-inside text-[#767676] text-sm ml-4 mt-2">
|
||||
<li>- 싱가포르는 국토 내에 호수와 하천이적고 집수공간도 적어 빗물로 확보할 수 있는 수자원량은 전체 물 수요의 20%에 불과함</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>싱가포르는 도심에 저수지를 건설하 고 건물에 빗물이용시설을 건설하여 빗물을 재이용하고 있음</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>싱가포르는 2002년부터 하수재이용수인 NEWater를 개발, 비음 용수와 간접음용수로 활용하고 있음<br/>
|
||||
<ul className="list-inside text-[#767676] text-sm/6 ml-4 mt-2">
|
||||
<li>- 재이용수를 NEWater라는 이름으로 명명하고 NEWater에 대한 신뢰 확보를 위해 NEWater 방문자 센터도 건설함</li>
|
||||
<li>- 비음용수로 많이 사용하며 건기에는 NEWater를 저수지에 추가하여 원수와 섞음. 원수는 상수도에서 처리 한 후 가정에 수돗물로 공급함</li>
|
||||
<li>- NEWater는 전용 파이프를 통해 주로 웨이퍼 제조 공장, 산업단지, 상업용 건물에서 산업 및 냉각수로 사용. 제일 많이 사용하는 곳은 웨이퍼 제조 공장</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>2024년 4월 기준 1㎥ 당 공업용수 가격은 $1.66이며, Non-domestic 기준 NEWater 가격은 $2.41</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>공업용수 가격 $1.66은 수도요금(water tariff)과 수자원세(waterborne tax)로 구분됨. $1.66에는 $1만큼의 수자원세가 포함됨</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>2023년 싱가포르는 산업용수 수요가 높은 산업에 대해 물 재 이용을 의무화했으며, 정부는 기금을 통해 물 재이용을 지원함</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// apps/dm/app/(landing)/reuse/business/globalCase/taiwan/page.tsx
|
||||
"use client";
|
||||
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import CaseImageSlider from "@/components/CaseImageSlider";
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="h-5 w-[3px] bg-sky-500" />
|
||||
<h3 className="text-2xl font-bold text-neutral-900">{children}</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type HrProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Hr({ className = "" }: HrProps) {
|
||||
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
|
||||
}
|
||||
|
||||
type ImgCardProps = {
|
||||
src: string;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
function ImgCard({ src, alt }: ImgCardProps) {
|
||||
return (
|
||||
<div className="w-full overflow-x-auto">
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
className="block mx-auto max-w-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GreywaterPage() {
|
||||
return (
|
||||
<div className="bg-white">
|
||||
<SubPageHeader
|
||||
title="[대만] 국가 주도의 공업용수 공급"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "물 재이용 사례" },
|
||||
{ label: "해외 재이용 사례" },
|
||||
{ label: "대만", href: "/reuse/business/globalCase/taiwan" },
|
||||
]}
|
||||
hideFooterLine
|
||||
hideGuideText
|
||||
wrapClassName="!pb-0"
|
||||
/>
|
||||
|
||||
<Hr className="my-7" />
|
||||
|
||||
{/* 슬라이드 이미지 영역 */}
|
||||
<CaseImageSlider
|
||||
className="mt-8"
|
||||
maxWidthClassName="max-w-[760px]"
|
||||
slides={[
|
||||
{ src: "/images/landing/business/globalCase/taiwan_01.png", alt: "대만 사례 1" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 재생 수자원 개발법 제정(2015년제정, 2022개정) */}
|
||||
<section className="mt-8">
|
||||
<SectionTitle>재생 수자원 개발법 제정(2015년제정, 2022개정)</SectionTitle>
|
||||
<div className="px-4">
|
||||
<ul className="mt-6 space-y-2 text-base text-gray-600">
|
||||
<li>
|
||||
• 목적: 수자원 지속 가능성 향상, 재생 수자원 활용을 통해 물부족 문제 해결
|
||||
</li>
|
||||
<li>
|
||||
• 내용: 재생 수자원 정의·분류, 이용 의무, 이용 제한, 수질 기준 등
|
||||
</li>
|
||||
</ul>
|
||||
<div className="rounded-sm bg-[#e9f0f1] p-8 mt-5">
|
||||
<ul className="space-y-1 text-2xl text-[#1A1A1A]">
|
||||
<li>
|
||||
– 제4조. 물 소비 계획에 따른 재이용수 이용 관련 내용 (일정 비율 재이용수 사용 규정)
|
||||
</li>
|
||||
<li>
|
||||
– 제5조. 재이용수의 운영자, 사용자 관련 내용 (지자체에서 하수 무상공급 제공 가능)
|
||||
</li>
|
||||
<li>
|
||||
– 제7조. 재이용수 이용 범위, 이용량 재이용수 수질 기준 내용 (식품, 제약산업 사용 불가)
|
||||
</li>
|
||||
<li>
|
||||
– 제8조. 재이용수 사업 주체, 책임 등에 관한 내용 (광역인 경우, 정부가 지자체 지정)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Hr className="my-11" />
|
||||
|
||||
{/* 대만정부 하수재이용 사업 시행 */}
|
||||
<section className="mt-6">
|
||||
<SectionTitle>대만정부 하수재이용 사업 시행</SectionTitle>
|
||||
<div className="px-4">
|
||||
<ul className="mt-6 space-y-4 text-base text-gray-600">
|
||||
<li>
|
||||
• 대만은 물 재이용에 적극적이며 국가주도로 하수재이용 사업 시행(안핑)<br />
|
||||
<ul className="list-inside text-[#767676] ml-4 mt-2">
|
||||
<li>– 1,618억의 사업비 중 92% 국비, 지방비 8% 투입</li>
|
||||
<li>– 재이용수는 24km 관로로 타이난 TSMC로 공급</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
• 안핑을 포함한 16건 공사 완료시 628,100 CMD 공급 가능
|
||||
</li>
|
||||
<li>
|
||||
• 공급단가는 수질과 운영조건에 따라 다르게 책정
|
||||
</li>
|
||||
</ul>
|
||||
{/* 테이블 (2단 병렬: 좌/우 데이터) */}
|
||||
<div className="mt-3">
|
||||
<div className="overflow-x-auto">
|
||||
<p className="text-center text-2xl py-2">〈대만 재이용 시설별 재이용수 공급단지〉</p>
|
||||
<table className="min-w-[900px] w-full text-center text-sm">
|
||||
<thead className="border">
|
||||
<tr>
|
||||
<th className="bg-[#C2CAD2] px-4 py-3 text-black">사업</th>
|
||||
<th className="bg-sky-600 px-4 py-3 text-white">가격(NTD/㎥)</th>
|
||||
<th className="bg-green-500 px-4 py-3 text-white">가격(원/㎥)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className="bg-white text-[#4F4F4F] border border-t-0">
|
||||
<tr className="border-t">
|
||||
<td className="px-4 py-3 bg-white">Fongshan(가오슝)</td>
|
||||
<td className="px-4 py-3 bg-[#EAF5F5]">23.39</td>
|
||||
<td className="px-4 py-3 bg-[#E7F4E2]">1,038원</td>
|
||||
</tr>
|
||||
<tr className="border-t">
|
||||
<td className="px-4 py-3 bg-white">Linhai(가오슝)</td>
|
||||
<td className="px-4 py-3 bg-[#EAF5F5]">19.28</td>
|
||||
<td className="px-4 py-3 bg-[#E7F4E2]">858원</td>
|
||||
</tr>
|
||||
<tr className="border-t">
|
||||
<td className="px-4 py-3 bg-white">Anping(타이난)</td>
|
||||
<td className="px-4 py-3 bg-[#EAF5F5]">38.28</td>
|
||||
<td className="px-4 py-3 bg-[#E7F4E2]">1,699원</td>
|
||||
</tr>
|
||||
<tr className="border-t">
|
||||
<td className="px-4 py-3 bg-white">Yongkang(타이난)</td>
|
||||
<td className="px-4 py-3 bg-[#EAF5F5]">23.67</td>
|
||||
<td className="px-4 py-3 bg-[#E7F4E2]">1,050원</td>
|
||||
</tr>
|
||||
<tr className="border-t">
|
||||
<td className="px-4 py-3 bg-white">Rende(타이난)</td>
|
||||
<td className="px-4 py-3 bg-[#EAF5F5]">28.26</td>
|
||||
<td className="px-4 py-3 bg-[#E7F4E2]">1,254원</td>
|
||||
</tr>
|
||||
<tr className="border-t">
|
||||
<td className="px-4 py-3 bg-white">Shuinan(타이중)</td>
|
||||
<td className="px-4 py-3 bg-[#EAF5F5]">15.00</td>
|
||||
<td className="px-4 py-3 bg-[#E7F4E2]">668원</td>
|
||||
</tr>
|
||||
<tr className="border-t">
|
||||
<td className="px-4 py-3 bg-white">Quaitou(가오슝)</td>
|
||||
<td className="px-4 py-3 bg-[#EAF5F5]">35.44</td>
|
||||
<td className="px-4 py-3 bg-[#E7F4E2]">1,573원</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Hr className="my-11" />
|
||||
|
||||
{/* 특이사항 */}
|
||||
<section className="pb-[200px]">
|
||||
<SectionTitle>특이사항</SectionTitle>
|
||||
<ul className="mt-6 space-y-4 pl-4 text-base text-gray-600">
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>대만은 풍부한 강수량에도, 지형적 특성으로 인해 수자원 확보에 어려움을 겪고 있으며, 대만 남부지역의 경우 우기와 건기 사이의 강수량 차이가 큼</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>대만은 최근 심각한 가뭄이 여러 해에 걸쳐서 발생하고 있으며, 강수량 감소와 함께 대만 정부 주도하에 반도체 산업을 국가 핵심 산업으로 선정하고 반도체 산업 육성 계획을 세움. 그 결과, 2023년 파운드리 시장의 68.3%를 차지하는 반도체 강국으로 성장했으며 이로 인한 공업용수 수요가 크게 증가</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>반도체 산업은 물 소비가 많아, 안정적인 용수공급이 매우 중요한데 현재 19개의 반도체 공장이 물이 부족한 대만 남부지역에 위치함. 2021년 가뭄 발생 당시, 산업단지 기업들도 용수 공급에 어려움을 겪음. 이는 대만 남부 지역의 물 부족, 물 분배 등에 대한 문제를 야기함</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>대만 정부는 산업단지 기업들에게 2023년부터 물에 대한 추가요금 부과를 시작했으며, 농업용수에 대해서는 강력한 억제정책을 시행함. 안정적인 용수 공급을 위해서 정부와 기업에서 자체적으로 재이용수 시설에 대한 투자가 이루어지고 있음.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>대만은 2004년 기후변화로 인한 물 부족 상황을 해결하기 위해 수자원 보호 전략을 승인하고, 2015년 대만 환경부는 재생 수자원 개발법을 발표, 2022년에는 한 차례 개정</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<ul className="list-disc list-outside pl-6 space-y-1 leading-relaxed">
|
||||
<li>대만은 물 소비량이 많은 산업(high water consuming industries)과 하수처리장(municipal wastewater treatment plants), 두 곳을 재이용수의 주요 공급원으로 계획</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
// app/main/layout.tsx
|
||||
import "../reuse.module.css";
|
||||
import type { ReactNode } from "react";
|
||||
import TopNav from "@/components/TopNav";
|
||||
import { StoreProvider } from "../store";
|
||||
|
||||
export default function ReuseLandingLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<StoreProvider>
|
||||
<TopNav />
|
||||
<div className="pt-[83px]">{children}</div> {/* TopNav 높이만큼 */}
|
||||
</>
|
||||
</StoreProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -532,6 +532,8 @@ const postSdmLoginData = async (data: DemandCompType) => {
|
||||
let loginSuccess = false;
|
||||
if (status === 200 && loginData != null) {
|
||||
alert("수요처 로그인에 성공하였습니다.");
|
||||
delete loginData.dmdAplyPswd;
|
||||
delete loginData.rgtr;
|
||||
sessionStorage.setItem("user", JSON.stringify(loginData));
|
||||
loginSuccess = true;
|
||||
} else if (status === 401) {
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
|
||||
import { sha512 } from 'js-sha512';
|
||||
import toast from "react-hot-toast";
|
||||
import { Button, Input } from "@common/components";
|
||||
|
||||
type LoginModalProps = {
|
||||
open: boolean;
|
||||
@@ -18,7 +17,7 @@ type FieldKey = "bizNo" | "workerNo" | "pw";
|
||||
const REGISTER_HREF = "/apply/dmdco";
|
||||
|
||||
function isBizNoValid(v: string) {
|
||||
return /^\d{3}-\d{2}-\d{5}$/.test(v);
|
||||
return /^\d{10}$/.test(v);
|
||||
}
|
||||
|
||||
function inputCls(opts: { hasError?: boolean; disabled?: boolean }) {
|
||||
@@ -31,6 +30,7 @@ function inputCls(opts: { hasError?: boolean; disabled?: boolean }) {
|
||||
export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps) {
|
||||
const titleId = useId();
|
||||
const router = useRouter();
|
||||
const { actionLogin } = useDmStore((state) => state);
|
||||
|
||||
const [bizNo, setBizNo] = useState(""); // 사업자번호
|
||||
const [workerNo, setWorkerNo] = useState(""); // 종사업자번호
|
||||
@@ -67,8 +67,9 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
if (noWorker) {
|
||||
setWorkerNo("");
|
||||
setErrors((p) => {
|
||||
const { workerNo: _unused, ...rest } = p;
|
||||
return rest;
|
||||
const nextErrors = { ...p };
|
||||
delete nextErrors.workerNo;
|
||||
return nextErrors;
|
||||
});
|
||||
}
|
||||
}, [noWorker]);
|
||||
@@ -84,11 +85,15 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
|
||||
if (!b) next.bizNo = "사업자등록번호를 입력해주세요.";
|
||||
else if (!isBizNoValid(b))
|
||||
next.bizNo = "사업자등록번호 형식이 올바르지 않습니다. (예: 123-45-67890)";
|
||||
next.bizNo = "사업자등록번호는 10자리 숫자만 입력 가능합니다.";
|
||||
|
||||
if (!noWorker && !w) next.workerNo = "종사자등록번호를 입력해주세요.";
|
||||
if (!noWorker && !w) next.workerNo = "종사업자등록번호를 입력해주세요.";
|
||||
else if (!noWorker && !/^\d{4}$/.test(w))
|
||||
next.workerNo = "종사업자등록번호는 4자리 숫자만 입력 가능합니다.";
|
||||
|
||||
if (!p) next.pw = "비밀번호를 입력해주세요.";
|
||||
else if (p.length < 8) next.pw = "비밀번호는 8자 이상 입력해주세요.";
|
||||
else if (p.length > 16) next.pw = "비밀번호는 16자 이내로 입력해주세요.";
|
||||
|
||||
setErrors(next);
|
||||
return next;
|
||||
@@ -126,44 +131,24 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const param = {
|
||||
dmdInfoId: bizNo + (noWorker ? "0000" : workerNo), // 종사업자번호 해당없음 "0000"으로 채움
|
||||
dmdAplyPswd: sha512.hex(pw),
|
||||
};
|
||||
await actionLogin(param as DemandCompType);
|
||||
|
||||
// ✅ TODO: 실제 로그인 API 연결 지점
|
||||
// const res = await fetch("/api/demand/login", {
|
||||
// method: "POST",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// body: JSON.stringify({ bizNo, workerNo: noWorker ? null : workerNo, pw }),
|
||||
// });
|
||||
// if (!res.ok) { ... }
|
||||
// const data = await res.json();
|
||||
// onSuccess?.({ userName: data.userName, accessToken: data.token });
|
||||
|
||||
// --- 데모: 비밀번호가 "1234"면 성공 처리(원하면 제거) ---
|
||||
const serverOk = pw.trim() === "1234";
|
||||
if (!serverOk) {
|
||||
setErrors((p) => ({ ...p, pw: "잘못된 비밀번호입니다. (데모: 1234 입력 시 성공)" }));
|
||||
setTouched((p) => ({ ...p, pw: true }));
|
||||
return;
|
||||
const sessionUser = sessionStorage.getItem("user");
|
||||
if (sessionUser) {
|
||||
const parsed = JSON.parse(sessionUser) as { dmdCoNm?: string };
|
||||
onSuccess?.({ userName: parsed.dmdCoNm ?? "수요처" });
|
||||
onClose();
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
onSuccess?.({ userName: "마인드원", accessToken: "demo_token" });
|
||||
onClose();
|
||||
router.refresh();
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// const handleLogin = () => {
|
||||
// const data = form.getValues();
|
||||
// const passwordStr = sha512.hex(data.password)
|
||||
// data.dmdAplyPswd = passwordStr;
|
||||
// const param = {
|
||||
// dmdInfoId: data.dmdInfoId + (isBznmTotNoDisabled ? '0000' : data.bznmTotNo),
|
||||
// dmdAplyPswd: data.dmdAplyPswd,
|
||||
// }
|
||||
// actionLogin(param as unknown as DemandCompType);
|
||||
// };
|
||||
|
||||
const initPassword = () => {
|
||||
alert("관리자(031-0000-0000)에게 비밀번호 초기화 요청을 유선으로 하시기 바랍니다.");
|
||||
}
|
||||
@@ -192,7 +177,7 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="닫기"
|
||||
className="absolute right-8 top-8 h-10 w-10 grid place-items-center rounded-full hover:bg-gray-100"
|
||||
className="absolute right-8 top-8 z-40 h-10 w-10 grid place-items-center rounded-full hover:bg-gray-100"
|
||||
>
|
||||
<span className="text-2xl leading-none">×</span>
|
||||
</button>
|
||||
@@ -219,7 +204,7 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
<input
|
||||
value={formatBusinessNumber(bizNo)}
|
||||
onChange={(e) => {
|
||||
setBizNo(e.target.value);
|
||||
setBizNo(e.target.value.replace(/\D/g, "").slice(0, 10));
|
||||
setErrors((p) => ({ ...p, bizNo: undefined }));
|
||||
}}
|
||||
onBlur={() => {
|
||||
@@ -257,7 +242,7 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
<input
|
||||
value={workerNo}
|
||||
onChange={(e) => {
|
||||
setWorkerNo(e.target.value);
|
||||
setWorkerNo(e.target.value.replace(/\D/g, "").slice(0, 4));
|
||||
setErrors((p) => ({ ...p, workerNo: undefined }));
|
||||
}}
|
||||
onBlur={() => {
|
||||
@@ -315,7 +300,6 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
|
||||
? "bg-sky-300 cursor-not-allowed"
|
||||
: "bg-sky-500 hover:bg-sky-600 active:bg-sky-700",
|
||||
].join(" ")}
|
||||
onClick={handleLogin}
|
||||
>
|
||||
{submitting ? "로그인 중..." : "로그인"}
|
||||
</button>
|
||||
|
||||
@@ -5,8 +5,8 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import LoginModal from "@/components/LoginModal";
|
||||
import { fetchAnonymousMenu, isMenuActive, type MenuItem } from "@/app/store/common";
|
||||
import { useDmStore } from "@/app/store";
|
||||
|
||||
// const REGISTER_HREF = "/reuse/apply/place";
|
||||
const LS_TOKEN_KEY = "reuse_accessToken";
|
||||
const LS_USER_KEY = "reuse_userName";
|
||||
|
||||
@@ -21,6 +21,7 @@ export default function TopNav() {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const { actionLogout } = useDmStore((state) => state);
|
||||
const [loginOpen, setLoginOpen] = useState(false);
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
const [topMenus, setTopMenus] = useState<MenuItem[]>([]);
|
||||
@@ -68,11 +69,12 @@ export default function TopNav() {
|
||||
|
||||
const openLogin = () => setLoginOpen(true);
|
||||
|
||||
const logout = () => {
|
||||
const logout = async () => {
|
||||
localStorage.removeItem(LS_TOKEN_KEY);
|
||||
localStorage.removeItem(LS_USER_KEY);
|
||||
setUserName(null);
|
||||
router.refresh();
|
||||
await actionLogout();
|
||||
router.push("/main");
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,15 @@ const compat = new FlatCompat({
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
files: ["**/*.{ts,tsx,mts,cts}"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{rules:{
|
||||
// other rules..
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
Reference in New Issue
Block a user