@@ -1,4 +1,3 @@
|
||||
// apps/dm/app/(landing)/reuse/_components/ImageSlider.tsx
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
@@ -9,167 +8,151 @@ type Slide = { src: string; alt?: string };
|
||||
type Props = {
|
||||
slides: Slide[];
|
||||
className?: string;
|
||||
maxWidthClassName?: string; // 예: "max-w-[760px]"
|
||||
maxWidthClassName?: string;
|
||||
autoPlayMs?: number;
|
||||
imageHeightClassName?: string;
|
||||
};
|
||||
|
||||
const PER_VIEW = 2;
|
||||
|
||||
export default function CaseImageSlider({
|
||||
slides,
|
||||
className = "",
|
||||
maxWidthClassName = "max-w-[760px]",
|
||||
maxWidthClassName = "max-w-[700px]",
|
||||
autoPlayMs = 4500,
|
||||
imageHeightClassName = "h-[340px]",
|
||||
}: Props) {
|
||||
const safeSlides = useMemo(() => slides.filter(Boolean), [slides]);
|
||||
const [idx, setIdx] = useState(0);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
|
||||
const startX = useRef(0);
|
||||
const deltaX = useRef(0);
|
||||
|
||||
const total = safeSlides.length;
|
||||
const canSlide = total > 1;
|
||||
const pageCount = Math.ceil(safeSlides.length / PER_VIEW);
|
||||
const canSlide = pageCount > 1;
|
||||
|
||||
const go = (dir: number) => {
|
||||
const goTo = (nextPage: number) => {
|
||||
if (!canSlide) return;
|
||||
setIdx((prev) => {
|
||||
const next = prev + dir;
|
||||
if (next < 0) return total - 1;
|
||||
if (next >= total) return 0;
|
||||
return next;
|
||||
});
|
||||
|
||||
if (nextPage < 0) setPage(pageCount - 1);
|
||||
else if (nextPage >= pageCount) setPage(0);
|
||||
else setPage(nextPage);
|
||||
};
|
||||
|
||||
// ← → 키보드 지원(선택)
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (!canSlide) return;
|
||||
if (e.key === "ArrowLeft") go(-1);
|
||||
if (e.key === "ArrowRight") go(1);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [canSlide, total]);
|
||||
const goNext = () => goTo(page + 1);
|
||||
const goPrev = () => goTo(page - 1);
|
||||
|
||||
// 드래그(마우스/터치)로 넘기기
|
||||
const onPointerDown = (e: React.PointerEvent) => {
|
||||
// ✅ 자동롤링 (hover 시 정지 포함)
|
||||
useEffect(() => {
|
||||
if (!canSlide || dragging || isHover) return;
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
setPage((prev) => (prev + 1 >= pageCount ? 0 : prev + 1));
|
||||
}, autoPlayMs);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [canSlide, dragging, isHover, pageCount, autoPlayMs]);
|
||||
|
||||
// ✅ swipe
|
||||
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!canSlide) return;
|
||||
|
||||
setDragging(true);
|
||||
startX.current = e.clientX;
|
||||
deltaX.current = 0;
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
const onPointerMove = (e: React.PointerEvent) => {
|
||||
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!dragging) return;
|
||||
deltaX.current = e.clientX - startX.current;
|
||||
};
|
||||
|
||||
const onPointerUp = () => {
|
||||
if (!dragging) return;
|
||||
|
||||
setDragging(false);
|
||||
|
||||
const threshold = 60; // 넘김 감도
|
||||
if (deltaX.current > threshold) go(-1);
|
||||
else if (deltaX.current < -threshold) go(1);
|
||||
const threshold = 50;
|
||||
|
||||
if (deltaX.current > threshold) goPrev();
|
||||
if (deltaX.current < -threshold) goNext();
|
||||
|
||||
deltaX.current = 0;
|
||||
};
|
||||
|
||||
if (!total) return null;
|
||||
if (!safeSlides.length) return null;
|
||||
|
||||
return (
|
||||
<section className={className}>
|
||||
<div className={`mx-auto ${maxWidthClassName}`}>
|
||||
<div className="relative">
|
||||
{/* 좌측 버튼 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => go(-1)}
|
||||
className="absolute left-[-60px] top-1/2 -translate-y-1/2 z-10
|
||||
w-[80px] h-[80px] rounded-full border border-black/20 bg-white
|
||||
grid place-items-center shadow-sm hover:bg-black/[0.03]"
|
||||
>
|
||||
<Image
|
||||
src="/images/landing/icon_left_arrow.png"
|
||||
alt="이전"
|
||||
width={12}
|
||||
height={22}
|
||||
className="w-[12px] h-[22px]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
||||
{/* 우측 버튼 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => go(1)}
|
||||
className="absolute right-[-60px] top-1/2 -translate-y-1/2 z-10
|
||||
w-[80px] h-[80px] rounded-full border border-black/20 bg-white
|
||||
grid place-items-center shadow-sm hover:bg-black/[0.03]"
|
||||
>
|
||||
<Image
|
||||
src="/images/landing/icon_right_arrow.png"
|
||||
alt="다음"
|
||||
width={12}
|
||||
height={22}
|
||||
className="w-[12px] h-[22px]"
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
||||
{/* 뷰포트 */}
|
||||
<div
|
||||
className="overflow-hidden select-none touch-pan-y cursor-grab active:cursor-grabbing"
|
||||
onMouseEnter={() => setIsHover(true)} // ✅ hover 시작 → 정지
|
||||
onMouseLeave={() => setIsHover(false)} // ✅ hover 끝 → 재시작
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
"overflow-hidden",
|
||||
"select-none touch-pan-y",
|
||||
"cursor-grab active:cursor-grabbing",
|
||||
"w-[583px] h-[400px] mx-auto",
|
||||
].join(" ")}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onPointerCancel={onPointerUp}
|
||||
className="flex transition-transform duration-500 ease-out"
|
||||
style={{
|
||||
transform: `translateX(-${page * 100}%)`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex transition-transform duration-300 ease-out"
|
||||
style={{ transform: `translateX(-${idx * 100}%)` }}
|
||||
>
|
||||
{safeSlides.map((s, i) => (
|
||||
<div key={`${s.src}-${i}`} className="min-w-full">
|
||||
<Image
|
||||
src={s.src}
|
||||
alt={s.alt ?? `slide-${i + 1}`}
|
||||
width={583}
|
||||
height={400}
|
||||
className="w-full h-full object-contain"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
draggable={false}
|
||||
/>
|
||||
{Array.from({ length: pageCount }).map((_, pageIndex) => {
|
||||
const pageSlides = safeSlides.slice(
|
||||
pageIndex * PER_VIEW,
|
||||
pageIndex * PER_VIEW + PER_VIEW
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={pageIndex}
|
||||
className="grid min-w-full grid-cols-2 gap-3"
|
||||
>
|
||||
{pageSlides.map((slide, i) => (
|
||||
<div
|
||||
key={`${slide.src}-${i}`}
|
||||
className={`relative ${imageHeightClassName} overflow-hidden rounded-[20px] bg-neutral-100`}
|
||||
>
|
||||
<Image
|
||||
src={slide.src}
|
||||
alt={slide.alt ?? `slide-${pageIndex * PER_VIEW + i + 1}`}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 50vw, 340px"
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 도트 */}
|
||||
{canSlide && (
|
||||
<div className="mt-3 flex items-center justify-center gap-2">
|
||||
{safeSlides.map((_, i) => {
|
||||
const active = i === idx;
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => setIdx(i)}
|
||||
aria-label={`${i + 1}번 슬라이드`}
|
||||
className={[
|
||||
"h-2 w-2 rounded-full",
|
||||
active ? "bg-black/70" : "bg-black/20 hover:bg-black/35",
|
||||
].join(" ")}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="mt-5 flex items-center justify-center gap-[6px]">
|
||||
{Array.from({ length: pageCount }).map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => setPage(i)}
|
||||
className={[
|
||||
"h-[8px] w-[8px] rounded-full transition-all",
|
||||
page === i ? "bg-[#18BCEB]" : "bg-[#D8DDE3]",
|
||||
].join(" ")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import Image from "next/image";
|
||||
* - BUGFIX 2) 슬라이드 중(busy) clipWidth 변화로 트랙 스냅(-W) 덮어쓰지 않게 분리
|
||||
*/
|
||||
|
||||
type TabKey = "rain" | "reuse" | "sewage" | "global";
|
||||
type TabKey = "rain" | "greywater" | "sewage" | "global";
|
||||
|
||||
type CaseItem = {
|
||||
id: string;
|
||||
@@ -34,7 +34,7 @@ type CaseItem = {
|
||||
|
||||
const TABS: { key: TabKey; label: string }[] = [
|
||||
{ key: "rain", label: "빗물 재이용 사례" },
|
||||
{ key: "reuse", label: "중수 재이용 사례" },
|
||||
{ key: "greywater", label: "중수 재이용 사례" },
|
||||
{ key: "sewage", label: "하수 재이용 사례" },
|
||||
{ key: "global", label: "해외 재이용 사례" },
|
||||
];
|
||||
@@ -43,113 +43,143 @@ const TABS: { key: TabKey; label: string }[] = [
|
||||
const MOCK: Record<TabKey, CaseItem[]> = {
|
||||
rain: [
|
||||
{
|
||||
id: "rain-0",
|
||||
title: "경기도 수원시 빗물이용과 중수도",
|
||||
desc: "(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기…",
|
||||
imageSrc: "/images/landing/rain_01.png",
|
||||
href: "/example/rain/#suwon",
|
||||
id: "rain-1",
|
||||
title: "경기도 수원시 빗물 재이용",
|
||||
desc: "월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물공급기를 설치하여 축서기 도시 열섬효과 및 도로 비산먼지 저감",
|
||||
imageSrc: "/images/landing/business/rain/suwon_01.png",
|
||||
href: "/example#example-rain-suwon",
|
||||
},
|
||||
{
|
||||
id: "rain-2",
|
||||
title: "제주특별자치도 빗물 재이용",
|
||||
desc: "강우시 비닐하우스에서 발생하는 대체수자원인 빗물을 순환 활용하고자 하는 사업으로, 지하수 사용 저감과 집중호우시 도로와 농경지 침수피해 예방",
|
||||
imageSrc: "/images/landing/business/rain/jeju_01.png",
|
||||
href: "/example#example-rain-jeju",
|
||||
},
|
||||
],
|
||||
greywater: [
|
||||
{
|
||||
id: "greywater-1",
|
||||
title: "경기도 안산시 중수 재이용",
|
||||
desc: "수자원을 효율적으로 활용하여 지속가능성 도모 및 물 절약 실현을 위해 안산시 중수 재이용 사업 추진, 공공기관 및 산업체에 중수 공급",
|
||||
imageSrc: "/images/landing/business/greywater/no_img.jpg",
|
||||
href: "/example#example-greywater-ansan",
|
||||
},
|
||||
{
|
||||
id: "greywater-2",
|
||||
title: "경기도 용인시 중수 재이용",
|
||||
desc: "물 부족 등의 비상상황에 대한 대비, 하수라도 사용할 집결 등 수자원의 효율적 활용과 지속가능성 도모를 위해 용인시 중수 재이용 사업 추진, 공공기관 및 산업체에 중수 공급",
|
||||
imageSrc: "/images/landing/business/greywater/no_img.jpg",
|
||||
href: "/example#example-greywater-yongin",
|
||||
},
|
||||
{
|
||||
id: "greywater-3",
|
||||
title: "경기도 평택시 중수 재이용",
|
||||
desc: "수자원을 효율적으로 활용하여 지속가능성 도모 및 물 절약 실현을 위해 평택시 중수 재이용 사업 추진, 공공기관 및 산업체에 중수 공급",
|
||||
imageSrc: "/images/landing/business/greywater/no_img.jpg",
|
||||
href: "/example#example-greywater-pyeongtaek",
|
||||
},
|
||||
// {
|
||||
// id: "rain-2",
|
||||
// title: "경기도 수원시 빗물이용과 중수도",
|
||||
// desc: "(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기…",
|
||||
// imageSrc: "/images/landing/rain_01.png",
|
||||
// href: "#",
|
||||
// },
|
||||
],
|
||||
reuse: Array.from({ length: 6 }).map((_, i) => ({
|
||||
id: `reuse-${i}`,
|
||||
title: `중수 재이용 사례 ${i + 1}`,
|
||||
desc: "설명 문구가 들어갑니다. (최대 2~3줄) …",
|
||||
imageSrc: "/images/landing/sample_case.png",
|
||||
href: "#",
|
||||
})),
|
||||
sewage: [
|
||||
{
|
||||
id: "sewage-1",
|
||||
title: "경기도 파주시 하수 재이용",
|
||||
desc: "운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에...",
|
||||
imageSrc: "/images/landing/sewage_01.png",
|
||||
href: "/example/sewage/#paju",
|
||||
title: "구리시 하수 재이용",
|
||||
desc: "고농도 미세먼지 발생 및 폭염의 급증에 따라 공공하수처리시설 발생 재이용수 일부를 도로자동청소시스템에 활용하여 미세먼지 저감 및 폭염 취약성 완화",
|
||||
imageSrc: "/images/landing/business/sewage/guri_01.png",
|
||||
href: "/example#example-sewage-guri",
|
||||
},
|
||||
{
|
||||
id: "sewage-2",
|
||||
title: "충남보령 하수재이용",
|
||||
desc: "보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업...",
|
||||
imageSrc: "/images/landing/sewage_02.png",
|
||||
href: "/example/sewage/#bolyeong",
|
||||
title: "부천시 하수재이용",
|
||||
desc: "굴포하수처리시설 하수처리수 재이용수 생산시설 설치 운영을 통해 여월천 등 부천 관내 하천의 건천화 방지, 하천 생태계 보존 및 주민들에게 친수 공간 제공",
|
||||
imageSrc: "/images/landing/business/sewage/bucheon_01.png",
|
||||
href: "/example#example-sewage-bucheon",
|
||||
},
|
||||
{
|
||||
id: "sewage-3",
|
||||
title: "여수시 하수처리수 재이용",
|
||||
desc: "(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로...",
|
||||
imageSrc: "/images/landing/sewage_03.png",
|
||||
href: "/example/sewage/#yeosu",
|
||||
title: "수원시 하수처리수 재이용",
|
||||
desc: "황구지천 공공하수처리시설 하수처리수 재이용사업으로 황구 호매실천 및 금곡천 건천화 방지로 주민들에게 친수 공간 제공 및 하천 생태계 보전",
|
||||
imageSrc: "/images/landing/business/sewage/suwon_01.png",
|
||||
href: "/example#example-sewage-suwon",
|
||||
},
|
||||
{
|
||||
id: "sewage-4",
|
||||
title: "청주시 하수 재이용 시설",
|
||||
desc: "청주시 하수처리수 재이용 민간투자사업(BTO) 청주시 하수처리수 재이용 민간투자사업(BTO) 청주시 하수처리수 재이용 민간투자사업(BTO)...",
|
||||
imageSrc: "/images/landing/sewage_04.png",
|
||||
href: "/example/sewage/#cheongju",
|
||||
title: "파주시 하수 재이용 시설",
|
||||
desc: "운정 및 금촌하수처리시설의 하수처리수를 막공법으로 재처리하여 파주LCD산업단지 내 3개 업체에 공업용수를 공급(LG디스플레이, 희성전자, 엔비텍)",
|
||||
imageSrc: "/images/landing/business/sewage/paju_01.png",
|
||||
href: "/example#example-sewage-paju",
|
||||
},
|
||||
{
|
||||
id: "sewage-5",
|
||||
title: "수원시 황구지천 하천유지용수",
|
||||
desc: "황구지천 공공하수처리시설 하수처리수 재이용사업으로 향후 호매실천 및 금곡천 건천화 방지로...",
|
||||
imageSrc: "/images/landing/sewage_05.png",
|
||||
href: "/example/sewage/#hwanggu",
|
||||
title: "포항시 하수 재이용 시설",
|
||||
desc: "포항시 내 공단 개발 확대로 지속적이고 안정적인 공업용수 공급 추진(포스코, 동국산업 등), 포항하수처리장 하수처리수를 재처리하여 공업용수로 공급",
|
||||
imageSrc: "/images/landing/business/sewage/pohang_01.png",
|
||||
href: "/example#example-sewage-pohang",
|
||||
},
|
||||
{
|
||||
id: "sewage-6",
|
||||
title: "구리시 도로자동청소시스템",
|
||||
desc: "고농도 미세먼지 발생 및 폭염의 급증에 따라 공공하수처리시설 발생 재이용수 일부를 도로자동청소시스템에 활용...",
|
||||
imageSrc: "/images/landing/sewage_06.png",
|
||||
href: "/example/sewage/#guri",
|
||||
title: "울산광역시 하수 재이용 시설",
|
||||
desc: "18년 갈수기시 낙동강 수질 악화로 울산석유화학단지 용수 공급 부족을 해소하기 위해 미포산단 내 한화케미칼 등 8개사 공업용수 공급",
|
||||
imageSrc: "/images/landing/business/sewage/ulsan_01.png",
|
||||
href: "/example#example-sewage-ulsan",
|
||||
},
|
||||
// ...Array.from({ length: 6 }).map((_, i) => ({
|
||||
// id: `sewage-x-${i}`,
|
||||
// title: `하수 재이용 사례 ${i + 4}`,
|
||||
// desc: "설명 문구가 들어갑니다. (최대 2~3줄) …",
|
||||
// imageSrc: "/images/landing/sample_case.png",
|
||||
// href: "#",
|
||||
// })),
|
||||
{
|
||||
id: "sewage-7",
|
||||
title: "여수시 하수 재이용 시설",
|
||||
desc: "수요자가 원하는 양질의 공업용수를 저렴하게 공급하여 한정된 수자원의 효율적 이용 및 지역적 물 부족 해소, 하수재이용을 통한 오염총량 저감 및 방류수역 수질개선을 위한 여수국가산단 내 공업용수 공급(GS칼텍스, YNCC 등)",
|
||||
imageSrc: "/images/landing/business/sewage/yeosu_01.png",
|
||||
href: "/example#example-sewage-yeosu",
|
||||
},
|
||||
{
|
||||
id: "sewage-8",
|
||||
title: "보령시 하수 재이용 시설",
|
||||
desc: "한국중부발전(주)의 보령 및 신보령화력발전소에 10,000㎥/일의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진, 하수처리수 재이용을 통한 수자원 절약 및 지역 물 부족 해소",
|
||||
imageSrc: "/images/landing/business/sewage/boryeong_01.png",
|
||||
href: "/example#example-sewage-boryeong",
|
||||
},
|
||||
{
|
||||
id: "sewage-9",
|
||||
title: "아산시 하수 재이용 시설",
|
||||
desc: "신도시 개발에 따른 인구 급증으로 발생하는 하수를 재처리하여 탕정산업단지(삼성디스플레이)에 공업용수를 안정적으로 공급함으로써 지역기반 및 활성화를 도모",
|
||||
imageSrc: "/images/landing/business/sewage/asan_01.png",
|
||||
href: "/example#example-sewage-asan",
|
||||
}
|
||||
],
|
||||
global: [
|
||||
{
|
||||
id: "global-1",
|
||||
title: "해외 재이용 사례 개요",
|
||||
desc: "2024년 하수도 통계 기준 15.6%이며, 제2차 물 재이용 기본 계획을 고려한 2030년 목표는 17%, 우리나라의 하수 처리 재이용률은 15% 수준...",
|
||||
imageSrc: "/images/landing/overview_01.png",
|
||||
href: "/example/globalCase/overview",
|
||||
desc: "전 세계적으로 물 재이용은 호주, 유럽, 아프리카, 미국 등 가뭄이 잦거나 심각한 가뭄이 발생한 국가에서 많이 이루어짐. 이스라엘, 쿠웨이트 등 중동 및 북아프리카 지역은 가용 수자원이 제한되어 있어 재이용량 비중도 매우 높은 편",
|
||||
imageSrc: "/images/landing/business/globalCase/overview_01.png",
|
||||
href: "/example#example-global-overview",
|
||||
},
|
||||
{
|
||||
id: "global-2",
|
||||
title: "[대만] 국가 주도의 공업용수 공급",
|
||||
desc: "대만은 물 재이용에 적극적이며 국가주도로 하수재이용 사업 시행(안핑), 안핑을 포함한 16건 공사 완료시 628,100 CMD 공급 가능...",
|
||||
imageSrc: "/images/landing/taiwan_01.png",
|
||||
href: "/example/globalCase/taiwan",
|
||||
desc: "수자원 지속 가능성 향상, 재생 수자원 활용을 통해 물부족 문제 해결. 재생 수자원 정의·분류, 이용 의무, 이용 제한, 수질 기준 등",
|
||||
imageSrc: "/images/landing/business/globalCase/taiwan_01.png",
|
||||
href: "/example#example-global-taiwan",
|
||||
},
|
||||
{
|
||||
id: "global-3",
|
||||
title: "[호주] 생활 속 물 재이용 / 광역 물 재이용수 공급",
|
||||
desc: "SEQ Water Grid는 브리즈번 남동부지역의 식수 공급을 위해 설정한 광역 수로망, 이 광역수로망을 활용하여 물이 부족한 지역으로 물을 공급...",
|
||||
imageSrc: "/images/landing/australia_01.png",
|
||||
href: "/example/globalCase/australia",
|
||||
title: "[싱가포르] 수요가 높은 산업의 물 재이용 의무화",
|
||||
desc: "2003년 공식 NEWater 공장을 가동했으며 신뢰확보를 위해 NEWater Visitor Center를 함께 건설. NEWater 전체파이프 공급 → 반도체 공장에서 가장 많이 사용",
|
||||
imageSrc: "/images/landing/business/globalCase/singapore_01.png",
|
||||
href: "/example#example-global-singapore",
|
||||
},
|
||||
{
|
||||
id: "global-4",
|
||||
title: "[싱가포르] 수요가 높은 산업의 물 재이용 의무화",
|
||||
desc: "2003년 공식 NEWater 공장을 가동했으며 신뢰확보를 위해 NEWater Visitor Center를 함께 건설, NEWater 전체파이프 공급 → 반도체 공장에서 가장 많이 사용...",
|
||||
imageSrc: "/images/landing/singapore_01.png",
|
||||
href: "/example/globalCase/singapore",
|
||||
title: "[호주] 생활 속 물 재이용 / 광역 물 재이용수 공급",
|
||||
desc: "SEQ Water Grid는 브리즈번 남동부지역의 식수 공급을 위해 설정한 광역 수로망. 이 광역수로망을 활용하여 물이 부족한 지역으로 물을 공급",
|
||||
imageSrc: "/images/landing/business/globalCase/australia_01.png",
|
||||
href: "/example#example-global-australia",
|
||||
},
|
||||
{
|
||||
id: "global-5",
|
||||
title: "[EU] 법률을 제정, EU국가에서 물 재이용 유도",
|
||||
desc: "유럽 지역의 물 부족 문제를 인식하고 ’20년 물 재이용 법률을 제정 (2020/741), 2023년부터 모든 EU국가에서 시행...",
|
||||
imageSrc: "/images/landing/eu_01.png",
|
||||
href: "/example/globalCase/eu",
|
||||
desc: "유럽 지역의 물 부족 문제를 인식하고 ’20년 물 재이용 법률을 제정 (2020/741), 2023년부터 모든 EU국가에서 시행",
|
||||
imageSrc: "/images/landing/business/globalCase/eu_01.png",
|
||||
href: "/example#example-global-eu",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -249,7 +279,7 @@ export default function CaseTabsCarousel() {
|
||||
// 탭별 마지막 그룹 인덱스 기억
|
||||
const tabIndexRef = useRef<Record<TabKey, number>>({
|
||||
rain: 0,
|
||||
reuse: 0,
|
||||
greywater: 0,
|
||||
sewage: 0,
|
||||
global: 0,
|
||||
});
|
||||
|
||||
+43
-56
@@ -1,68 +1,55 @@
|
||||
"use client";
|
||||
|
||||
export type LnbMenuItem = {
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export type LnbMenu = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type LnbConfig = {
|
||||
title: string;
|
||||
menus: LnbMenuItem[];
|
||||
type Props = {
|
||||
menus?: LnbMenu[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const LNB_MAP = {
|
||||
intro: {
|
||||
title: "소개",
|
||||
menus: [
|
||||
{ label: "물 재이용 법", href: "/intro/law" },
|
||||
{ label: "빗물 이용시설", href: "/intro/rain" },
|
||||
{ label: "중수도", href: "/intro/reuseWater" },
|
||||
{ label: "하수처리수", href: "/intro/treated" },
|
||||
],
|
||||
},
|
||||
const defaultMenus: LnbMenu[] = [
|
||||
{ label: "수요처 등록", href: "/apply/dmdco" },
|
||||
{ label: "수요정보등록", href: "/apply/register" },
|
||||
{ label: "수요신청현황", href: "/apply/status" },
|
||||
];
|
||||
|
||||
status: {
|
||||
title: "현황",
|
||||
menus: [
|
||||
{ label: "빗물이용시설(현황)", href: "/status/rain" },
|
||||
{ label: "중수도(현황)", href: "/status/greywater" },
|
||||
{ label: "하수처리수 재이용시설(현황)", href: "/status/sewage" },
|
||||
],
|
||||
},
|
||||
export default function Lnb({ menus, className = "" }: Props) {
|
||||
const pathname = usePathname();
|
||||
const navMenus = menus ?? defaultMenus;
|
||||
|
||||
case: {
|
||||
title: "사례",
|
||||
menus: [
|
||||
{ label: "빗물 재이용 사례", href: "/example/rain" },
|
||||
{ label: "중수 재이용 사례", href: "/example/greywater" },
|
||||
{ label: "하수 재이용 사례", href: "/example/sewage" },
|
||||
{ label: "해외 재이용 사례", href: "/example/globalCase" },
|
||||
],
|
||||
},
|
||||
return (
|
||||
<aside className={`fixed right-2 top-[110px] z-50 w-[260px] ${className}`}>
|
||||
<ul className="space-y-1">
|
||||
{navMenus.map((menu) => {
|
||||
const isActive =
|
||||
pathname === menu.href ||
|
||||
pathname.startsWith(`${menu.href}/`);
|
||||
|
||||
apply: {
|
||||
title: "수요신청",
|
||||
menus: [
|
||||
{ label: "수요처등록", href: "/apply/dmdco" },
|
||||
{ label: "수요정보등록", href: "/apply/register" },
|
||||
{ label: "수요신청현황", href: "/apply/status" },
|
||||
],
|
||||
},
|
||||
} satisfies Record<string, LnbConfig>;
|
||||
|
||||
export function getCategory(pathname: string): keyof typeof LNB_MAP {
|
||||
if (pathname.startsWith("/intro")) return "intro";
|
||||
if (pathname.startsWith("/status")) return "status";
|
||||
if (pathname.startsWith("/example")) return "case";
|
||||
if (pathname.startsWith("/apply")) return "apply";
|
||||
return "intro";
|
||||
}
|
||||
|
||||
export function isActiveMenu(pathname: string, href: string) {
|
||||
return pathname === href || pathname.startsWith(`${href}/`);
|
||||
}
|
||||
|
||||
// 왼쪽 메뉴는 더 이상 사용 안 함
|
||||
export default function Lnb() {
|
||||
return null;
|
||||
return (
|
||||
<li key={menu.href}>
|
||||
<Link
|
||||
href={menu.href}
|
||||
className={`
|
||||
block text-[16px] font-medium transition px-5 py-2
|
||||
${
|
||||
isActive
|
||||
? "text-[#00AEEF]"
|
||||
: "text-[#767676] hover:text-[#00AEEF]"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{menu.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -12,10 +12,10 @@ import { useDmStore } from "@/app/store";
|
||||
const LS_USER_KEY = "user";
|
||||
|
||||
const DEFAULT_NAV_ITEMS: Pick<MenuItem, "menuCd" | "menuKornNm" | "urlPath" | "newWndwYn">[] = [
|
||||
{ menuCd: 1, menuKornNm: "물 재이용 소개", urlPath: "/intro/law", newWndwYn: "N" },
|
||||
{ menuCd: 2, menuKornNm: "물 재이용 현황", urlPath: "/status/rain", newWndwYn: "N" },
|
||||
{ menuCd: 3, menuKornNm: "물 재이용 사례", urlPath: "/examle/rain", newWndwYn: "N" },
|
||||
{ menuCd: 4, menuKornNm: "수요신청", urlPath: "/apply/place", newWndwYn: "N" },
|
||||
{ menuCd: 1, menuKornNm: "물 재이용 소개", urlPath: "/intro", newWndwYn: "N" },
|
||||
{ menuCd: 2, menuKornNm: "물 재이용 현황", urlPath: "/status", newWndwYn: "N" },
|
||||
{ menuCd: 3, menuKornNm: "물 재이용 사례", urlPath: "/example", newWndwYn: "N" },
|
||||
{ menuCd: 4, menuKornNm: "수요신청", urlPath: "/apply/dmdco", newWndwYn: "N" },
|
||||
];
|
||||
|
||||
export default function TopNav() {
|
||||
|
||||
Reference in New Issue
Block a user