Files
frontend/apps/dm/components/BusinessSubNav.tsx
T

208 lines
7.5 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { usePathname } from "next/navigation";
type SectionItem = {
id: string;
label: string;
};
const PAGE_SECTION_MAP: Record<string, SectionItem[]> = {
// #물 재이용 소개 -물 재이용 법-
law: [
{ id: "law-plan", label: "법률 제·개정 및 기본계획 수립" },
{ id: "plan-performance", label: "제2차 계획 대비 실적 현황" },
{ id: "domestic-change", label: "국내 여건 변화" },
{ id: "policy-history", label: "물 재이용 정책 변화 추이" },
{ id: "law-relation", label: "타법령과의 관계 검토" },
{ id: "domestic-linkage", label: "국내 연계 현황" },
],
// #물 재이용 소개 - 빗물 이용시설 -
rain: [
{ id: "rain-intro", label: "빗물 재이용 소개" },
{ id: "rain-mandatory", label: "빗물이용시설 설치 관리 법적 의무 대상" },
{ id: "rain-design", label: "빗물이용시설 설계 방안" },
{ id: "rain-standards", label: "빗물이용시설 시설기준 및 관리기준" },
],
// #물 재이용 소개 - 중수도 -
reuseWater: [
{ id: "reuse-intro", label: "중수 재이용 소개" },
{ id: "reuse-mandatory", label: "중수도 설치 의무 대상" },
{ id: "reuse-standard", label: "중수도 시설기준 및 관리기준" },
],
// #물 재이용 소개 - 하수처리수 -
treated: [
{ id: "treated-intro", label: "하수처리수 개요" },
{ id: "treated-mandatory", label: "하수처리수 재이용시설 의무 대상" },
],
// #물 재이용 현황 - 빗물이용시설(현황) -
statusRain: [
{ id: "status-rain-building", label: "빗물이용시설의 건축물별 빗물이용량 현황" },
{ id: "status-rain-purpose-distribution", label: "빗물이용시설의 용도별 분포(2023년 기준)" },
{ id: "status-rain-purpose-usage", label: "빗물이용시설의 용도별 빗물이용량 현황" },
],
// #물 재이용 현황 - 중수도(현황) -
statusGreywater: [
{ id: "status-greywater-overall", label: "전체 중수도 설치 및 이용 현황" },
{ id: "status-greywater-factory", label: "공장 및 발전시설 내 중수도 설치 현황" },
{ id: "status-greywater-building", label: "중수도 건축물별 이용량 현황" },
{ id: "status-greywater-usage", label: "중수도 용도별 이용량 현황" },
],
// #물 재이용 현황 - 하수처리수 재이용시설(현황) -
statusTreated: [
{ id: "status-treated-year", label: "연도별 하수처리수 재이용 현황" },
{ id: "status-treated-usage", label: "용도별 하수처리수 재이용 현황" },
{ id: "status-treated-facility", label: "하수처리시설 설치 및 이용 현황" },
{ id: "status-treated-region", label: "시도별 하수처리수 재이용 현황" },
],
// #물 재이용 사례 - 빗물 재이용 사례 -
caseRain: [
{ id: "suwon", label: "경기도 수원시" },
{ id: "jeju", label: "제주특별자치도" },
],
// #물 재이용 사례 - 중수 재이용 사례 -
greywater: [
{ id: "ansan", label: "경기도 안산시" },
{ id: "yongin", label: "경기도 용인시" },
{ id: "pyeongtaek", label: "경기도 평택시" },
],
// #물 재이용 사례 - 하수 재이용 사례 -
sewage: [
{ id: "bolyeong", label: "충청남도 보령시" },
{ id: "cheongju", label: "청주시" },
{ id: "guri", label: "구리시 도로자동청소시스템" },
{ id: "hwanggu", label: "수원시 황구지천" },
{ id: "paju", label: "파주시" },
{ id: "yeosu", label: "여수시" },
],
// #물 재이용 사례 - 하수 재이용 사례 -
globalCase: [
{ id: "overview", label: "사례 개요" },
{ id: "taiwan", label: "대만" },
{ id: "australia", label: "호주" },
{ id: "singapore", label: "싱가포르" },
{ id: "eu", label: "EU" },
],
};
function getSectionKey(pathname: string): string {
// 물 재이용 소개
if (pathname.includes("/intro/law")) return "law"; // 물 재이용 법
if (pathname.includes("/intro/rain")) return "rain"; // 빗물 이용시설
if (pathname.includes("/intro/reuseWater")) return "reuseWater"; // 중수도
if (pathname.includes("/intro/treated")) return "treated"; // 하수처리수
// 물 재이용 현황
if (pathname.includes("/status/rain")) return "statusRain";
if (pathname.includes("/status/greywater")) return "statusGreywater";
if (pathname.includes("/status/sewage")) return "statusTreated";
// 물 재이용 사례
if (pathname.includes("/example/rain")) return "caseRain";
if (pathname.includes("/example/greywater")) return "greywater";
if (pathname.includes("/example/sewage")) return "sewage";
if (pathname.includes("/example/global")) return "globalCase";
const segments = pathname.split("/").filter(Boolean);
return segments[segments.length - 1] ?? "";
}
export default function BusinessSubNav() {
const pathname = usePathname();
const sectionKey = getSectionKey(pathname);
const items = useMemo(() => PAGE_SECTION_MAP[sectionKey] ?? [], [sectionKey]);
const [activeId, setActiveId] = useState("");
useEffect(() => {
if (!items.length) return;
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
const firstVisible = visible[0];
if (firstVisible) {
setActiveId(firstVisible.target.id);
}
},
{
rootMargin: "-120px 0px -55% 0px",
threshold: [0.2, 0.35, 0.5, 0.7],
},
);
const elements = items
.map((item) => document.getElementById(item.id))
.filter(Boolean) as HTMLElement[];
elements.forEach((el) => observer.observe(el));
const hash = window.location.hash.replace("#", "");
const initialActive =
items.find((item) => item.id === hash)?.id ?? items[0]?.id ?? "";
setActiveId(initialActive);
return () => observer.disconnect();
}, [items]);
const handleMove = (id: string) => {
setActiveId(id);
const target = document.getElementById(id);
if (!target) return;
window.history.replaceState(null, "", `#${id}`);
const top = target.getBoundingClientRect().top + window.scrollY - 120;
window.scrollTo({
top,
behavior: "smooth",
});
};
if (!items.length) return null;
return (
<aside className="fixed right-2 top-1/3 z-50 w-[220px] shrink-0">
<ul className="space-y-2">
{items.map((item) => {
const active = activeId === item.id;
return (
<li key={item.id}>
<button
type="button"
onClick={() => handleMove(item.id)}
className={[
"flex w-full items-center gap-2 rounded-full bg-white px-3 py-2 text-left transition-all duration-200",
active
? "border border-sky-500 text-[#2B2B2B]"
: "text-[#B2B8C2]",
].join(" ")}
>
<span
className={[
"h-[12px] w-[12px] shrink-0 rounded-full border",
active ? "border-sky-500 bg-sky-500" : "border-[#D0D5DD]",
].join(" ")}
/>
<span className={active ? "text-sm font-semibold" : "text-sm"}>
{item.label}
</span>
</button>
</li>
);
})}
</ul>
</aside>
);
}