dm_new: 수요처등록수정

This commit is contained in:
2026-02-20 17:31:46 +09:00
parent b0baff824b
commit 46c1d33750
10 changed files with 944 additions and 133 deletions
@@ -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>
);
}
+3 -3
View File
@@ -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>
);
}
+2
View File
@@ -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) {
+26 -42
View File
@@ -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 -3
View File
@@ -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 (
+9
View File
@@ -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",