208 lines
7.5 KiB
TypeScript
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>
|
|
);
|
|
} |