대민서비스 물재이용 랜딩 페이지(수요신청 외 서브페이지 추가)

This commit is contained in:
LGS
2026-02-12 17:41:36 +09:00
parent 0de9e66988
commit d4775ff745
74 changed files with 2502 additions and 173 deletions
@@ -0,0 +1,519 @@
"use client";
import React, {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
/**
* CaseTabsCarousel.tsx (FINAL OPTIMIZED)
* - 탭별 데이터
* - 카드 perPage(1/2/3) 반응형
* - 카드 perPage개 = 1그룹, 좌/우 클릭 시 1그룹 단위 이동
* - 3그룹 가상화(prev/cur/next)로 DOM 최소화
* - px 기반 트랙 이동 + "측정 박스 분리"로 좌/우 비침 방지
* - stale state 제거(방향/인덱스 ref 관리)로 슬라이드 안정성 강화
* - BUGFIX 1) onTransitionEnd 버블링 방지 (target===currentTarget, transform만 처리)
* - BUGFIX 2) 슬라이드 중(busy) clipWidth 변화로 트랙 스냅(-W) 덮어쓰지 않게 분리
*/
type TabKey = "rain" | "reuse" | "sewage" | "global";
type CaseItem = {
id: string;
title: string;
desc: string;
imageSrc: string;
href?: string;
};
const TABS: { key: TabKey; label: string }[] = [
{ key: "rain", label: "빗물 재이용 사례" },
{ key: "reuse", label: "중수 재이용 사례" },
{ key: "sewage", label: "하수 재이용 사례" },
{ key: "global", label: "해외 재이용 사례" },
];
// 실제 데이터로 교체해서 쓰세요(구조 유지)
const MOCK: Record<TabKey, CaseItem[]> = {
rain: [
{
id: "rain-0",
title: "경기도 수원시 빗물이용과 중수도",
desc: "(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기…",
imageSrc: "/images/landing/rain_01.png",
href: "/reuse/business/rain/suwon",
},
// {
// 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: "#",
},
{
id: "sewage-2",
title: "충남보령 하수재이용",
desc: "보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업...",
imageSrc: "/images/landing/sewage_02.png",
href: "#",
},
{
id: "sewage-3",
title: "여수시 하수처리수 재이용",
desc: "(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로...",
imageSrc: "/images/landing/sewage_03.png",
href: "#",
},
...Array.from({ length: 6 }).map((_, i) => ({
id: `sewage-x-${i}`,
title: `하수 재이용 사례 ${i + 4}`,
desc: "설명 문구가 들어갑니다. (최대 2~3줄) …",
imageSrc: "/images/landing/sample_case.png",
href: "#",
})),
],
global: Array.from({ length: 6 }).map((_, i) => ({
id: `global-${i}`,
title: `해외 재이용 사례 ${i + 1}`,
desc: "설명 문구가 들어갑니다. (최대 2~3줄) …",
imageSrc: "/images/landing/sample_case.png",
href: "#",
})),
};
/* ---------------- utils ---------------- */
function chunk<T>(arr: T[], size: number) {
const res: T[][] = [];
if (size <= 0) return res;
for (let i = 0; i < arr.length; i += size) res.push(arr.slice(i, i + size));
return res;
}
function mod(n: number, m: number) {
return ((n % m) + m) % m;
}
function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
const onChange = () => setReduced(mq.matches);
onChange();
mq.addEventListener?.("change", onChange);
return () => mq.removeEventListener?.("change", onChange);
}, []);
return reduced;
}
/**
* wrapRef: perPage 계산용
* clipRef: "패딩 없는 측정 박스" 폭(px) 계산용
*/
function useCarouselMeasure() {
const wrapRef = useRef<HTMLDivElement | null>(null);
const clipRef = useRef<HTMLDivElement | null>(null);
const [perPage, setPerPage] = useState(3);
const [clipWidth, setClipWidth] = useState(0);
// perPage: container width로 반응형 결정 (rAF로 디바운스)
useEffect(() => {
const el = wrapRef.current;
if (!el) return;
let raf = 0;
const calc = () => {
const w = el.clientWidth;
if (w < 640) setPerPage(1);
else if (w < 1024) setPerPage(2);
else setPerPage(3);
};
const ro = new ResizeObserver(() => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(calc);
});
calc();
ro.observe(el);
return () => {
cancelAnimationFrame(raf);
ro.disconnect();
};
}, []);
// clipWidth: 패딩 없는 박스에서 width를 정확히 측정 (sub-pixel 반올림)
useEffect(() => {
const el = clipRef.current;
if (!el) return;
let raf = 0;
const calc = () => {
const w = Math.round(el.getBoundingClientRect().width);
setClipWidth(w);
};
const ro = new ResizeObserver(() => {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(calc);
});
calc();
ro.observe(el);
return () => {
cancelAnimationFrame(raf);
ro.disconnect();
};
}, []);
return { wrapRef, clipRef, perPage, clipWidth };
}
/* ---------------- component ---------------- */
export default function CaseTabsCarousel() {
const [active, setActive] = useState<TabKey>("rain");
// 탭별 마지막 그룹 인덱스 기억
const tabIndexRef = useRef<Record<TabKey, number>>({
rain: 0,
reuse: 0,
sewage: 0,
global: 0,
});
const reducedMotion = usePrefersReducedMotion();
const { wrapRef, clipRef, perPage, clipWidth } = useCarouselMeasure();
const items = useMemo(() => MOCK[active] ?? [], [active]);
const groups = useMemo(() => chunk(items, perPage), [items, perPage]);
const groupCount = groups.length;
const [cur, setCur] = useState(0);
const curRef = useRef(0);
// px 기반 트랙
const [trackPx, setTrackPx] = useState(0);
const [anim, setAnim] = useState(false);
const [busy, setBusy] = useState(false);
// 방향 ref (onTransitionEnd에서 trackPx 비교하지 않음)
const dirRef = useRef<-1 | 1 | 0>(0);
// cur 최신값 유지
useEffect(() => {
curRef.current = cur;
}, [cur]);
/**
* 리셋 로직 분리:
* - 탭/perPage/groupCount 바뀔 때만 "인덱스/상태" 리셋
* - clipWidth 변화는 별도 effect에서 "busy 아닐 때만" 중앙(-W)로 배치
*/
useLayoutEffect(() => {
const saved = tabIndexRef.current[active] ?? 0;
const nextCur = groupCount ? mod(saved, groupCount) : 0;
dirRef.current = 0;
setCur(nextCur);
setBusy(false);
setAnim(false);
}, [active, perPage, groupCount]);
// clipWidth 변화로 트랙 위치를 잡되, 슬라이드 중(busy)엔 덮어쓰지 않음
useLayoutEffect(() => {
if (busy) return;
if (clipWidth > 0) setTrackPx(-clipWidth);
else setTrackPx(0);
}, [clipWidth, busy]);
// 3그룹 가상화
const viewGroups = useMemo(() => {
if (!groupCount) return [[], [], []] as CaseItem[][];
return [
groups[mod(cur - 1, groupCount)] ?? [],
groups[mod(cur, groupCount)] ?? [],
groups[mod(cur + 1, groupCount)] ?? [],
];
}, [groups, cur, groupCount]);
const canSlide = groupCount > 1 && clipWidth > 0;
const go = useCallback(
(dir: -1 | 1) => {
if (!canSlide) return;
if (busy) return;
dirRef.current = dir;
setBusy(true);
setAnim(!reducedMotion);
// 중앙(-W) 기준: prev=0, next=-2W
setTrackPx(dir === 1 ? -2 * clipWidth : 0);
// reduced motion이면 즉시 확정
if (reducedMotion) {
const nextCur = mod(curRef.current + dir, groupCount);
tabIndexRef.current[active] = nextCur;
setCur(nextCur);
setAnim(false);
setTrackPx(-clipWidth);
setBusy(false);
dirRef.current = 0;
}
},
[active, busy, canSlide, clipWidth, groupCount, reducedMotion]
);
// BUGFIX 1) 자식 transitionend 버블링 방지 + transform만 처리
const onTransitionEnd = useCallback(
(e: React.TransitionEvent<HTMLDivElement>) => {
// 자식 transitionend 버블링 방지
if (e.target !== e.currentTarget) return;
if (e.propertyName !== "transform") return;
if (!busy || reducedMotion) return;
const dir = dirRef.current;
if (dir !== 1 && dir !== -1) return;
const nextCur = mod(curRef.current + dir, groupCount);
tabIndexRef.current[active] = nextCur;
// 1) 먼저 transition을 끄고(중요)
setAnim(false);
// 2) cur를 회전 + 트랙을 즉시 중앙(-W)로 스냅
// (이 순간은 "무조건 애니메이션 없이" 처리되어야 함)
setCur(nextCur);
setTrackPx(-clipWidth);
// 3) 다음 프레임(2번 rAF)에서 다시 애니메이션 켜고 busy 해제
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setAnim(!reducedMotion);
setBusy(false);
dirRef.current = 0;
});
});
},
[active, busy, clipWidth, groupCount, reducedMotion]
);
const goDot = useCallback(
(idx: number) => {
if (!groupCount) return;
if (busy) return;
const target = mod(idx, groupCount);
tabIndexRef.current[active] = target;
dirRef.current = 0;
setCur(target);
setAnim(false);
if (clipWidth > 0) setTrackPx(-clipWidth);
},
[active, busy, clipWidth, groupCount]
);
const trackStyle: React.CSSProperties = useMemo(
() => ({
transform: `translate3d(${trackPx}px, 0, 0)`,
transition: anim ? "transform 700ms cubic-bezier(.22,.61,.36,1)" : "none",
willChange: busy ? "transform" : undefined,
}),
[trackPx, anim, busy]
);
return (
<section className="w-full">
{/* 탭 바 */}
<div className="mx-auto max-w-[1520px] pt-10">
<div className="">
<div className="grid grid-cols-4">
{TABS.map((t) => {
const isActive = t.key === active;
return (
<button
key={t.key}
type="button"
onClick={() => setActive(t.key)}
className={[
"relative h-[74px] w-full text-base font-semibold",
"transition-colors",
// 기본(비활성): 검정 배경 + 회색 텍스트
!isActive ? "text-[#767676] hover:text-black border-b-2 border-sky-400" : "",
// 활성: 흰 배경 + 진한 텍스트 + 하늘색 테두리(위/좌/우) + 아래는 열어둠
isActive
? [
"bg-[#eaf4f4] text-black",
"border-t-2 border-l-2 border-r-2 border-sky-400",
"border-b-0", // 아래는 컨텐츠와 이어지도록 열어둠
"z-10", // 테두리가 위로 올라오게
].join(" ")
: "",
].join(" ")}
>
{t.label}
{/* 활성 탭 하단에 얇은 라인이 필요하면(이미지처럼) */}
{isActive && (
<span className="pointer-events-none absolute left-0 right-0 bottom-[-1px] h-[2px] bg-[#eaf4f4]" />
)}
</button>
);
})}
</div>
</div>
</div>
{/* 슬라이더 */}
<div ref={wrapRef} className="mx-auto max-w-[1520px] px-6 pb-5 pt-10 bg-[#eaf4f4]">
<div className="relative">
{/* 좌우 버튼 */}
<button
type="button"
onClick={() => go(-1)}
disabled={!canSlide || busy}
className={[
"absolute left-0 top-1/2 z-10 -translate-y-1/2",
"w-[80px] h-[80px] rounded-full bg-white shadow-[0_10px_25px_rgba(0,0,0,0.12)]",
"grid place-items-center border-2 border-[#C2CAD2]",
!canSlide || busy
? "opacity-40 cursor-not-allowed"
: "hover:brightness-95",
].join(" ")}
aria-label="이전"
>
<img
src="/images/landing/icon_left_arrow.png"
alt="이전"
className="w-6 h-6 object-contain"
/>
</button>
<button
type="button"
onClick={() => go(1)}
disabled={!canSlide || busy}
className={[
"absolute right-0 top-1/2 z-10 -translate-y-1/2",
"w-[80px] h-[80px] rounded-full bg-white shadow-[0_10px_25px_rgba(0,0,0,0.12)]",
"grid place-items-center border-2 border-[#C2CAD2]",
!canSlide || busy
? "opacity-40 cursor-not-allowed"
: "hover:brightness-95",
].join(" ")}
aria-label="다음"
>
<img
src="/images/landing/icon_right_arrow.png"
alt="다음"
className="w-6 h-6 object-contain"
/>
</button>
{/* 중요: clip(가림)과 measure(측정) 분리 */}
<div className="overflow-hidden [clip-path:inset(0)] [contain:paint]">
<div className="w-[1250px] overflow-hidden m-auto">
{/* 측정은 패딩 없는 박스에서 */}
<div ref={clipRef} className="w-full">
{/* track */}
<div className="flex" style={trackStyle} onTransitionEnd={onTransitionEnd}>
{viewGroups.map((group, gi) => (
<div
key={`pane-${gi}`}
className="shrink-0"
style={{ width: clipWidth || "100%" }}
>
{/* 그룹 내부: perPage 컬럼 */}
<div
className="grid gap-7"
style={{
gridTemplateColumns: `repeat(${perPage}, minmax(0, 1fr))`,
}}
>
{(group ?? []).map((c) => (
<article
key={c.id}
className="overflow-hidden rounded-[22px] border-2 border-sky-400 bg-white"
>
<div className="h-[241px] w-full bg-slate-100">
<img
src={c.imageSrc}
alt={c.title}
className="h-full w-full object-cover"
loading="lazy"
/>
</div>
<div className="p-8">
<h3 className="text-[28px] font-extrabold leading-snug tracking-tight">
{c.title}
</h3>
<p className="mt-4 line-clamp-3 text-[16px] leading-7 text-slate-600">
{c.desc}
</p>
<a
href={c.href ?? "#"}
className="mt-8 inline-flex h-[56px] w-full items-center justify-center gap-3 rounded-md bg-[#02A0D6] text-[18px] font-bold text-white transition hover:brightness-110"
>
자세히 보기 <span aria-hidden>→</span>
</a>
</div>
</article>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* 도트 */}
<div className="mt-8 flex items-center justify-center gap-3">
{Array.from({ length: Math.max(groupCount, 1) }).map((_, i) => (
<button
key={`dot-${i}`}
type="button"
onClick={() => goDot(i)}
className={[
"h-[10px] w-[10px] rounded-full transition",
i === cur
? "bg-black opacity-100"
: "bg-black opacity-30 hover:opacity-60",
].join(" ")}
aria-label={`${i + 1}번 그룹`}
/>
))}
</div>
</div>
</div>
</section>
);
}
@@ -0,0 +1,167 @@
// apps/dm/app/(landing)/reuse/_components/ImageSlider.tsx
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
type Slide = { src: string; alt?: string };
type Props = {
slides: Slide[];
className?: string;
maxWidthClassName?: string; // 예: "max-w-[760px]"
};
export default function CaseImageSlider({
slides,
className = "",
maxWidthClassName = "max-w-[760px]",
}: Props) {
const safeSlides = useMemo(() => slides.filter(Boolean), [slides]);
const [idx, setIdx] = useState(0);
const [dragging, setDragging] = useState(false);
const startX = useRef(0);
const deltaX = useRef(0);
const total = safeSlides.length;
const canSlide = total > 1;
const go = (dir: number) => {
if (!canSlide) return;
setIdx((prev) => {
const next = prev + dir;
if (next < 0) return total - 1;
if (next >= total) return 0;
return next;
});
};
// ← → 키보드 지원(선택)
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 onPointerDown = (e: React.PointerEvent) => {
if (!canSlide) return;
setDragging(true);
startX.current = e.clientX;
deltaX.current = 0;
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
};
const onPointerMove = (e: React.PointerEvent) => {
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);
deltaX.current = 0;
};
if (!total) 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]"
>
<img
src="/images/landing/icon_left_arrow.png"
alt="이전"
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]"
>
<img
src="/images/landing/icon_right_arrow.png"
alt="다음"
className="w-[12px] h-[22px]"
/>
</button>
{/* 뷰포트 */}
<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}
>
<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">
<img
src={s.src}
alt={s.alt ?? `slide-${i + 1}`}
className="w-full h-full object-contain"
draggable={false}
/>
</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>
)}
</div>
</section>
);
}
+91 -44
View File
@@ -1,22 +1,33 @@
// apps/dm/app/(landing)/reuse/_components/Lnb.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useMemo, useState } from "react";
export type LnbItem = {
label: string;
href?: string; // ✅ href 없는 부모(2뎁스 컨테이너)도 허용
href?: string; // href 없는 부모(2뎁스 컨테이너)도 허용
children?: LnbItem[];
};
function isExactActive(pathname: string, href?: string) {
if (!href) return false;
return pathname === href; // ✅ exact match만 허용 (중요!)
return pathname === href;
}
export default function Lnb({ title, items }: { title: string; items: LnbItem[] }) {
const pathname = usePathname();
// ✅ 현재 경로가 속한 2depth 그룹을 기본 open
const defaultOpenLabel = useMemo(() => {
const found = items.find((it) => it.children?.some((c) => isExactActive(pathname, c.href)));
return found?.label ?? null;
}, [items, pathname]);
// ✅ 아코디언: 한 번에 하나만 열리도록 openLabel 1개만 관리
const [openLabel, setOpenLabel] = useState<string | null>(defaultOpenLabel);
return (
<aside className="w-[260px]">
{/* 타이틀 */}
@@ -33,62 +44,98 @@ export default function Lnb({ title, items }: { title: string; items: LnbItem[]
{items.map((item) => {
const hasChildren = !!item.children?.length;
// ✅ 1뎁스는 exact match 기준 active
// 1뎁스 active
const active1 = isExactActive(pathname, item.href);
// ✅ 2뎁스가 있으면(혹시라도) 자식 exact match도 포함
const active2 =
item.children?.some((c) => isExactActive(pathname, c.href)) ?? false;
// 2뎁스 active(자식 중 exact)
const active2 = item.children?.some((c) => isExactActive(pathname, c.href)) ?? false;
const active = active1 || active2;
// ✅ href 없는 부모(2뎁스 컨테이너)
if (!item.href && hasChildren) {
// ✅ children 있는 경우: 아코디언 헤더 + 패널
if (hasChildren && !item.href) {
const isOpen = openLabel === item.label;
return (
<li key={item.label}>
{/* 아코디언 헤더 */}
<button
type="button"
onClick={() => setOpenLabel((prev) => (prev === item.label ? null : item.label))}
className={[
"w-full px-5 py-6",
"flex items-center justify-between",
"border-b border-slate-300",
"text-center text-base font-semibold",
active ? "bg-slate-100 text-slate-900" : "bg-slate-100 text-slate-500",
].join(" ")}
aria-expanded={isOpen}
>
<span className="w-full text-center">{item.label}</span>
{/* ✅ 우측 화살표(이미지처럼) */}
<span
aria-hidden
className={[
"ml-3 shrink-0 transition-transform duration-200",
isOpen ? "rotate-180" : "rotate-0",
].join(" ")}
>
<img
src="/images/landing/drop_arrow.svg"
alt="드롭다운 화살표 아이콘"
className="w-7 h-7"
/>
</span>
</button>
{/* 패널: height auto 애니메이션(그리드 트릭) */}
<div
className={[
"block px-5 py-6 text-center text-base font-semibold",
"border-b border-slate-300",
active ? "bg-white text-sky-500" : "bg-slate-100 text-slate-500",
"grid transition-[grid-template-rows] duration-200 ease-out",
isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
].join(" ")}
>
{item.label}
<div className="overflow-hidden bg-white">
<ul>
{item.children!.map((child) => {
const childActive = isExactActive(pathname, child.href);
return (
<li key={child.href ?? child.label}>
{child.href ? (
<Link
href={child.href}
className={[
"block px-5 py-4 pl-[60px] text-[18px] font-medium",
"border-b border-slate-200 transition-colors",
childActive
? "bg-white text-sky-500 cursor-default"
: "bg-slate-50 text-slate-500 hover:bg-white hover:text-sky-500",
].join(" ")}
>
{/* 이미지처럼 불릿 필요하면 */}
<span className="mr-2 text-current">
•
</span>
{child.label}
</Link>
) : (
<div className="block px-5 py-4 text-[18px] font-medium border-b border-slate-200 bg-slate-50 text-slate-500">
{child.label}
</div>
)}
</li>
);
})}
</ul>
</div>
</div>
<ul>
{item.children!.map((child) => {
const childActive = isExactActive(pathname, child.href);
return (
<li key={child.href ?? child.label}>
{child.href ? (
<Link
href={child.href}
className={[
"block px-5 py-4 text-center text-[18px] font-medium",
"border-b border-slate-200 transition-colors",
childActive
? "bg-white text-sky-500 cursor-default" // ✅ active 고정
: "bg-slate-50 text-slate-500 hover:bg-white hover:text-sky-500", // ✅ hover=active
].join(" ")}
>
{child.label}
</Link>
) : (
<div className="block px-5 py-4 text-center text-[18px] font-medium border-b border-slate-200 bg-slate-50 text-slate-500">
{child.label}
</div>
)}
</li>
);
})}
</ul>
</li>
);
}
// ✅ 일반 1뎁스 메뉴
// ✅ 일반 1뎁스 메뉴(기존 유지)
return (
<li key={item.href ?? item.label}>
{item.href ? (
@@ -98,8 +145,8 @@ export default function Lnb({ title, items }: { title: string; items: LnbItem[]
"block px-5 py-4 text-center text-base font-semibold",
"border-b border-slate-300 transition-colors",
active
? "bg-white text-sky-500 cursor-default" // ✅ active 고정 (hover 영향 없음)
: "bg-slate-100 text-slate-500 hover:bg-white hover:text-sky-500", // ✅ hover=active
? "bg-white text-sky-500 cursor-default"
: "bg-[#F0F0F0] text-[#767676] hover:bg-white hover:text-sky-500",
].join(" ")}
>
{item.label}
@@ -10,6 +10,7 @@ type Props = {
note?: string;
underlineWidthClass?: string;
className?: string;
wrapClassName?: string;
hideFooterLine?: boolean;
hideGuideText?: boolean;
};
@@ -20,12 +21,13 @@ export default function SubPageHeader({
note = "* 는 필수 값입니다.",
underlineWidthClass = "w-40",
className = "",
wrapClassName = "",
hideFooterLine = false,
hideGuideText = false,
}: Props) {
return (
<section className={`text-black ${className}`}>
<div className="pb-5">
<div className={`pb-5 ${wrapClassName}`}>
<div className="flex items-start justify-between gap-6">
{/* 좌측 타이틀 */}
<div className="inline-block">
@@ -38,7 +40,7 @@ export default function SubPageHeader({
{crumbs.map((c, i) => (
<span key={`${c.label}-${i}`}>
{c.href ? (
<Link href={c.href} className="hover:text-white">
<Link href={c.href} className="hover:text-sky-500">
{c.label}
</Link>
) : (
@@ -6,9 +6,9 @@ import { useEffect, useId, useState } from "react";
import { useRouter } from "next/navigation";
const NAV_ITEMS = [
{ href: "/reuse/intro", label: "재이용소개" },
{ href: "/reuse/cases", label: "재이용사례" },
{ href: "/reuse/business", label: "물재이용사업" },
{ href: "/reuse/intro/law", label: "물 재이용 소개" },
{ href: "/reuse/cases/rain", label: "물 재이용 현황" },
{ href: "/reuse/business/rain/suwon", label: "물 재이용 사례" },
{ href: "/reuse/apply/place", label: "수요신청" },
];
+28 -9
View File
@@ -7,27 +7,46 @@ export const LNB_DEMAND: LnbItem[] = [
{ href: "/reuse/apply/status", label: "수요신청현황" },
];
// (예시) 재이용사례처럼 2depth가 있는 메뉴
export const LNB_CASES: LnbItem[] = [
export const LNB_INTRO = [
{ label: "물 재이용 법", href: "/reuse/intro/law" },
{ label: "빗물 이용시설", href: "/reuse/intro/rain" },
{ label: "중수도", href: "/reuse/intro/greywater" },
{ label: "하수처리수", href: "/reuse/intro/treated" },
];
export const LNB_CASES = [
{ label: "빗물이용시설(현황)", href: "/reuse/cases/rain" },
{ label: "중수도(현황)", href: "/reuse/cases/greywater" },
{ label: "하수처리수 재이용시설(현황)", href: "/reuse/cases/treated" },
];
// 물 재이용 사례처럼 2depth가 있는 메뉴
export const LNB_BUSINESS: LnbItem[] = [
{
label: "빗물 재이용 사례",
children: [
{ label: "경기도 파주시", href: "/reuse/cases/rain/paju" },
{ label: "경기도 수원시", href: "/reuse/cases/rain/suwon" },
{ label: "충청남도 보령시", href: "/reuse/cases/rain/boryeong" },
{ label: "충청북도 청주시", href: "/reuse/cases/rain/cheongju" },
{ label: "경기도 수원시", href: "/reuse/business/rain/suwon" },
// { label: "충청남도 보령시", href: "/reuse/business/rain/bolyeong" },
// { label: "충청북도 청주시", href: "/reuse/business/rain/cheongju" },
],
},
{
label: "중수 재이용 사례",
children: [{ label: "사례 목록", href: "/reuse/cases/graywater" }],
children: [{ label: "사례 목록", href: "/reuse/business/graywater" }],
},
{
label: "하수 재이용 사례",
children: [{ label: "사례 목록", href: "/reuse/cases/sewage" }],
children: [
{ label: "파주시", href: "/reuse/business/sewage/paju" },
{ label: "충청남도 보령시", href: "/reuse/business/sewage/bolyeong" },
{ label: "여수", href: "/reuse/business/sewage/yeosu" },
{ label: "청주", href: "/reuse/business/sewage/cheongju" },
{ label: "수원시 황구지천", href: "/reuse/business/sewage/hwanggu" },
{ label: "구리시 도로자동청소시스템", href: "/reuse/business/sewage/guri" },
],
},
{
label: "해외 재이용 사례",
children: [{ label: "사례 목록", href: "/reuse/cases/global" }],
children: [{ label: "사례 목록", href: "/reuse/business/global" }],
},
];
+51 -63
View File
@@ -1,75 +1,63 @@
// apps/dm/app/(landing)/reuse/_sections/Cases.tsx
"use client";
import { useMemo, useState } from "react";
type Tab = {
key: string;
label: string;
img: string;
};
const tabs: readonly [Tab, ...Tab[]] = [
{ key: "a", label: "경기권역 반도체 사업장 물 재이용", img: "/landing/reuse/case_diagram_01.png" },
{ key: "b", label: "경기도 파주시 하수재이용", img: "/landing/reuse/case_diagram_01.png" },
{ key: "c", label: "충남 보령 하수재이용", img: "/landing/reuse/case_diagram_01.png" },
{ key: "d", label: "수원시 빗물이용과 중수도", img: "/landing/reuse/case_diagram_01.png" },
];
export default function Cases() {
const [active, setActive] = useState<string>(() => tabs[0]?.key ?? "");
const current = tabs.find((t) => t.key === active) ?? tabs[0];
if (!current) {
return null; // 또는 로딩/빈상태 UI
}
return (
<section id="cases" className="bg-white">
<div className="mx-auto max-w-[1520px] px-6 py-[90px] h-[800px]">
<div className="text-center">
<h2 className="text-2xl font-bold">재이용사례</h2>
<div className="mx-auto mt-2 h-[4px] w-[90px] bg-sky-600" />
</div>
<section id="cases" className="bg-[#EAF6F6]">
<div className="mx-auto max-w-[1520px] px-6 h-[870px] flex items-center">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 w-full">
{/* 좌측 : 카피 영역 */}
<div className="flex flex-col justify-center">
<div className="pl-2 lg:pl-10">
<p className="text-[32px] font-extrabold text-slate-900 tracking-[-0.02em]">
빗물+중수도+하수도
</p>
<div className="mt-10 border-b">
<div className="flex flex-wrap gap-2">
{tabs.map((t) => {
const on = t.key === active;
return (
<button
key={t.key}
onClick={() => setActive(t.key)}
className={[
"px-4 py-2 text-xs font-semibold rounded-t-lg border",
on ? "bg-white border-b-white text-sky-700" : "bg-gray-50 text-gray-700 hover:text-sky-700",
].join(" ")}
>
{t.label}
</button>
);
})}
<h2 className="mt-6 leading-[1.1] tracking-[-0.03em]">
<span className="block text-[64px] font-extrabold text-slate-900">
데이터로 보는
</span>
<span className="mt-3 block text-[64px] font-extrabold text-[#004A94]">
물 재이용 현황
</span>
</h2>
<div className="mt-14">
<a
href="#"
className="
inline-flex items-center justify-center gap-3
w-[260px] h-[52px]
bg-[#01ACE5] text-white
rounded-sm
hover:bg-[#0A78C0]
transition-colors
"
>
물 재이용 현황 보러가기
<span className="text-lg">→</span>
</a>
</div>
</div>
</div>
</div>
<div className="mt-6 grid grid-cols-1 lg:grid-cols-[360px_1fr] gap-6 items-start">
<aside className="rounded-2xl border bg-gray-50 p-5">
<h3 className="font-bold text-sm">사업개요</h3>
<ul className="mt-3 text-xs text-gray-600 leading-6 list-disc pl-4">
<li>대상/규모/공급구조 등 요약</li>
<li>공정/기간/운영 방식</li>
<li>정량 효과(예: m³/일, 비용)</li>
</ul>
<h3 className="mt-6 font-bold text-sm">사업 추진 및 목적</h3>
<p className="mt-2 text-xs text-gray-600 leading-6">
탭별 설명 문구를 여기에 넣습니다.
</p>
</aside>
<div className="rounded-2xl border bg-white p-4">
<img src={current.img} alt="" className="w-full h-auto" />
{/* 우측 : 이미지 카드 */}
<div className="flex justify-center lg:justify-end">
<div
className="
relative
w-full max-w-[720px]
"
>
<img
src="/images/landing/section_03.png"
alt="물 재이용 현황 데이터 테이블"
className="w-full h-auto object-cover"
/>
</div>
</div>
</div>
</div>
</section>
@@ -9,7 +9,7 @@ export default function Hero() {
<div className="absolute inset-0 z-10 flex items-center justify-center">
<div className="flex flex-col items-center justify-items-center gap-[40px] mx-auto max-w-[1520px] px-6 text-center text-white">
<h1 className="text-5xl font-bold tracking-tight">물재이용</h1>
<h1 className="text-5xl font-bold tracking-tight">물 재이용</h1>
<p className="text-xl font-medium leading-6 text-white/85 max-w-[720px] mx-auto">
빗물, 오수, 하수처리수, 폐수처리수, 발전소 온배수를 재이용처리하여<br />
생활, 공업, 농업, 조경, 하천유지 등의 용도로 재이용합니다.
@@ -20,7 +20,7 @@ export default function Hero() {
</p>
</div>
</div>
<div className="absolute inset-x-0 bottom-20 flex flex-col items-center justify-items-center gap-2 mt-10">
<div className="absolute inset-x-0 bottom-[120px] flex flex-col items-center justify-items-center gap-2 mt-10">
<img src="/images/landing/scroll_down.svg" alt="" className="w-[26px] h-[40px]" />
<span className="text-white/80 text-base tracking-[.25em]">scroll down</span>
</div>
@@ -1,49 +1,85 @@
// apps/dm/app/(landing)/reuse/_sections/IntroCards.tsx
const cards = [
{ title: "친환경 수자원 확보", img: "/landing/reuse/card_01.jpg" },
{ title: "알뜰하게 재이용하는 중수", img: "/landing/reuse/card_02.jpg" },
{ title: "하수처리수 재이용", img: "/landing/reuse/card_03.jpg" },
{ title: "2030년 물 재이용 목표", img: "/landing/reuse/card_04.jpg" },
"use client";
import Link from "next/link";
type IntroCardItem = {
title: string;
desc: string;
image: string;
href: string;
};
const INTRO_CARDS: IntroCardItem[] = [
{
title: "물 재이용 법",
desc: "물의 재이용 촉진 및 지원에 관한 법률을 시행하고, 일부 법률은 개정되었습니다.",
image: "/images/landing/card_01.jpg",
href: "/reuse/intro/law",
},
{
title: "친환경 수자원 빗물",
desc: "가뭄을 극복하고 홍수를 예방하기 위해 비가 내릴 때 빗물을 모아두었다가 사용합니다.",
image: "/images/landing/card_02.jpg",
href: "/reuse/intro/rain",
},
{
title: "알뜰하게 재이용되는 중수",
desc: "한번 사용한 물을 중수도 시설을 통해 재생하여 개별적 또는 지역적으로 재이용할 수 있습니다.",
image: "/images/landing/card_03.jpg",
href: "/reuse/intro/greywater",
},
{
title: "하수처리수 재이용",
desc: "사용하고 버려진 공공하수처리장으로 모여진 물을 용도에 맞는 처리 후 재이용 할 수 있습니다.",
image: "/images/landing/card_04.jpg",
href: "/reuse/intro/treated",
},
];
export default function IntroCards() {
return (
<section id="intro" className="bg-white">
<div className="mx-auto max-w-[1520px] px-6 py-[90px] h-[800px]">
<div className="mx-auto max-w-[1520px] px-6 py-[90px] h-[870px]">
<div className="text-center">
<h2 className="text-2xl font-bold">재이용소개</h2>
<div className="mx-auto mt-2 h-[4px] w-[90px] bg-sky-600" />
<p className="mt-3 text-sm text-gray-500">
<div className="inline-block">
<h2 className="text-[40px] font-extrabold tracking-tight text-slate-900 leading-none">물 재이용 소개</h2>
<div className="h-2 bg-gradient-to-r from-sky-300 to-green-300"></div>
</div>
<p className="mt-5 text-xl text-gray-500">
지속 가능한 미래를 위한 약속, 깨끗한 물의 새로운 순환을 시작합니다.
</p>
</div>
<div className="mt-10 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{cards.map((c) => (
<article
key={c.title}
className="group relative overflow-hidden rounded-2xl shadow-sm border bg-gray-50 min-h-[220px]"
>
<img
src={c.img}
alt=""
className="absolute inset-0 h-full w-full object-cover opacity-90 group-hover:scale-[1.03] transition"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
<div className="relative p-5 h-full flex flex-col justify-end">
<h3 className="text-white font-semibold text-sm">{c.title}</h3>
<section className="mx-auto max-w-8xl px-4 py-12">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{INTRO_CARDS.map((card, idx) => (
<Link
key={idx}
href={card.href}
aria-label={`${card.title} 상세로 이동`}
className="group relative h-[280px] md:h-[320px] lg:h-[500px] overflow-hidden rounded-2xl cursor-pointer focus:outline-none focus:ring-2 focus:ring-sky-400/60"
>
{/* 배경 이미지 (이미 그라디언트 포함) */}
<img
src={card.image}
alt={card.title}
className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 hover:scale-105"
/>
{/* 예: 첫 카드에만 다운로드 버튼 */}
{c.title === "친환경 수자원 확보" && (
<button className="mt-3 inline-flex items-center justify-center rounded-lg bg-orange-500 px-4 py-2 text-xs font-semibold text-white w-fit">
리플렛 다운로드
</button>
)}
</div>
</article>
))}
</div>
{/* 텍스트 영역 */}
<div className="absolute bottom-0 z-10 p-6 text-white">
<h3 className="text-lg font-semibold tracking-tight">
{card.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-white/90 line-clamp-3">
{card.desc}
</p>
</div>
</Link>
))}
</div>
</section>
</div>
</section>
);
@@ -1,20 +1,21 @@
// apps/dm/app/(landing)/reuse/_sections/ProcessFlow.tsx
import CaseTabsCarousel from "../_components/CaseTabsCarousel";
export default function ProcessFlow() {
return (
<section id="processflow" className="bg-[#f7fbff]">
<div className="mx-auto max-w-[1520px] px-6 py-[90px]">
<section id="processflow" className="bg-white">
<div className="mx-auto max-w-[1520px] px-6 py-[50px] h-[870px]">
<div className="text-center">
<h2 className="text-2xl font-bold">물재이용 사업 추진 절차</h2>
<div className="mx-auto mt-2 h-[4px] w-[120px] bg-sky-600" />
<div className="inline-block">
<h2 className="text-[40px] font-extrabold tracking-tight text-slate-900 leading-none">물 재이용 사례</h2>
<div className="h-2 bg-gradient-to-r from-sky-300 to-green-300"></div>
</div>
</div>
<div className="mt-10 rounded-2xl border bg-white p-6 h-[760px]">
<img
src="/landing/reuse/process_flow.png"
alt="물재이용 사업 추진 절차"
className="w-full h-auto"
/>
</div>
{/* 탭메뉴 영역 */}
<CaseTabsCarousel />
</div>
</section>
);
@@ -0,0 +1,16 @@
// apps/dm/app/(landing)/reuse/business/layout.tsx
import type { ReactNode } from "react";
import Lnb from "../_components/Lnb";
import { LNB_BUSINESS } from "../_constants/lnb";
export default function BusinessLayout({ children }: { children: ReactNode }) {
return (
<div className="flex gap-0">
<Lnb title="사례" items={LNB_BUSINESS} />
<main className="flex-1 px-[100px] pt-[80px]">
{children}
</main>
</div>
);
}
@@ -0,0 +1,93 @@
// apps/dm/app/(landing)/reuse/business/rain/suwon/page.tsx
"use client";
import SubPageHeader from "../../../_components/SubPageHeader";
import CaseImageSlider from "../../../_components/ImageSlider";
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/rain/suwon" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 슬라이드 이미지 영역 */}
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{ src: "/images/landing/business/rain/rain_01.png", alt: "수원시 하수 재이용 1" },
{ src: "/images/landing/business/rain/rain_02.png", alt: "수원시 하수 재이용 2" },
{ src: "/images/landing/business/rain/rain_03.png", alt: "수원시 하수 재이용 3" },
{ src: "/images/landing/business/rain/rain_04.png", alt: "수원시 하수 재이용 4" },
]}
/>
{/* 사업명 */}
<section className="mt-6">
<SectionTitle>사업명</SectionTitle>
<p className="mt-8 text-base text-gray-600">
경기도 수원시 빗물이용과 중수도
</p>
</section>
<Hr className="my-11" />
{/* 사업 방식 */}
<section className="pb-[200px]">
<SectionTitle>사업 방식</SectionTitle>
<p className="mt-8 text-base text-gray-600">
(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감<br/>
※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(‘21.7월, 수원시청).<br/>
다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란<br/><br/>
- (중수도) 수원시청, 장안구청, 월드컵경기장에 빗물 연계 중수도를 화장실·조경용수 등으로 사용하여 수도요금 절감(연 7백~30백만원)
</p>
</section>
</div>
);
}
@@ -0,0 +1,111 @@
// apps/dm/app/(landing)/reuse/business/sewage/bolyeong/page.tsx
"use client";
import SubPageHeader from "../../../_components/SubPageHeader";
import CaseImageSlider from "../../../_components/ImageSlider";
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/sewage/bolyeong" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 슬라이드 이미지 영역 */}
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{ src: "/images/landing/business/sewage/bolyeong_01.png", alt: "보령시 하수 재이용 1" },
]}
/>
{/* 사업명 */}
<section className="mt-6">
<SectionTitle>사업명</SectionTitle>
<p className="mt-8 text-base text-gray-600">
충남 보령 하수재이용<br/>
: 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진
</p>
</section>
<Hr className="my-11" />
{/* 사업 방식 */}
<section className="mt-6">
<SectionTitle>사업 방식</SectionTitle>
<p className="mt-8 text-base text-gray-600">
2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결
</p>
</section>
{/* 사업 주요 내용 */}
<section className="mt-6">
<SectionTitle>사업 주요 내용</SectionTitle>
<p className="mt-8 text-base text-gray-600">
처리공정 : 전처리 UF + 후처리 RO<br/>
시설용량 : 1만m³/일<br/>
관로 : 8.7km<br/>
사업비 : 315억원<br/>
위치: 충청남도 보령시 대청방조제로 322 일원
</p>
</section>
<Hr className="my-11" />
{/* 특이사항 */}
<section className="pb-[200px]">
<SectionTitle>특이사항</SectionTitle>
<p className="mt-8 text-base text-gray-600">
국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU(환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕으로 추진 되어 경제성보다 공익성을 바탕으로<br/>
국가전력 기반시설의 안정적 운영 도모
</p>
</section>
</div>
);
}
@@ -0,0 +1,94 @@
// apps/dm/app/(landing)/reuse/business/sewage/cheongju/page.tsx
"use client";
import SubPageHeader from "../../../_components/SubPageHeader";
import CaseImageSlider from "../../../_components/ImageSlider";
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/sewage/cheongju" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 슬라이드 이미지 영역 */}
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{ src: "/images/landing/business/sewage/cheongju_01.png", alt: "청주시 하수 재이용 1" },
]}
/>
{/* 사업명 */}
<section className="mt-6">
<SectionTitle>사업명</SectionTitle>
<p className="mt-8 text-base text-gray-600">
청주시 하수 재이용 시설
</p>
</section>
<Hr className="my-11" />
{/* 사업 주요 내용 */}
<section className="pb-[200px]">
<SectionTitle>사업 주요 내용</SectionTitle>
<p className="mt-8 text-base text-gray-600">
청주시 하수처리수 재이용 민간투자사업(BTO)<br/>
공급량 : 35,000㎥/일<br/>
공급관로 : 8.3㎞<br/>
사업비 : 622억원<br/>
수요처 : SK하이닉스 등 일반산업단지<br/>
지역 (위치) : 충청북도 청주시 흥덕구 가락리 135-9번지 일원<br/>
처리용량 : 35.000(m³/일)<br/>
적용기술 : 분리막 (UF : 전처리), (RO : 주처리)
</p>
</section>
</div>
);
}
@@ -0,0 +1,171 @@
// apps/dm/app/(landing)/reuse/business/sewage/guri/page.tsx
"use client";
import SubPageHeader from "../../../_components/SubPageHeader";
import CaseImageSlider from "../../../_components/ImageSlider";
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/sewage/hwanggu" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 슬라이드 이미지 영역 */}
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{ src: "/images/landing/business/sewage/hwanggu_01.png", alt: "수원시 황구지천 하수 재이용 1" },
]}
/>
{/* 사업명 */}
<section className="mt-6">
<SectionTitle>사업명</SectionTitle>
<p className="mt-8 text-base text-gray-600">
수원시 황수원시 황구지천 하수처리수 재이용사업
</p>
</section>
<Hr className="my-11" />
{/* 사업목적 */}
<section className="mt-6">
<SectionTitle>사업목적</SectionTitle>
<p className="mt-8 text-base text-gray-600">
황구지천 공공하수처리시설 하수처리수 재이용사업으로 향후 호매실천 및 금곡천 건천화 방지로 주민들에게 친수 공간 제공 및 하천 생태계 보전
</p>
</section>
<Hr className="my-11" />
{/* 사업 주요 내용 */}
<section className="mt-6">
<SectionTitle>사업 주요 내용</SectionTitle>
<p className="mt-8 text-base text-gray-600">
총사업비 : 110억원<br/>
위 치 : 수원시 권선구 호매실천, 금곡천<br/>
관 로 : 3.9km<br/>
공 급 량 : 25,000㎥/일(하수처리시설 용량: 45,000㎥/일)<br/>
</p>
</section>
<Hr className="my-11" />
{/* 공급계획 */}
<section className="pb-[200px]">
<SectionTitle>공급계획</SectionTitle>
{/* 데이터 테이블 */}
<div className="overflow-x-auto mt-8">
<table className="w-full border border-[#E3E7EA] text-center">
<thead>
<tr>
{/* 구분 */}
<th className="w-1/4 bg-[#E3E7EA] text-black font-semibold py-4 border border-gray-300">
구분
</th>
{/* 공급량 */}
<th className="w-2/4 bg-[#1FA3C6] text-white font-semibold py-4 border border-gray-300">
공급량
</th>
{/* 비고 */}
<th className="w-2/4 bg-[#72B84F] text-white font-semibold py-4 border border-gray-300">
비고
</th>
</tr>
</thead>
<tbody>
<tr>
{/* 좌측 병합 */}
<td
rowSpan={3}
className="bg-white font-medium text-gray-700 border border-gray-300"
>
하천 등 유지용수
</td>
<td className="bg-white py-6 border border-gray-300 text-gray-700 text-lg font-medium">
12,500m³/일
</td>
<td className="bg-white py-6 border border-gray-300 text-gray-700">
호매실천 공급
</td>
</tr>
<tr>
<td className="bg-white py-6 border border-gray-300 text-gray-700 text-lg font-medium">
6,250m³/일
</td>
<td className="bg-white py-6 border border-gray-300 text-gray-700">
금곡천1 공급
</td>
</tr>
<tr>
<td className="bg-white py-6 border border-gray-300 text-gray-700 text-lg font-medium">
6,250m³/일
</td>
<td className="bg-white py-6 border border-gray-300 text-gray-700">
금곡천2 공급
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
);
}
@@ -0,0 +1,171 @@
// apps/dm/app/(landing)/reuse/business/sewage/hwanggu/page.tsx
"use client";
import SubPageHeader from "../../../_components/SubPageHeader";
import CaseImageSlider from "../../../_components/ImageSlider";
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/sewage/hwanggu" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 슬라이드 이미지 영역 */}
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{ src: "/images/landing/business/sewage/hwanggu_01.png", alt: "수원시 황구지천 하수 재이용 1" },
]}
/>
{/* 사업명 */}
<section className="mt-6">
<SectionTitle>사업명</SectionTitle>
<p className="mt-8 text-base text-gray-600">
수원시 황수원시 황구지천 하수처리수 재이용사업
</p>
</section>
<Hr className="my-11" />
{/* 사업목적 */}
<section className="mt-6">
<SectionTitle>사업목적</SectionTitle>
<p className="mt-8 text-base text-gray-600">
황구지천 공공하수처리시설 하수처리수 재이용사업으로 향후 호매실천 및 금곡천 건천화 방지로 주민들에게 친수 공간 제공 및 하천 생태계 보전
</p>
</section>
<Hr className="my-11" />
{/* 사업 주요 내용 */}
<section className="mt-6">
<SectionTitle>사업 주요 내용</SectionTitle>
<p className="mt-8 text-base text-gray-600">
총사업비 : 110억원<br/>
위 치 : 수원시 권선구 호매실천, 금곡천<br/>
관 로 : 3.9km<br/>
공 급 량 : 25,000㎥/일(하수처리시설 용량: 45,000㎥/일)<br/>
</p>
</section>
<Hr className="my-11" />
{/* 공급계획 */}
<section className="pb-[200px]">
<SectionTitle>공급계획</SectionTitle>
{/* 데이터 테이블 */}
<div className="overflow-x-auto mt-8">
<table className="w-full border border-[#E3E7EA] text-center">
<thead>
<tr>
{/* 구분 */}
<th className="w-1/4 bg-[#E3E7EA] text-black font-semibold py-4 border border-gray-300">
구분
</th>
{/* 공급량 */}
<th className="w-2/4 bg-[#1FA3C6] text-white font-semibold py-4 border border-gray-300">
공급량
</th>
{/* 비고 */}
<th className="w-2/4 bg-[#72B84F] text-white font-semibold py-4 border border-gray-300">
비고
</th>
</tr>
</thead>
<tbody>
<tr>
{/* 좌측 병합 */}
<td
rowSpan={3}
className="bg-white font-medium text-gray-700 border border-gray-300"
>
하천 등 유지용수
</td>
<td className="bg-white py-6 border border-gray-300 text-gray-700 text-lg font-medium">
12,500m³/일
</td>
<td className="bg-white py-6 border border-gray-300 text-gray-700">
호매실천 공급
</td>
</tr>
<tr>
<td className="bg-white py-6 border border-gray-300 text-gray-700 text-lg font-medium">
6,250m³/일
</td>
<td className="bg-white py-6 border border-gray-300 text-gray-700">
금곡천1 공급
</td>
</tr>
<tr>
<td className="bg-white py-6 border border-gray-300 text-gray-700 text-lg font-medium">
6,250m³/일
</td>
<td className="bg-white py-6 border border-gray-300 text-gray-700">
금곡천2 공급
</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
);
}
@@ -0,0 +1,112 @@
// apps/dm/app/(landing)/reuse/business/sewage/paju/page.tsx
"use client";
import SubPageHeader from "../../../_components/SubPageHeader";
import CaseImageSlider from "../../../_components/ImageSlider";
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/sewage/paju" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 슬라이드 이미지 영역 */}
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{ src: "/images/landing/business/sewage/paju_01.png", alt: "파주시 하수 재이용 1" },
]}
/>
{/* 사업명 */}
<section className="mt-6">
<SectionTitle>사업명</SectionTitle>
<p className="mt-8 text-base text-gray-600">
경기도 파주시 하수재이용<br/>
: 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급<br/>
*LG디스플레이(P10공장), 희성전자, 엔비텍 등
</p>
</section>
<Hr className="my-11" />
{/* 사업 배경 및 목적 */}
<section className="mt-6">
<SectionTitle>사업 배경 및 목적</SectionTitle>
<p className="mt-8 text-base text-gray-600">
민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)
</p>
</section>
{/* 사업 주요 내용 */}
<section className="mt-6">
<SectionTitle>사업 주요 내용</SectionTitle>
<p className="mt-8 text-base text-gray-600">
처리공정 : 전처리 UF + 후처리 RO<br/>
공급량 : 4만m³/일<br/>
관로 : 13.6km<br/>
사업비 : 651억원<br/>
위치 : 경기도 파주시 금촌동 714번지 일원<br/>
시설용량 : 41,20m³/일
</p>
</section>
<Hr className="my-11" />
{/* 특이사항 */}
<section className="pb-[200px]">
<SectionTitle>특이사항</SectionTitle>
<p className="mt-8 text-base text-gray-600">
하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례
</p>
</section>
</div>
);
}
@@ -0,0 +1,101 @@
// apps/dm/app/(landing)/reuse/business/sewage/yeosu/page.tsx
"use client";
import SubPageHeader from "../../../_components/SubPageHeader";
import CaseImageSlider from "../../../_components/ImageSlider";
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="여수시 하수처리수 재이용 민간투자사업(BTO)"
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "물 재이용 사례" },
{ label: "하수 재이용 사례" },
{ label: "여수시", href: "/reuse/business/sewage/yeosu" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 슬라이드 이미지 영역 */}
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{ src: "/images/landing/business/sewage/yeosu_01.png", alt: "여수시 하수 재이용 1" },
]}
/>
{/* 사업명 */}
<section className="mt-6">
<SectionTitle>사업명</SectionTitle>
<p className="mt-8 text-base text-gray-600">
여수시 하수처리수 재이용 민간 투자사업(BTO)
</p>
</section>
<Hr className="my-11" />
{/* 사업 목적 */}
<section className="mt-6">
<SectionTitle>사업 목적</SectionTitle>
<p className="mt-8 text-base text-gray-600">
수요자가 원하는 양질의 공업용수를 저렴하게 곰급하여 한정된 수자원의 효율적 이용 및 지역적 물 부족 해소, 하수재이용을 통한 오염총량 저감 및 방류수역 수질개선
</p>
</section>
{/* 사업 주요 내용 */}
<section className="pb-[200px]">
<SectionTitle>사업 주요 내용</SectionTitle>
<p className="mt-8 text-base text-gray-600">
총사업비 : GS칼텍스, YNCC(주) 등 750억원<br/>
시행사 : 여수그린허브(주)<br/>
운영사 : 테크로스<br/>
위치: 여수시 신월로 2844일원 (여수 하수처리시설 부지 내)<br/>
처리공법 : 전처리시설(MF) + 주처리시설(RO)<br/>
시설용량 및 공급량 : 5만m³/일<br/>
관로 : 18.6km
</p>
</section>
</div>
);
}
@@ -0,0 +1,96 @@
// apps/dm/app/(landing)/reuse/cases/greywater/page.tsx
"use client";
import SubPageHeader from "../../_components/SubPageHeader";
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-[24px] 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: "물 재이용 현황", href: "/reuse/cases/rain" },
{ label: "중수도(현황)" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 1) 전체 중수도 설치 및 이용 현황 (공장 및 발전시설 제외) */}
<section className="mt-6">
<SectionTitle>전체 중수도 설치 및 이용 현황 (공장 및 발전시설 제외)</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/greywater_overall.png" alt="전체 중수도 설치 및 이용 현황 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 2) 중수공장 및 발전시설 내 중수도 설치 및 이용 현황 */}
<section>
<SectionTitle>공장 및 발전시설 내 중수도 설치 및 이용 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/greywater_factory.png" alt="공장 및 발전시설 내 중수도 설치 및 이용 현황 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 3) 중수도 건축물별 이용량 현황 */}
<section className="pb-16">
<SectionTitle>중수도 건축물별 이용량 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/greywater_building.png" alt="중수도 건축물별 이용량 현황 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 4) 중수도 용도별 이용량 현황 */}
<section className="pb-[200px]">
<SectionTitle>중수도 용도별 이용량 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/greywater_usage.png" alt="중수도 용도별 이용량 현황 설명" />
</div>
</section>
</div>
);
}
@@ -0,0 +1,16 @@
// apps/dm/app/(landing)/reuse/cases/layout.tsx
import type { ReactNode } from "react";
import Lnb from "../_components/Lnb";
import { LNB_CASES } from "../_constants/lnb";
export default function CasesLayout({ children }: { children: ReactNode }) {
return (
<div className="flex gap-0">
<Lnb title="현황" items={LNB_CASES} />
<main className="flex-1 px-[100px] pt-[80px]">
{children}
</main>
</div>
);
}
@@ -0,0 +1,86 @@
// apps/dm/app/(landing)/reuse/cases/rain/page.tsx
"use client";
import SubPageHeader from "../../_components/SubPageHeader";
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-[24px] 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: "물 재이용 현황", href: "/reuse/cases/rain" },
{ label: "빗물이용시설(현황)" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
<section className="mt-6">
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/rainwater_building.png" alt="빗물이용시설의 건축물별 빗물이용량 현황 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 2) 빗물이용시설의 용도별 분포 (2023년 기준) */}
<section>
<SectionTitle>빗물이용시설의 용도별 분포 (2023년 기준)</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/rainwater_usage.png" alt="빗물이용시설의 용도별 분포 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
<section className="pb-[200px]">
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/rainwater_usage_amount.png" alt="빗물이용시설의 용도별 빗물이용량 현황 설명" />
</div>
</section>
</div>
);
}
@@ -0,0 +1,96 @@
// apps/dm/app/(landing)/reuse/cases/treated/page.tsx
"use client";
import SubPageHeader from "../../_components/SubPageHeader";
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-[24px] 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: "물 재이용 현황", href: "/reuse/cases/rain" },
{ label: "하수처리수 재이용시설(현황)" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 1) 연도별 하수처리수 재이용 현황 */}
<section className="mt-6">
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/treated_water_year.png" alt="연도별 하수처리수 재이용 현황 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 2) 용도별 하수처리수 재이용 현황 */}
<section>
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/treated_water_usage.png" alt="용도별 하수처리수 재이용 현황 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 3) 하수처리시설 설치 및 이용 현황 */}
<section>
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/treated_water_facility.png" alt="하수처리시설 설치 및 이용 현황 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 4) 시도별 하수처리수 재이용 현황 */}
<section className="pb-[200px]">
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/cases/treated_water_region.png" alt="시도별 하수처리수 재이용 현황 설명" />
</div>
</section>
</div>
);
}
@@ -0,0 +1,86 @@
// apps/dm/app/(landing)/reuse/intro/greywater/page.tsx
"use client";
import SubPageHeader from "../../_components/SubPageHeader";
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-[24px] 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: "물 재이용 소개", href: "/reuse/intro/law" },
{ label: "중수도" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 1) 중수 재이용 소개 */}
<section className="mt-6">
<SectionTitle>중수 재이용 소개</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/greywater_intro.png" alt="중수 재이용 소개" />
</div>
</section>
<Hr className="my-11" />
{/* 2) 중수도 설치 의무 대상 */}
<section>
<SectionTitle>중수도 설치 의무 대상</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/greywater_mandatory.png" alt="중수도 설치 의무 대상 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 3) 중수도 시설기준 및 관리기준 */}
<section className="pb-[200px]">
<SectionTitle>중수도 시설기준 및 관리기준</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/greywater_standard.png" alt="중수도 시설기준 및 관리기준 설명" />
</div>
</section>
</div>
);
}
@@ -0,0 +1,122 @@
// apps/dm/app/(landing)/reuse/intro/law/page.tsx
"use client";
import SubPageHeader from "../../_components/SubPageHeader";
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-[24px] 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 WaterReuseLawPage() {
return (
<div className="bg-white">
<SubPageHeader
title="물 재이용 법"
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "물 재이용 소개", href: "/reuse/intro/law" },
{ label: "물 재이용 법" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 1) 법제도/계획 수립 타임라인 */}
<section className="mt-6">
<SectionTitle>법제도 · 계획 및 기본계획 수립</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/law_timeline.png" alt="법제도 타임라인" />
</div>
</section>
<Hr className="my-11" />
{/* 2) 제2차 계획 대비 실적 현황 (2컬럼) */}
<section>
<SectionTitle>제 2차 계획 대비 실적 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/performance_left.png" alt="실적 현황 요약" />
</div>
</section>
<Hr className="my-11" />
{/* 3) 국내 관련 현황 */}
<section>
<SectionTitle>국내 관련 현황</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/domestic_textbox.png" alt="국내 관련 현황 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 4) 정책 변천 추이 */}
<section>
<SectionTitle>물 재이용 정책 변천 추이</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/policy_timeline.png" alt="정책 변천 타임라인" />
</div>
</section>
<Hr className="my-11" />
{/* 5) 타 법령과의 관계도 */}
<section>
<SectionTitle>타법령과의 관계도</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/rel_note.png" alt="관계도 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 6) 국내 연계 현황 */}
<section className="pb-[200px]">
<SectionTitle>국내 연계 현황</SectionTitle>
<div className="mt-8">
<div className="mx-[20px] mb-8">
<p className="text-[24px] font-semibold mb-2">주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요</p>
<p className="text-[#4F4F4F]">국가 상위계획의 정책 방향이 기후위기 대응 등 수자원의 공급 안전성을 위한 물 재이용 활성화로 정책 실효성 제고 필요<br />
• 물관리 계획(국가수도기본계획, 국가 및 유역하수도종합계획, 하천유역수자원관리계획 등)과의 연계성 확보 → 통합적인 관점에서 물이용 체계 구축 필요
</p>
</div>
<ImgCard src="/images/landing/intro/linkage.png" alt="국내 연계 현황" />
</div>
</section>
</div>
);
}
@@ -0,0 +1,16 @@
// apps/dm/app/(landing)/reuse/intro/layout.tsx
import type { ReactNode } from "react";
import Lnb from "../_components/Lnb";
import { LNB_INTRO } from "../_constants/lnb";
export default function IntroLayout({ children }: { children: ReactNode }) {
return (
<div className="flex gap-0">
<Lnb title="소개" items={LNB_INTRO} />
<main className="flex-1 px-[100px] pt-[80px]">
{children}
</main>
</div>
);
}
@@ -0,0 +1,76 @@
// apps/dm/app/(landing)/reuse/intro/law/page.tsx
"use client";
import SubPageHeader from "../../_components/SubPageHeader";
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-[24px] 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 RainwaterReusePage() {
return (
<div className="bg-white">
<SubPageHeader
title="빗물 이용시설"
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "물 재이용 소개", href: "/reuse/intro/law" },
{ label: "빗물 이용시설" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 1) 빗물 재이용 소개 */}
<section className="mt-6">
<SectionTitle>빗물 재이용 소개</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/rainreuse_intro.png" alt="빗물 재이용 소개" />
</div>
</section>
<Hr className="my-11" />
{/* 2) 빗물이용시설 설치 관리 법적 의무 대상 */}
<section className="pb-[200px]">
<SectionTitle>빗물이용시설 설치 관리 법적 의무 대상</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/rainreuse_mandatory.png" alt="법적 의무 대상 설명" />
</div>
</section>
</div>
);
}
@@ -0,0 +1,76 @@
// apps/dm/app/(landing)/reuse/intro/treated/page.tsx
"use client";
import SubPageHeader from "../../_components/SubPageHeader";
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-[24px] 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 TreatedWaterPage() {
return (
<div className="bg-white">
<SubPageHeader
title="하수처리수"
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "물 재이용 소개", href: "/reuse/intro/law" },
{ label: "하수처리수" },
]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
/>
<Hr className="my-7" />
{/* 1) 하수처리수 */}
<section className="mt-6">
<SectionTitle>하수처리수</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/treatedwater_intro.png" alt="하수처리수 설명" />
</div>
</section>
<Hr className="my-11" />
{/* 2) 하수처리수 재이용시설 의무 대상 */}
<section className="pb-[200px]">
<SectionTitle>하수처리수 재이용시설 의무 대상</SectionTitle>
<div className="mt-8">
<ImgCard src="/images/landing/intro/treatedwater_mandatory.png" alt="하수처리수 재이용시설 의무 대상 설명" />
</div>
</section>
</div>
);
}
+13 -5
View File
@@ -11,13 +11,21 @@ import Cases from "./_sections/Cases";
import ProcessFlow from "./_sections/ProcessFlow";
const LANDING_SECTIONS = [
{ id: "main", label: "Main" },
{ id: "intro", label: "재이용소개" },
{ id: "cases", label: "재이용사례" },
{ id: "main", label: "Main" },
{
id: "intro",
label: "물 재이용 소개", // ✅ 상단 메뉴용
sideLabel: "소개", // ✅ 사이드 인디게이터용
},
{
id: "cases",
label: "물 재이용 현황", // ✅ 상단 메뉴용
sideLabel: "현황", // ✅ 사이드 인디게이터용
},
{
id: "processflow",
label: "물재이용사업", // ✅ 상단 메뉴용
sideLabel: "추진절차", // ✅ 사이드 인디게이터용
label: "물 재이용 사례", // ✅ 상단 메뉴용
sideLabel: "재이용사례", // ✅ 사이드 인디게이터용
},
];
Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 14L12.0008 9.42L17 14" stroke="#0A090B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB