기타 메인 포함 퍼블리싱 완료

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
LGS
2026-04-30 18:27:55 +09:00
co-authored by Copilot
parent d015432fc6
commit 262c386e2e
47 changed files with 2687 additions and 1616 deletions
+191 -165
View File
@@ -153,6 +153,10 @@ function SelectBox({
);
}
function Hr({ className = "" }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
export default function DemandPlaceRegisterPage() {
const { Common, initializeCommonData } = useDmStore((state) => state);
const { selectSidoData, selectSigunData } = Common;
@@ -401,177 +405,199 @@ export default function DemandPlaceRegisterPage() {
const pageTitle = editMode ? "수요처수정" : "수요처등록";
return (
<main className="bg-white">
<SubPageHeader
title={pageTitle}
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "수요신청", href: "/reuse/apply" },
{ label: pageTitle },
]}
/>
<div className="border border-neutral-200 bg-white">
<FormRow label="시도" required>
<SelectBox
value={sido}
onChange={(v) => {
setSido(v);
setSigungu("");
}}
options={sidoOptions}
/>
</FormRow>
<Divider />
<FormRow label="시군구" required>
<select
value={sigungu}
onChange={(e) => setSigungu(e.target.value)}
className="h-11 w-[220px] rounded border border-neutral-300 bg-white px-3 text-sm text-neutral-800 focus:border-neutral-400 focus:outline-none"
>
{sigunguOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</FormRow>
<Divider />
<FormRow label="회사명" required>
<div className="max-w-[420px]">
<TextInput
value={company}
onChange={setCompany}
placeholder="회사명을 입력해주세요."
<main className="min-w-0 flex-1">
<div className="relative">
{/* 서브 타이틀 + 브레드크럼 */}
<SubPageHeader
title={pageTitle}
crumbs={[
{ label: "Home", href: "/main" },
{ label: "수요신청", href: "/apply/dmdco" },
{ label: pageTitle },
]}
/>
</div>
</FormRow>
<Divider />
{/* 컨텐츠 영역 */}
<div className="max-w-[1320px] pb-24">
{/* 타이틀 */}
<div className="inline-block">
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
<span className="relative z-10">{pageTitle}</span>
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
</h2>
</div>
{/* 구분선 */}
<Hr className="mt-[40px]" />
<FormRow label="사업자등록번호" required>
<div className="max-w-[420px]">
<TextInput
value={formatBusinessNumber(bizNo)}
onChange={(v) => setBizNo(v.replace(/\D/g, "").slice(0, 10))}
placeholder="사업자 등록번호를 입력해주세요."
/>
</div>
</FormRow>
{/* 필수값 안내 */}
<p className="my-4 text-[16px] text-neutral-600 font-medium">
<span className="text-red-500">*</span> 는 필수 값입니다.
</p>
<Divider />
<div className="border border-neutral-200 bg-white">
<FormRow label="시도" required>
<SelectBox
value={sido}
onChange={(v) => {
setSido(v);
setSigungu("");
}}
options={sidoOptions}
/>
</FormRow>
<FormRow label="종사자등록번호">
<div className="flex flex-wrap items-center gap-4">
<div className="w-[420px] max-w-full">
<TextInput
value={workerNo}
onChange={(v) => setWorkerNo(v.replace(/\D/g, "").slice(0, 4))}
placeholder="종사자등록번호를 입력해주세요."
disabled={noWorkerNo}
/>
<Divider />
<FormRow label="시군구" required>
<select
value={sigungu}
onChange={(e) => setSigungu(e.target.value)}
className="h-11 w-[220px] rounded border border-neutral-300 bg-white px-3 text-sm text-neutral-800 focus:border-neutral-400 focus:outline-none"
>
{sigunguOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</FormRow>
<Divider />
<FormRow label="회사명" required>
<div className="max-w-[420px]">
<TextInput
value={company}
onChange={setCompany}
placeholder="회사명을 입력해주세요."
/>
</div>
</FormRow>
<Divider />
<FormRow label="사업자등록번호" required>
<div className="max-w-[420px]">
<TextInput
value={formatBusinessNumber(bizNo)}
onChange={(v) => setBizNo(v.replace(/\D/g, "").slice(0, 10))}
placeholder="사업자 등록번호를 입력해주세요."
/>
</div>
</FormRow>
<Divider />
<FormRow label="종사자등록번호">
<div className="flex flex-wrap items-center gap-4">
<div className="w-[420px] max-w-full">
<TextInput
value={workerNo}
onChange={(v) => setWorkerNo(v.replace(/\D/g, "").slice(0, 4))}
placeholder="종사자등록번호를 입력해주세요."
disabled={noWorkerNo}
/>
</div>
<label className="flex items-center gap-2 text-sm text-neutral-700">
<input
type="checkbox"
checked={noWorkerNo}
onChange={(e) => {
setNoWorkerNo(e.target.checked);
if (e.target.checked) setWorkerNo("");
}}
className="h-4 w-4 rounded border-neutral-300"
/>
해당없음
</label>
</div>
</FormRow>
<Divider />
<FormRow label="사업자주소" required>
<div className="max-w-[420px] space-y-2">
<div className="flex gap-2">
<div className="w-[180px]">
<TextInput value={zip} onChange={setZip} placeholder="우편번호" />
</div>
<button
type="button"
onClick={() => setIsPostcodeModalOpen(true)}
className="h-11 rounded bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
>
우편번호 검색
</button>
</div>
<TextInput value={addr1} onChange={setAddr1} placeholder="주소" />
<TextInput
value={addr2}
onChange={setAddr2}
placeholder="상세 주소를 입력하세요."
inputRef={detailAddrRef}
/>
</div>
</FormRow>
<Divider />
<FormRow label="전화번호" required>
<div className="max-w-[420px]">
<TextInput
value={tel}
onChange={(v) => setTel(formatKoreanPhoneNumber(v))}
placeholder="전화번호를 입력하세요."
/>
</div>
</FormRow>
<Divider />
<FormRow label="비밀번호" required={!editMode}>
<div className="max-w-[420px]">
<TextInput
value={pw}
onChange={setPw}
placeholder="비밀번호를 입력하세요."
type="password"
/>
</div>
</FormRow>
<Divider />
<FormRow label="비밀번호 확인" required={!editMode}>
<div className="max-w-[420px]">
<TextInput
value={pw2}
onChange={setPw2}
placeholder="비밀번호를 한번 더 입력하세요."
type="password"
/>
</div>
</FormRow>
</div>
<div className="flex justify-center py-12">
<button
type="button"
onClick={onSubmit}
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
>
{editMode ? "수요처 수정" : "수요처 등록"}
</button>
</div>
<PostcodeModal
open={isPostcodeModalOpen}
onClose={() => setIsPostcodeModalOpen(false)}
onSelect={handleSelectAddress}
/>
</div>
<label className="flex items-center gap-2 text-sm text-neutral-700">
<input
type="checkbox"
checked={noWorkerNo}
onChange={(e) => {
setNoWorkerNo(e.target.checked);
if (e.target.checked) setWorkerNo("");
}}
className="h-4 w-4 rounded border-neutral-300"
/>
해당없음
</label>
</div>
</FormRow>
<Divider />
<FormRow label="사업자주소" required>
<div className="max-w-[420px] space-y-2">
<div className="flex gap-2">
<div className="w-[180px]">
<TextInput value={zip} onChange={setZip} placeholder="우편번호" />
</div>
<button
type="button"
onClick={() => setIsPostcodeModalOpen(true)}
className="h-11 rounded bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
>
우편번호 검색
</button>
</div>
<TextInput value={addr1} onChange={setAddr1} placeholder="주소" />
<TextInput
value={addr2}
onChange={setAddr2}
placeholder="상세 주소를 입력하세요."
inputRef={detailAddrRef}
/>
</div>
</FormRow>
<Divider />
<FormRow label="전화번호" required>
<div className="max-w-[420px]">
<TextInput
value={tel}
onChange={(v) => setTel(formatKoreanPhoneNumber(v))}
placeholder="전화번호를 입력하세요."
/>
</div>
</FormRow>
<Divider />
<FormRow label="비밀번호" required={!editMode}>
<div className="max-w-[420px]">
<TextInput
value={pw}
onChange={setPw}
placeholder="비밀번호를 입력하세요."
type="password"
/>
</div>
</FormRow>
<Divider />
<FormRow label="비밀번호 확인" required={!editMode}>
<div className="max-w-[420px]">
<TextInput
value={pw2}
onChange={setPw2}
placeholder="비밀번호를 한번 더 입력하세요."
type="password"
/>
</div>
</FormRow>
</div>
<div className="flex justify-center py-12">
<button
type="button"
onClick={onSubmit}
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
>
{editMode ? "수요처 수정" : "수요처 등록"}
</button>
</div>
<PostcodeModal
open={isPostcodeModalOpen}
onClose={() => setIsPostcodeModalOpen(false)}
onSelect={handleSelectAddress}
/>
</div>
</main>
);
}
+8 -3
View File
@@ -3,9 +3,14 @@ import Lnb from "@/components/Lnb";
export default function ApplyLayout({ children }: { children: ReactNode }) {
return (
<div className="flex gap-0">
<div className="relative">
{/* 우측 메뉴 */}
<Lnb />
<main className="flex-1 px-[100px] pt-[80px]">{children}</main>
{/* 본문 */}
<div className="flex max-w-[1400px] gap-[56px] py-[50px]">
{children}
</div>
</div>
);
}
}
+264 -242
View File
@@ -422,261 +422,279 @@ export default function DemandRegisterPage() {
setUsePurpose(value);
}
const pageTitle = "수요정보등록";
return (
<>
<main className="bg-white">
<main className="min-w-0 flex-1">
<div className="relative">
{/* 서브 타이틀 + 브레드크럼 */}
<SubPageHeader
title={pageTitle}
crumbs={[
{ label: "Home", href: "/main" },
{ label: "수요신청", href: "/apply/dmdco" },
{ label: pageTitle },
]}
/>
{/* 서브 타이틀 + 브레드크럼 */}
<SubPageHeader
title="수요정보등록"
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "수요신청", href: "/reuse/apply" },
{ label: "수요정보등록" },
]}
/>
{/* 컨텐츠 영역 */}
<div>
{/* 기본정보 테이블 */}
<div className="border border-neutral-200 bg-white">
<FormRow label="요구시기" required>
<DateInput value={reqDate} onChange={setReqDate} />
</FormRow>
<Divider />
<FormRow label="요구수량(m²/d)" required>
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
</FormRow>
<Divider />
<FormRow label="사용용도" required>
<select
value={usePurpose}
onChange={(e) => fncSetOptionValue(e)}
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
>
<option value="">선택하세요</option>
{useTypesData.map((item) => (
<option key={item.val} value={item.val}>
{item.text}
</option>
))}
</select>
</FormRow>
<Divider />
<FormRow label="희망단가(원)" required>
<TextInput value={hopePrice} onChange={setHopePrice} />
</FormRow>
</div>
{/* 요구수질 섹션 타이틀 바 */}
<div className="grid grid-cols-12">
<div className="col-span-12 lg:col-span-2 inline-flex justify-center items-center gap-3 px-6 py-6 text-center">
<span className="text-lg font-semibold text-neutral-800">요구수질</span>
<button
type="button"
onClick={() => setOpenRef(true)}
className="rounded border-[2px] border-[#DE3412] px-2 py-3 text-[15px] font-medium text-[#4F4F4F] hover:bg-orange-50"
>
수질기준 참고
</button>
{/* 수질기준참고 모달 */}
<WaterQualityRefModal
open={openRef}
onClose={() => setOpenRef(false)}
imageSrc="/images/landing/water_quality_ref_1.png"
/>
</div>
</div>
{/* 요구수질 테이블 */}
<div className="border border-neutral-200 bg-white">
{/* 1행 */}
<QualityRow
leftLabel="총대장균군수(개/mg)"
leftControl={
<div className="flex items-center gap-4">
<TextInput
value={coliform}
onChange={setColiform}
disabled={noDetect}
className={noDetect ? "bg-neutral-100 text-neutral-400" : ""}
/>
<label className="flex items-center gap-2 text-sm text-neutral-700">
<input
type="checkbox"
checked={noDetect}
onChange={(e) => {
setNoDetect(e.target.checked);
if (e.target.checked) setColiform("");
}}
className="h-4 w-4 rounded border-neutral-300"
/>
불검출
</label>
{/* 컨텐츠 영역 */}
<div className="max-w-[1320px] pb-24">
{/* 타이틀 */}
<div className="inline-block">
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
<span className="relative z-10">{pageTitle}</span>
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
</h2>
</div>
}
rightLabel="총질소(T-N)(mg/L)"
rightControl={
<InlineSuffixInput value={tn} onChange={setTn} suffix="이하" />
}
/>
<Divider />
{/* 구분선 */}
<Hr className="mt-[40px]" />
{/* 2행 */}
<QualityRow
leftLabel="결합잔류염소(mg/L)"
/* leftRequired */
leftControl={<TextInput value={residualCl} onChange={setResidualCl} />}
rightLabel="총인(T-P)(mg/L)"
rightControl={
<InlineSuffixInput value={tp} onChange={setTp} suffix="이하" />
}
/>
<Divider />
{/* 필수값 안내 */}
<p className="my-4 text-[16px] text-neutral-600 font-medium">
<span className="text-red-500">*</span> 는 필수 값입니다.
</p>
{/* 3행 */}
<QualityRow
leftLabel="탁도(NTU)"
/* leftRequired */
leftControl={
<InlineSuffixInput value={turbidity} onChange={setTurbidity} suffix="이하" />
}
rightLabel="수소이온농도(pH)"
rightControl={
<div className="flex items-center gap-2">
<TextInput
value={ph.min}
onChange={(v) => setPh((p) => ({ ...p, min: v }))}
className="w-32"
/>
<span className="text-neutral-500">~</span>
<TextInput
value={ph.max}
onChange={(v) => setPh((p) => ({ ...p, max: v }))}
className="w-32"
/>
</div>
}
/>
<Divider />
{/* 기본정보 테이블 */}
<div className="border border-neutral-200 bg-white">
<FormRow label="요구시기" required>
<DateInput value={reqDate} onChange={setReqDate} />
</FormRow>
{/* 4행 */}
<QualityRow
leftLabel={
<>
생물화학적산소요구량
<br />
(BOD)(mg/L)
</>
}
/* leftRequired */
leftControl={<InlineSuffixInput value={bod} onChange={setBod} suffix="이하" />}
rightLabel="염화물(mgCL/L)"
rightControl={<InlineSuffixInput value={chloride} onChange={setChloride} suffix="이하" />}
/>
<Divider />
<Divider />
{/* 5행 */}
<QualityRow
leftLabel="냄새"
/* leftRequired */
leftControl={
<select
value={odor}
onChange={(e) => setOdor(e.target.value)}
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
>
<option>불쾌하지 않을 것</option>
<option>약간 있음</option>
<option>있음</option>
</select>
}
rightLabel="전기전도도(µs/cm)"
rightControl={<InlineSuffixInput value={ec} onChange={setEc} suffix={farmText} />}
/>
<Divider />
<FormRow label="요구수량(m²/d)" required>
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
</FormRow>
{/* 6행 */}
<QualityRow
leftLabel="색도(도)"
/* leftRequired */
leftControl={<InlineSuffixInput value={color} onChange={setColor} suffix="이하" />}
rightLabel="비고"
rightControl={
<textarea
value={remark}
onChange={(e) => setRemark(e.target.value)}
className="h-11 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm outline-none focus:border-neutral-500"
/>
}
/>
<Divider />
<Divider />
{/* 후보지 선택 */}
<div className="grid grid-cols-12">
<LabelCell>
공급처 후보지 선택
<br />
(필수아님)
</LabelCell>
<div className="col-span-12 md:col-span-10 p-6">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:gap-4">
<TextInput value={cand1} onChange={setCand1} className="max-w-[220px]" />
<TextInput value={cand2} onChange={setCand2} className="max-w-[220px]" />
<TextInput value={cand3} onChange={setCand3} className="max-w-[220px]" />
<div className="flex items-center gap-3 xl:ml-2">
<button
type="button"
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-700"
onClick={(e) => {
searchCandidate(e);
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
}}>
후보지 조회
</button>
<button
type="button"
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-600"
onClick={(e) => {
// TODO: 유사사례 연결
searchSimilarCase(e);
}}
<FormRow label="사용용도" required>
<select
value={usePurpose}
onChange={(e) => fncSetOptionValue(e)}
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
>
유사사례
</button>
</div>
</div>
</div>
</div>
</div>
<option value="">선택하세요</option>
{useTypesData.map((item) => (
<option key={item.val} value={item.val}>
{item.text}
</option>
))}
</select>
</FormRow>
{/* 하단 버튼 */}
<div className="flex justify-center py-12">
<button
type="button"
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
onClick={() => {
// TODO: 저장 처리
saveInfo();
}}
>
수요정보 등록
</button>
</div>
<Divider />
<FormRow label="희망단가(원)" required>
<TextInput value={hopePrice} onChange={setHopePrice} />
</FormRow>
</div>
{/* 요구수질 섹션 타이틀 바 */}
<div className="grid grid-cols-12">
<div className="col-span-12 lg:col-span-2 inline-flex justify-center items-center gap-3 px-2 py-6 text-center">
<span className="text-lg font-semibold text-neutral-800">요구수질</span>
<button
type="button"
onClick={() => setOpenRef(true)}
className="rounded border-[2px] border-[#DE3412] px-2 py-3 text-[15px] font-medium text-[#4F4F4F] hover:bg-orange-50"
>
수질기준 참고
</button>
{/* 수질기준참고 모달 */}
<WaterQualityRefModal
open={openRef}
onClose={() => setOpenRef(false)}
imageSrc="/images/landing/water_quality_ref_1.png"
/>
</div>
</div>
{/* 요구수질 테이블 */}
<div className="border border-neutral-200 bg-white">
{/* 1행 */}
<QualityRow
leftLabel="총대장균군수(개/mg)"
leftControl={
<div className="flex items-center gap-4">
<TextInput
value={coliform}
onChange={setColiform}
disabled={noDetect}
className={noDetect ? "bg-neutral-100 text-neutral-400" : ""}
/>
<label className="flex items-center gap-2 text-sm text-neutral-700">
<input
type="checkbox"
checked={noDetect}
onChange={(e) => {
setNoDetect(e.target.checked);
if (e.target.checked) setColiform("");
}}
className="h-4 w-4 rounded border-neutral-300"
/>
불검출
</label>
</div>
}
rightLabel="총질소(T-N)(mg/L)"
rightControl={
<InlineSuffixInput value={tn} onChange={setTn} suffix="이하" />
}
/>
<Divider />
{/* 2행 */}
<QualityRow
leftLabel="결합잔류염소(mg/L)"
/* leftRequired */
leftControl={<TextInput value={residualCl} onChange={setResidualCl} />}
rightLabel="총인(T-P)(mg/L)"
rightControl={
<InlineSuffixInput value={tp} onChange={setTp} suffix="이하" />
}
/>
<Divider />
{/* 3행 */}
<QualityRow
leftLabel="탁도(NTU)"
/* leftRequired */
leftControl={
<InlineSuffixInput value={turbidity} onChange={setTurbidity} suffix="이하" />
}
rightLabel="수소이온농도(pH)"
rightControl={
<div className="flex items-center gap-2">
<TextInput
value={ph.min}
onChange={(v) => setPh((p) => ({ ...p, min: v }))}
className="w-32"
/>
<span className="text-neutral-500">~</span>
<TextInput
value={ph.max}
onChange={(v) => setPh((p) => ({ ...p, max: v }))}
className="w-32"
/>
</div>
}
/>
<Divider />
{/* 4행 */}
<QualityRow
leftLabel={
<>
생물화학적산소요구량
<br />
(BOD)(mg/L)
</>
}
/* leftRequired */
leftControl={<InlineSuffixInput value={bod} onChange={setBod} suffix="이하" />}
rightLabel="염화물(mgCL/L)"
rightControl={<InlineSuffixInput value={chloride} onChange={setChloride} suffix="이하" />}
/>
<Divider />
{/* 5행 */}
<QualityRow
leftLabel="냄새"
/* leftRequired */
leftControl={
<select
value={odor}
onChange={(e) => setOdor(e.target.value)}
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
>
<option>불쾌하지 않을 것</option>
<option>약간 있음</option>
<option>있음</option>
</select>
}
rightLabel="전기전도도(µs/cm)"
rightControl={<InlineSuffixInput value={ec} onChange={setEc} suffix={farmText} />}
/>
<Divider />
{/* 6행 */}
<QualityRow
leftLabel="색도(도)"
/* leftRequired */
leftControl={<InlineSuffixInput value={color} onChange={setColor} suffix="이하" />}
rightLabel="비고"
rightControl={
<textarea
value={remark}
onChange={(e) => setRemark(e.target.value)}
className="h-11 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm outline-none focus:border-neutral-500"
/>
}
/>
<Divider />
{/* 후보지 선택 */}
<div className="grid grid-cols-12">
<LabelCell>
공급처 후보지 선택
<br />
(필수아님)
</LabelCell>
<div className="col-span-12 md:col-span-10 p-6">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:gap-4">
<TextInput value={cand1} onChange={setCand1} className="max-w-[220px]" />
<TextInput value={cand2} onChange={setCand2} className="max-w-[220px]" />
<TextInput value={cand3} onChange={setCand3} className="max-w-[220px]" />
<div className="flex items-center gap-3 xl:ml-2">
<button
type="button"
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-700"
onClick={(e) => {
searchCandidate(e);
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
}}>
후보지 조회
</button>
<button
type="button"
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-600"
onClick={(e) => {
// TODO: 유사사례 연결
searchSimilarCase(e);
}}
>
유사사례
</button>
</div>
</div>
</div>
</div>
</div>
{/* 하단 버튼 */}
<div className="flex justify-center py-12">
<button
type="button"
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
onClick={() => {
// TODO: 저장 처리
saveInfo();
}}
>
수요정보 등록
</button>
</div>
</div>
</div>
</main>
@@ -836,6 +854,10 @@ function InlineSuffixInput({
);
}
function Hr({ className = "" }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
function DateInput({
value,
onChange,
+387 -375
View File
@@ -326,402 +326,414 @@ export default function DemandStatusPage() {
};
}, []);
const pageTitle = "수요신청현황";
return (
<main className="bg-white">
<SubPageHeader
title="수요신청현황"
crumbs={[
{ label: "Home", href: "/main" },
{ label: "수요신청", href: "/apply/status" },
{ label: "수요신청현황" },
]}
hideFooterLine
hideGuideText
/>
<main className="min-w-0 flex-1">
<div className="relative">
{/* 서브 타이틀 + 브레드크럼 */}
<SubPageHeader
title={pageTitle}
crumbs={[
{ label: "Home", href: "/main" },
{ label: "수요신청", href: "/apply/dmdco" },
{ label: pageTitle },
]}
/>
<div>
<section className="mt-6">
<div className="text-2xl font-bold text-slate-700">수요정보 신청현황</div>
{/* 컨텐츠 영역 */}
<div className="max-w-[1320px] pb-24">
{/* 타이틀 */}
<div className="inline-block">
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
<span className="relative z-10">{pageTitle}</span>
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
</h2>
</div>
<section className="mt-6">
<div className="text-2xl font-bold text-slate-700 text-center w-full">수요정보 신청현황</div>
<div className="mt-3 border border-neutral-200">
<table className="w-full table-fixed">
<thead className="bg-[#E7F4E2]">
<tr className="text-sm text-slate-700">
<th className="w-[60px] px-3 py-3 text-center">NO.</th>
<th className="w-[120px] px-3 py-3 text-center">시도</th>
<th className="w-[120px] px-3 py-3 text-center">시군구</th>
<th className="w-[180px] px-3 py-3 text-center">회사명</th>
<th className="px-3 py-3 text-center">공장주소</th>
<th className="w-[140px] px-3 py-3 text-center">전화번호</th>
<th className="w-[120px] px-3 py-3 text-center">요구시기</th>
<th className="w-[120px] px-3 py-3 text-center">등록일자</th>
<th className="w-[120px] px-3 py-3 text-center">수정일자</th>
<th className="w-[110px] px-3 py-3 text-center">진행상태</th>
</tr>
</thead>
<div className="mt-3 border border-neutral-200">
<table className="w-full table-fixed">
<thead className="bg-[#E7F4E2]">
<tr className="text-sm text-slate-700">
<th className="w-[60px] px-3 py-3 text-center">NO.</th>
<th className="w-[120px] px-3 py-3 text-center">시도</th>
<th className="w-[120px] px-3 py-3 text-center">시군구</th>
<th className="w-[180px] px-3 py-3 text-center">회사명</th>
<th className="px-3 py-3 text-center">공장주소</th>
<th className="w-[140px] px-3 py-3 text-center">전화번호</th>
<th className="w-[120px] px-3 py-3 text-center">요구시기</th>
<th className="w-[120px] px-3 py-3 text-center">등록일자</th>
<th className="w-[120px] px-3 py-3 text-center">수정일자</th>
<th className="w-[110px] px-3 py-3 text-center">진행상태</th>
</tr>
</thead>
<tbody className="bg-white text-base text-slate-700">
{transformedSdmInfoStatusList.length === 0 ? (
<tr>
<td colSpan={8} className="px-3 py-10 text-center text-slate-400">
조회된 신청내역이 없습니다.
</td>
</tr>
) : (
transformedSdmInfoStatusList.map((sdmInfoStatus, index) => {
const active = sdmInfoStatus.dmdInfoSn === selectedSdmInfoStatus?.dmdInfoSn;
return (
<tr
key={`${sdmInfoStatus.dmdInfoSn}-${index}`}
role="row"
tabIndex={0}
onClick={() => onSelectSdmInfoStatus(sdmInfoStatus.dmdInfoSn)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onSelectSdmInfoStatus(sdmInfoStatus.dmdInfoSn);
}
}}
className={[
"border-t border-neutral-200 outline-none",
"cursor-pointer",
active ? "bg-slate-50" : "hover:bg-slate-50",
"focus-visible:ring-2 focus-visible:ring-slate-300",
].join(" ")}
aria-selected={active}
>
<td className="px-3 py-3 text-center">{sdmInfoStatus.index || index + 1}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.ctpvCd)}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.sggCd)}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.dmdCoNm)}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.daddr)}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.telno)}</td>
<td className="px-3 py-3 text-center">{formatDate(sdmInfoStatus.dmndTmng)}</td>
<td className="px-3 py-3 text-center">{formatDate(sdmInfoStatus.regDt)}</td>
<td className="px-3 py-3 text-center">{formatDate(sdmInfoStatus.mdfcnDt)}</td>
<td className="px-3 py-3 text-center">
<StatusBadge v={formatValue(sdmInfoStatus.status)} />
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</section>
<section className="mt-10 pb-16" ref={detailRef}>
<div className="flex justify-between items-center">
<div className="text-2xl font-bold text-slate-700">수요신청 상세현황</div>
{
!isEditMode ? (
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
onClick={() => {
setOriginData(selectedSdmInfoStatus);
setEditData({...selectedSdmInfoStatus});
setIsEditMode(true)}
}
>
수정
</button>
) : (
<div className="flex gap-2">
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
onClick={() => {
saveInfo();
}}
>
저장
</button>
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
onClick={() => {
setEditData(originData);
setIsEditMode(false)
}}
>
취소
</button>
</div>
)
}
</div>
{!selectedSdmInfoStatus ? (
<div className="mt-3 border border-neutral-200 px-6 py-10 text-center text-sm text-slate-400">
상세 정보를 표시할 신청건이 없습니다.
</div>
) : (
<div className="mt-3 border border-neutral-200">
<div className="bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
요구수량
</div>
<div className="grid grid-cols-12 text-sm">
{topFields.map((field) => (
<div
key={field.label}
className="col-span-12 md:col-span-6 grid grid-cols-12 border-b border-neutral-200"
>
<div className="col-span-4 bg-sky-50 px-4 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
{field.label}
{field.required ? <span className="ml-1 text-red-500">*</span> : null}
<tbody className="bg-white text-base text-slate-700">
{transformedSdmInfoStatusList.length === 0 ? (
<tr>
<td colSpan={8} className="px-3 py-10 text-center text-slate-400">
조회된 신청내역이 없습니다.
</td>
</tr>
) : (
transformedSdmInfoStatusList.map((sdmInfoStatus, index) => {
const active = sdmInfoStatus.dmdInfoSn === selectedSdmInfoStatus?.dmdInfoSn;
return (
<tr
key={`${sdmInfoStatus.dmdInfoSn}-${index}`}
role="row"
tabIndex={0}
onClick={() => onSelectSdmInfoStatus(sdmInfoStatus.dmdInfoSn)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onSelectSdmInfoStatus(sdmInfoStatus.dmdInfoSn);
}
}}
className={[
"border-t border-neutral-200 outline-none",
"cursor-pointer",
active ? "bg-slate-50" : "hover:bg-slate-50",
"focus-visible:ring-2 focus-visible:ring-slate-300",
].join(" ")}
aria-selected={active}
>
<td className="px-3 py-3 text-center">{sdmInfoStatus.index || index + 1}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.ctpvCd)}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.sggCd)}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.dmdCoNm)}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.daddr)}</td>
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.telno)}</td>
<td className="px-3 py-3 text-center">{formatDate(sdmInfoStatus.dmndTmng)}</td>
<td className="px-3 py-3 text-center">{formatDate(sdmInfoStatus.regDt)}</td>
<td className="px-3 py-3 text-center">{formatDate(sdmInfoStatus.mdfcnDt)}</td>
<td className="px-3 py-3 text-center">
<StatusBadge v={formatValue(sdmInfoStatus.status)} />
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</section>
<section className="mt-10 pb-16" ref={detailRef}>
<div className="flex justify-between items-center">
<div className="text-2xl font-bold text-slate-700">수요신청 상세현황</div>
{
!isEditMode ?
<div className="col-span-8 px-4 py-4 flex items-center justify-center text-center">
{field.key==='useUsgCd'? useTypesData.filter(item=>item.val===field.value)[0]?.text: field.value}
</div>
: <div className="col-span-8 px-4 py-4 flex items-center justify-center text-center">
{field.key ==='dmndTmng'?
<DateInput value={editData?.[field.key] ?? ""} onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: v,
}))
} />
: field.key ==='useUsgCd'?
<select
value={editData?.[field.key] ?? ""}
onChange={(e) =>
setEditData((prev) => ({
...prev,
[field.key]: e.target.value,
}))
!isEditMode ? (
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
onClick={() => {
setOriginData(selectedSdmInfoStatus);
setEditData({...selectedSdmInfoStatus});
setIsEditMode(true)}
}
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
>
<option value="">선택하세요</option>
{useTypesData.map((item) => (
<option key={item.val} value={item.val}>
{item.text}
</option>
))}
</select>
: <TextInput value={editData?.[field.key] ?? ""} onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: v,
}))
}></TextInput>
}
>
수정
</button>
) : (
<div className="flex gap-2">
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
onClick={() => {
saveInfo();
}}
>
저장
</button>
<button
type="button"
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
onClick={() => {
setEditData(originData);
setIsEditMode(false)
}}
>
취소
</button>
</div>
)
}
</div>
))}
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
요구수질
</div>
{waterFields.map((field) => {
const { a, b } = editData?.ph ? getRange(editData.ph) : { a: "", b: "" };
return (
<div
key={field.label}
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b border-neutral-200"
>
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
{field.label}
{!selectedSdmInfoStatus ? (
<div className="mt-3 border border-neutral-200 px-6 py-10 text-center text-sm text-slate-400">
상세 정보를 표시할 신청건이 없습니다.
</div>
) : (
<div className="mt-3 border border-neutral-200">
<div className="bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
요구수량
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
{
!isEditMode ? field.value
:
field.key === "grssClfg" ? (
<div className="flex items-center gap-4">
<TextInput
value={
editData?.grssClfg === -1
? ""
: editData?.grssClfg ?? ""
}
onChange={(v) =>
setEditData((prev) => ({
...prev,
grssClfg: v,
}))
}
disabled={editData?.grssClfg === -1}
className={
editData?.grssClfg === -1
? "bg-neutral-100 text-neutral-400"
: ""
}
/>
<label className="flex items-center gap-2 w-3/4 text-sm text-neutral-700">
<input
type="checkbox"
checked={editData?.grssClfg === -1}
onChange={(e) => {
const checked = e.target.checked;
setEditData((prev) => ({
...prev,
grssClfg: checked ? -1 : "",
}));
}}
className="h-4 w-4 rounded border-neutral-300"
/>
불검출
</label>
<div className="grid grid-cols-12 text-sm">
{topFields.map((field) => (
<div
key={field.label}
className="col-span-12 md:col-span-6 grid grid-cols-12 border-b border-neutral-200"
>
<div className="col-span-4 bg-sky-50 px-4 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
{field.label}
{field.required ? <span className="ml-1 text-red-500">*</span> : null}
</div>
) : field.key === 'smll' ? (
<select
value={
editData?.smll === -1
? ""
: editData?.smll ?? ""
}
onChange={(v) =>
setEditData((prev) => ({
...prev,
grssClfg: v,
}))
}
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500">
<option>불쾌하지 않을 것</option>
<option>약간 있음</option>
<option>있음</option>
</select>
) : field.key === 'ph' ? (
<>
<TextInput
value={a}
onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: `${v}~${b}`,
}))
}
/>
~
<TextInput
value={b}
onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: `${a}~${v}`,
}))
}
/>
</>
) : (
<>
<TextInput
value={editData?.[field.key] ?? ""}
onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: v,
}))
}
/>
{
field.key==='tn'||field.key === 'tph'||field.key === 'trbt'||field.key==='blgcOxygDmndQnt'||field.key==='clIonQnt'||field.key==='cdtnv'|| field.key=='crmty'
?<>이하</>:<></>
!isEditMode ?
<div className="col-span-8 px-4 py-4 flex items-center justify-center text-center">
{field.key==='useUsgCd'? useTypesData.filter(item=>item.val===field.value)[0]?.text: field.value}
</div>
: <div className="col-span-8 px-4 py-4 flex items-center justify-center text-center">
{field.key ==='dmndTmng'?
<DateInput value={editData?.[field.key] ?? ""} onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: v,
}))
} />
: field.key ==='useUsgCd'?
<select
value={editData?.[field.key] ?? ""}
onChange={(e) =>
setEditData((prev) => ({
...prev,
[field.key]: e.target.value,
}))
}
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
>
<option value="">선택하세요</option>
{useTypesData.map((item) => (
<option key={item.val} value={item.val}>
{item.text}
</option>
))}
</select>
: <TextInput value={editData?.[field.key] ?? ""} onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: v,
}))
}></TextInput>
}
</div>
}
</>
)
</div>
))}
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
요구수질
</div>
{waterFields.map((field) => {
const { a, b } = editData?.ph ? getRange(editData.ph) : { a: "", b: "" };
return (
<div
key={field.label}
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b border-neutral-200"
>
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
{field.label}
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
{
!isEditMode ? field.value
:
field.key === "grssClfg" ? (
<div className="flex items-center gap-4">
<TextInput
value={
editData?.grssClfg === -1
? ""
: editData?.grssClfg ?? ""
}
onChange={(v) =>
setEditData((prev) => ({
...prev,
grssClfg: v,
}))
}
disabled={editData?.grssClfg === -1}
className={
editData?.grssClfg === -1
? "bg-neutral-100 text-neutral-400"
: ""
}
/>
<label className="flex items-center gap-2 w-3/4 text-sm text-neutral-700">
<input
type="checkbox"
checked={editData?.grssClfg === -1}
onChange={(e) => {
const checked = e.target.checked;
setEditData((prev) => ({
...prev,
grssClfg: checked ? -1 : "",
}));
}}
className="h-4 w-4 rounded border-neutral-300"
/>
불검출
</label>
</div>
) : field.key === 'smll' ? (
<select
value={
editData?.smll === -1
? ""
: editData?.smll ?? ""
}
onChange={(v) =>
setEditData((prev) => ({
...prev,
grssClfg: v,
}))
}
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500">
<option>불쾌하지 않을 것</option>
<option>약간 있음</option>
<option>있음</option>
</select>
) : field.key === 'ph' ? (
<>
<TextInput
value={a}
onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: `${v}~${b}`,
}))
}
/>
~
<TextInput
value={b}
onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: `${a}~${v}`,
}))
}
/>
</>
) : (
<>
<TextInput
value={editData?.[field.key] ?? ""}
onChange={(v) =>
setEditData((prev) => ({
...prev,
[field.key]: v,
}))
}
/>
{
field.key==='tn'||field.key === 'tph'||field.key === 'trbt'||field.key==='blgcOxygDmndQnt'||field.key==='clIonQnt'||field.key==='cdtnv'|| field.key=='crmty'
?<>이하</>:<></>
}
</>
)
}
</div>
</div>
)})}
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
공급처 후보지
{
isEditMode &&
<button
type="button"
className="h-8 w-25 ml-12 rounded-md bg-[#888] px-2 text-base text-white hover:bg-neutral-700"
onClick={(e) => {
searchCandidate(e);
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
}}>
후보지 조회
</button>
}
</div>
{candidateNames.map((candidateName, index) => (
isEditMode ? (
<div
key={`candidate-${index}`}
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200"
>
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
후보지{index + 1}
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
<TextInput
value={candidateName}
disabled={true}
onChange={(v) => {
setEditData((prev) => {
const newList = [...(prev.splyCndtList || [])];
newList[index] = {
...newList[index],
ruseUtztnFcltNm: v,
};
return {
...prev,
splyCndtList: newList,
};
});
}}
/>
</div>
</div>
) :
<div
key={`candidate-${index}`}
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200"
>
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
후보지{index + 1}
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
{candidateName}
</div>
</div>
))}
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-t border-b border-neutral-200">
수요신청 진행상태
</div>
<div className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200">
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
진행상태
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
{formatValue(selectedSdmInfoStatus.status)}
</div>
</div>
<div className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200">
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
공급처매칭결과
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
{formatValue(selectedSdmInfoStatus.ruseUtztnFcltNm)}
</div>
</div>
</div>
</div>
)})}
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
공급처 후보지
{
isEditMode &&
<button
type="button"
className="h-8 w-25 ml-12 rounded-md bg-[#888] px-2 text-base text-white hover:bg-neutral-700"
onClick={(e) => {
searchCandidate(e);
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
}}>
후보지 조회
</button>
}
</div>
{candidateNames.map((candidateName, index) => (
isEditMode ? (
<div
key={`candidate-${index}`}
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200"
>
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
후보지{index + 1}
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
<TextInput
value={candidateName}
disabled={true}
onChange={(v) => {
setEditData((prev) => {
const newList = [...(prev.splyCndtList || [])];
newList[index] = {
...newList[index],
ruseUtztnFcltNm: v,
};
return {
...prev,
splyCndtList: newList,
};
});
}}
/>
</div>
</div>
) :
<div
key={`candidate-${index}`}
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200"
>
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
후보지{index + 1}
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
{candidateName}
</div>
</div>
))}
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-t border-b border-neutral-200">
수요신청 진행상태
</div>
<div className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200">
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
진행상태
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
{formatValue(selectedSdmInfoStatus.status)}
</div>
</div>
<div className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200">
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
공급처매칭결과
</div>
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
{formatValue(selectedSdmInfoStatus.ruseUtztnFcltNm)}
</div>
</div>
</div>
)}
</section>
</div>
)}
</section>
</div>
</div>
</main>
);
}
@@ -2,6 +2,50 @@
import CaseImageSlider from "@/components/CaseImageSlider";
type Slide = {
src: string;
alt: string;
};
type ContentBlock = {
title: string;
items: TextItem[];
};
type InfoCard = {
title: string;
desc: string;
};
type TableData = {
headers: string[];
rows: string[][];
};
type OverviewCompareRow = {
country: string;
flag?: string;
overseas: string;
current: string;
future: string;
};
type GlobalCase =
| {
type: "overview";
id: string;
title: string;
}
| {
type: "default";
id: string;
title: string;
slides: Slide[];
blocks?: ContentBlock[];
cards?: InfoCard[];
table?: TableData;
};
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
@@ -11,105 +55,720 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
);
}
// 구분선
function BlockTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-2">
<span className="relative inline-block h-[16px] w-[27px] align-middle">
<span className="absolute left-0 top-0 h-[16px] w-[16px] rounded-full bg-[#01ACE5]/40" />
<span className="absolute left-[7px] top-0 h-[16px] w-[16px] rounded-full bg-[#6DBB4F]/40" />
</span>
<h4 className="text-[20px] font-semibold text-[#1A1A1A]">{children}</h4>
</div>
);
}
function Hr({ className = "" }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
type TextItem =
| string
| {
text: React.ReactNode;
depth?: number; // 0: 기본 / 1: 하위 / 2: 더 하위
};
function ContentList({ items }: { items: TextItem[] }) {
return (
<ul className="mt-4 space-y-2 text-[16px] leading-[1.8] text-[#4F4F4F]">
{items.map((item, index) => {
const isString = typeof item === "string";
const text = isString ? item : item.text;
const depth = isString ? 0 : item.depth ?? 0;
if (depth === 3) {
return (
<li key={index} className="ml-[56px] list-none text-[#777]">
* {text}
</li>
);
}
if (depth === 2) {
return (
<li key={index} className="ml-[52px] list-none text-[#666]">
{text}
</li>
);
}
if (depth === 1) {
return (
<li key={index} className="ml-[34px] list-none text-[#777]">
- {text}
</li>
);
}
return (
<li key={index} className="ml-6 list-disc pl-1 font-semibold text-[#555]">
{text}
</li>
);
})}
</ul>
);
}
function ContentBlocks({
blocks,
cards,
table,
}: {
blocks?: ContentBlock[];
cards?: InfoCard[];
table?: TableData;
}) {
if (!blocks?.length) return null;
return (
<div className="mt-10 space-y-9">
{blocks.map((block, index) => (
<div key={`${block.title}-${index}`}>
<BlockTitle>{block.title}</BlockTitle>
{!!block.items.length && (
<ContentList items={block.items} />
)}
{block.title.includes("재생 수자원 개발법") && (
<InfoCards cards={cards} />
)}
{block.title.includes("대만 재이용 시설별 재이용수 공급단지") && (
<DataTable table={table} />
)}
</div>
))}
</div>
);
}
function InfoCards({ cards }: { cards?: InfoCard[] }) {
if (!cards?.length) return null;
return (
<div className="mt-8 grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-4">
{cards.map((card) => (
<div
key={card.title}
className="rounded-[8px] border-2 border-[#01ACE5] bg-[#EAF5F5] p-5"
>
<strong className="block text-[20px] font-bold text-[#333]">
{card.title}
</strong>
<p className="mt-3 text-[15px] leading-[1.6] text-[#555]">
{card.desc}
</p>
</div>
))}
</div>
);
}
function DataTable({ table }: { table?: TableData }) {
if (!table) return null;
return (
<div className="mt-8 overflow-x-auto">
<table className="w-full min-w-[860px] border border-[#D8DDE3] text-center">
<thead>
<tr>
{table.headers.map((header, index) => (
<th
key={header}
className={[
"border border-[#D8DDE3] px-4 py-3 text-[18px] font-medium",
index === 0
? "bg-[#BFC9D2] text-[#222]"
: index === 1
? "bg-[#14A9D9] text-white"
: "bg-[#70BF4B] text-white",
].join(" ")}
>
{header}
</th>
))}
</tr>
</thead>
<tbody>
{table.rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td
key={`${rowIndex}-${cellIndex}`}
className={[
"border border-[#D8DDE3] px-4 py-3 text-[16px] text-[#4F4F4F]",
cellIndex === 1
? "bg-[#EAF7F8]"
: cellIndex === 2
? "bg-[#EAF8E6]"
: "bg-white",
].join(" ")}
>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
/* =========================
사례개요 전용 컴포넌트
========================= */
const compareRows: OverviewCompareRow[] = [
{
country: "한국",
flag: "/images/landing/business/globalCase/flag_kr.png",
overseas: "12.1%",
current: "15.6%",
future: "17%",
},
{
country: "대만",
flag: "/images/landing/business/globalCase/flag_tw.png",
overseas: "2.4%",
current: "4.3%",
future: "16.5%",
},
];
const reuseRateRows = [
["이스라엘", "중동&북아프리카", "92.3", "튀르키예", "동유럽&중앙아시아", "26.6"],
["쿠웨이트", "중동&북아프리카", "89.3", "독일", "서유럽", "25.0"],
["호주", "동아시아&태평양", "49.0", "스페인", "서유럽", "24.4"],
["싱가포르", "동아시아&태평양", "44.4", "한국", "동아시아&태평양", "12.1"],
["그리스", "서유럽", "39.1", "미국", "북아메리카", "13.1"],
["사우디아라비아", "중동&북아프리카", "32.8", "프랑스", "서유럽", "9.5"],
["이탈리아", "서유럽", "31.7", "대만", "동아시아&태평양", "2.4"],
["룩셈부르크", "서유럽", "29.1", "일본", "동아시아&태평양", "0.8"],
["포르투갈", "서유럽", "29.0", "", "", ""],
];
function WaterBullet() {
return (
<img
src="/images/landing/business/globalCase/icon_bullet_water.png"
alt="bullet"
className="h-[33px] w-[27px] mt-1"
/>
);
}
function OverviewNoticeBox() {
return (
<div className="mx-auto mt-5 max-w-[1040px] rounded-[20px] bg-[#EAF7F8] px-8 py-7">
<div className="flex items-start gap-2">
<WaterBullet />
<div>
<strong className="block text-[26px] font-extrabold text-[#007FA4]">
우리나라 하수처리수 재이용률은 15% 수준
</strong>
<ul className="mt-5 list-disc space-y-2 pl-5 text-[16px] leading-[1.7] text-[#555]">
<li>
2024년 하수도 통계 기준 <b>15.6%</b>이며, 제2차 물 재이용 기본 계획을 고려한{" "}
<b style={{ color: "#1A1A1A" }}>2030년 목표는 17%</b>
</li>
<li>
대만은 가뭄과 공업용수 증가에 대응하여 <b style={{ color: "#1A1A1A" }}>현재 4.3% 수준이며, 16.5%까지 확대 계획 수립</b>
</li>
</ul>
</div>
</div>
</div>
);
}
function OverviewCompareTable() {
return (
<div className="mx-auto mt-5 max-w-[1040px] overflow-x-auto">
<table className="w-full min-w-[760px] text-center">
<thead>
<tr>
<th className="bg-[#BFC9D2] px-4 py-4 text-[18px] font-medium text-[#222] rounded-tl-[10px] rounded-tr-[10px]">
국가
</th>
<th className="bg-[#14A9D9] px-4 py-4 text-[18px] font-medium text-white rounded-tl-[10px] rounded-tr-[10px]">
해외정보
</th>
<th className="bg-[#55BDA5] px-4 py-4 text-[18px] font-medium text-white rounded-tl-[10px] rounded-tr-[10px]">
현재
</th>
<th className="bg-[#6AB84E] px-4 py-4 text-[18px] font-medium text-white rounded-tl-[10px] rounded-tr-[10px]">
미래(목표, 예측치)
</th>
</tr>
</thead>
<tbody>
{compareRows.map((row) => (
<tr key={row.country}>
<td className="border border-[#AEB9C2] bg-white px-4 py-4 text-[20px] font-bold text-[#333]">
<div className="flex items-center justify-center gap-3">
{row.flag && (
<img
src={row.flag}
alt={`${row.country} 국기`}
className="h-[28px] w-[42px] object-cover"
/>
)}
{row.country}
</div>
</td>
<td className="border border-[#AEB9C2] bg-[#EAF7F8] px-4 py-4 text-[28px] font-bold text-[#4F4F4F]">
{row.overseas}
</td>
<td className="border border-[#AEB9C2] bg-white px-4 py-4 text-[28px] font-bold text-[#4F4F4F]">
{row.current}
</td>
<td className="border border-[#AEB9C2] bg-[#EAF8E6] px-4 py-4 text-[28px] font-bold text-[#4F4F4F]">
{row.future}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function OverviewDescriptionBox() {
return (
<div className="mx-auto mt-5 max-w-[1040px] rounded-[20px] bg-[#EAF7F8] px-7 py-6">
<ul className="list-disc space-y-2 pl-5 text-[16px] leading-[1.7] text-[#555]">
<li>
전 세계적으로 물 재이용은 호주, 유럽, 아프리카, 미국 등 가뭄이 잦거나 심각한 가뭄이 발생한 국가에서 많이 이루어짐
</li>
<li>
이스라엘, 쿠웨이트 등 중동 및 북아프리카 지역은 가용 수자원이 제한되어 있어 재이용량 비중도 매우 높은 편
</li>
</ul>
</div>
);
}
function OverviewReuseRateTable() {
return (
<div className="mx-auto mt-5 max-w-[1040px] overflow-x-auto">
<table className="w-full min-w-[960px] text-center">
<thead>
<tr>
{["국가명", "지역", "재이용률", "국가명", "지역", "재이용률"].map((header, index) => (
<th
key={`${header}-${index}`}
className={[
"px-4 py-3 text-[17px] font-bold rounded-tl-[10px] rounded-tr-[10px]",
index % 3 === 0
? "bg-[#BFC9D2] text-[#222]"
: index % 3 === 1
? "bg-[#14A9D9] text-white"
: "bg-[#6AB84E] text-white",
].join(" ")}
>
{header}
</th>
))}
</tr>
</thead>
<tbody>
{reuseRateRows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td
key={`${rowIndex}-${cellIndex}`}
className={[
"border border-[#AEB9C2] px-4 py-3 text-[15px] text-[#4F4F4F]",
cellIndex % 3 === 1
? "bg-[#EAF7F8]"
: cellIndex % 3 === 2
? "bg-[#EAF8E6]"
: "bg-white font-semibold",
].join(" ")}
>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
function OverviewSection() {
return (
<div className="mt-10">
<div>
<BlockTitle>주요 국가 물 재이용 계획</BlockTitle>
<OverviewNoticeBox />
<OverviewCompareTable />
</div>
<div className="mt-8">
<BlockTitle>주요 국가의 하수처리수 재이용률 (%)</BlockTitle>
<OverviewDescriptionBox />
<OverviewReuseRateTable />
</div>
</div>
);
}
/* =========================
데이터
========================= */
const globalCases: GlobalCase[] = [
{
type: "overview",
id: "example-global-overview",
title: "사례 개요",
},
{
type: "default",
id: "example-global-taiwan",
title: "[대만] 국가 주도의 공업용수 공급",
slides: [
{
src: "/images/landing/business/globalCase/taiwan_01.png",
alt: "대만 국가 주도의 공업용수 공급 1",
},
{
src: "/images/landing/business/globalCase/taiwan_02.png",
alt: "대만 국가 주도의 공업용수 공급 2",
},
{
src: "/images/landing/business/globalCase/taiwan_03.png",
alt: "대만 국가 주도의 공업용수 공급 3",
},
],
blocks: [
{
title: "재생 수자원 개발법 제정 (2015년제정, 2022개정)",
items: [
{ text: "목적: 수자원 지속 가능성 향상, 재생 수자원 활용을 통해 물부족 문제 해결" },
{ text: "내용: 재생 수자원 정의·분류, 이용 의무, 이용 제한, 수질 기준 등" }
],
},
{
title: "대만정부 하수재이용 사업 시행",
items: [
{ text: "대만은 물 재이용에 적극적이며 국가주도로 하수재이용 사업 시행(안핑)" },
{ text: "1,518억의 사업비 중 92% 국비, 지방비 8% 투입", depth: 1 },
{ text: "재이용수는 24km 관로로 타이난 TSMC로 공급", depth: 1 },
{ text: "안핑을 포함한 16건 공사 완료시 628,100 CMD 공급 가능" },
{ text: "공급단가는 수질과 운영조건에 따라 다르게 책정" },
],
},
{
title: "대만 재이용 시설별 재이용수 공급단지",
items: [],
},
{
title: "특이사항",
items: [
"대만은 강수량이 풍부하지만 지역별 편차로 수자원 확보에 어려움을 겪고 있으며, 특히 남부 지역은 우기와 건기의 강수 차이가 큼",
"최근 수년간 심각한 가뭄이 발생하면서 반도체 산업을 국가 핵심 산업으로 지정, 공업용수 수요가 크게 증가",
"주요 반도체 공장이 남부에 집중되어 있어 물 부족 문제가 지속적으로 발생, 2021년 가뭄 당시 산업단지 용수 공급에도 차질 발생",
"정부는 2023년부터 산업용수 추가 요금을 부과하고, 농업용수 규제를 강화하는 등 용수 관리 정책 시행",
"기후변화 대응을 위해 수자원 보호 및 재이용 정책을 지속적으로 확대",
"산업용수 다소비 산업과 하수처리수를 재이용수 주요 공급원으로 활용 계획",
],
},
],
cards: [
{
title: "제4조.",
desc: "물 소비 계획에 따른 재이용수 이용 관련 내용 (일정 비율 재이용수 사용 규정)",
},
{
title: "제5조.",
desc: "재이용수의 운영자, 사용자 관련 내용 (지자체에서 하수 무상공급 제공 가능)",
},
{
title: "제7조.",
desc: "재이용수 이용 범위, 이용량 재이용수 수질 기준 내용",
},
{
title: "제8조.",
desc: "재이용수 사업 주체, 책임 등에 관한 내용 (광역인 경우, 정부가 지자체 지정)",
},
],
table: {
headers: ["사업", "가격(NTD/㎥)", "가격(원/㎥)"],
rows: [
["Fongshan(가오슝)", "23.39", "1,038원"],
["Linhai(가오슝)", "19.28", "858원"],
["Anping(타이난)", "38.28", "1,699원"],
["Yongkang(타이난)", "23.67", "1,050원"],
["Rende(타이난)", "28.26", "1,254원"],
["Shuinan(타이중)", "15.00", "668원"],
["Quaitou(가오슝)", "35.44", "1,573원"],
],
},
},
{
type: "default",
id: "example-global-singapore",
title: "[싱가포르] 수요가 높은 산업의 물 재이용 의무화",
slides: [
{
src: "/images/landing/business/globalCase/singapore_01.png",
alt: "싱가포르 NEWater 사례 1",
},
{
src: "/images/landing/business/globalCase/singapore_02.png",
alt: "싱가포르 NEWater 사례 2",
},
{
src: "/images/landing/business/globalCase/singapore_03.png",
alt: "싱가포르 NEWater 사례 3",
},
],
blocks: [
{
title: "싱가포르 NEWater 비중 확대",
items: [
"2003년 공식 NEWater 공장을 가동했으며 신뢰확보를 위해 NEWater Visitor Center를 함께 건설",
"NEWater 전체파이프 공급 → 반도체 공장에서 가장 많이 사용",
"2060년까지 NEWater 비중 55%까지 높이는 것이 싱가포르의 목표",
],
},
{
title: "싱가포르 재이용수 의무화 및 지원정책",
items: [
{ text: "공업용수 수요가 높은 산업에 대해 물 재이용을 의무화" },
{ text: "반도체업계 전체 비 재이용수(non-domestic용수) 17% 차지 → 공업용수 수요 원인", depth: 1 },
{ text: "2024년 1월 1일부터 연간 최소 60,000㎥를 소비하는 사업자는 재이용수 사용 의무화", depth: 1 },
{ text: "반도체 공장의 경우 최소 50% 이상의 재이용수를 요구함(현재는 43% 정도)", depth: 1 },
{ text: "재정 지원을 통해 재이용 활성화를 지원" },
{ text: "재이용 설비를 기준으로 2025년까지 싱가포르 수자원공사의 2가지 금융상품 제공", depth: 1 },
{ text: "1. Water Efficiency Fund", depth: 2 },
{ text: "2. Industrial Water Solutions Demonstration Fund", depth: 2 },
{ text: "2025년 기준 지원 한도 상향 → 물 재이용 활성 도모", depth: 1 },
{ text: "투입비용 50% → 75% 지원 / WSDP: $400만 → $500만", depth: 3 },
{ text: "물 효율이 높은 장비를 도입하는 데에도 최대 $300만 지원 가능", depth: 3 },
],
},
{
title: "특이사항",
items: [
{ text: "싱가포르는 천연 수자원이 많지 않아, 말레이시아 조호르와 1962년 맺은 협약으로 물을 수입해왔으며, 재이용수를 늘리기 위한 노력을 계속하고 있음" },
{ text: "싱가포르는 국토 내에 호수와 하천이 적고 집수공간도 적어 빗물로 확보할 수 있는 수자원량은 전체 물 수요의 20%에 불과함", depth: 1 },
{ text: "싱가포르는 2002년부터 하수 재이용수인 NEWater를 개발, 비음용수와 간접음용수로 활용하고 있음" },
{ text: "재이용수를 NEWater라는 이름으로 명명하고 NEWater에 대한 신뢰 확보를 위해 NEWater 방문자 센터도 건설함", depth: 1 },
{ text: "비음용수로 많이 사용하며 건기에는 NEWater를 저수지에 추가하여 원수와 섞음. 원수는 상수도에서 처리 한 후 가정에 수돗물로 공급함", depth: 1 },
{ text: "NEWater는 전용 파이프를 통해 주로 웨이퍼 제조 공장, 산업단지, 상업용 건물에서 산업 및 냉각수로 사용. 제일 많이 사용하는 곳은 웨이퍼 제조 공장", depth: 1 },
{ text: "2024년 4월 기준 1㎥ 당 공업용수 가격은 $1.66이며, Non-domestic 기준 NEWater 가격은 $2.41" },
{ text: "공업용수 가격 $1.66은 수도요금(water tariff)과 수자원세(waterborne tax)로 구분됨. $1.66에는 $1만큼의 수자원세가 포함됨" },
{ text: "2023년 싱가포르는 산업용수 수요가 높은 산업에 대해 물 재이용을 의무화했으며, 정부는 기금을 통해 물 재이용을 지원함" },
],
},
],
},
{
type: "default",
id: "example-global-australia",
title: "[호주] 생활 속 물 재이용 / 광역 물 재이용수 공급",
slides: [
{
src: "/images/landing/business/globalCase/australia_01.png",
alt: "호주 생활 속 물 재이용 1",
},
{
src: "/images/landing/business/globalCase/australia_02.png",
alt: "호주 광역 물 재이용수 공급 2",
},
{
src: "/images/landing/business/globalCase/australia_03.png",
alt: "호주 광역 물 재이용수 공급 3",
},
],
blocks: [
{
title: "생활용수로 재이용수의 적극 활용",
items: [
"재이용수를 공원, 골프장 등의 관계용수로 공급(초록색 지역)",
"Dual pipe로 재이용수를 특정 주거단지로 공급(보라색 지역)",
"현재 7,000여 가구가 재이용수를 사용",
"2030년까지 43,000가구로 확대 예정",
],
},
{
title: "호주 재이용수 광역 활용(퀸즈랜드주 브리즈번)",
items: [
{ text: "SEQ Water Grid는 브리즈번 남동부지역의 식수 공급을 위해 설정한 광역 수로망" },
{ text: "이 광역수로망을 활용하여 물이 부족한 지역으로 물을 공급" },
{
text: (
<>
12개 댐, 32개 수처리 시설, 3개 재이용시설, 담수화 플랜트 1개, 저수지 28개와{" "}
<span className="text-[#01ACE5]">
600km 이상의 물 공급 관로를 건설
</span>
</>
),
depth: 1,
},
{
text: (
<>
지역적으로 강수량이 고르지 않아,{" "}
<span className="text-[#01ACE5]">
댐의 저수율이 불균등 할 때
</span>{" "}
지역의 물 공급을 보충하는 중요한 역할 수행
</>
),
depth: 1,
},
{ text: "안핑을 포함한 16건 공사 완료시 628,100 CMD 공급 가능" },
{ text: "공급단가는 수질과 운영조건에 따라 다르게 책정" },
],
},
{
title: "특이사항",
items: [
"호주는 2000~2009년 강수량 감소로 밀레니엄 가뭄이 발생하여 심각한 물 부족 현상이 발생함",
"호주는 해안 도시에 인구가 밀집하여 거주하고 있으며 인구 증가율이 타 선진국 대비 높아, 향후 도시에 공급할 수자원 확보가 필요함",
"호주정부는 1990년대부터 하·폐수가 방류될 경우의 환경 피해에 대해 인식하기 시작하고 배출규제와 위원회를 구성하기 시작",
"연방정부는 주에 인센티브를 주는 방식으로 물 정책을 장려하고 토지소유권과 물 권리를 구분하여 거래를 허용하고 물 재이용 활성화를 위한 Water Smart Australia 프로그램을 시행",
"호주의 용수공급 체계는 영국과 미국의 특성들이 존재하지만, 주의 특성과 주정부의 정책 방향에 따라 주마다 많은 차이가 있음",
],
},
],
},
{
type: "default",
id: "example-global-eu",
title: "[EU] 법률을 제정, EU국가에서 물 재이용 유도",
slides: [
{
src: "/images/landing/business/globalCase/eu_01.png",
alt: "EU 물 재이용 기술 사례",
},
{
src: "/images/landing/business/globalCase/eu_02.png",
alt: "EU 물 재이용 유도 사례",
},
],
blocks: [
{
title: "물 재이용에 대한 법률을 제정(2020 제정, 2023 시행)",
items: [
"유럽 지역의 물 부족 문제를 인식하고 ’20년 물 재이용 법률을 제정 (2020/741)",
"2023년부터 모든 EU국가에서 시행",
],
},
{
title: "유럽연합 회원국 물 재이용 지원정책",
items: [
{ text: "유럽연합 차원에서 다양한 물 재이용 프로그램 자금 지원" },
{ text: "2021년부터 3년간 Life RECYCLIO 프로그램 지원 → 세부내용 물 재이용 프로그램", depth: 1 },
{ text: "1. 환경 관련 사업에 자금을 지원하는 Life 프로그램의 일환으로 80만 유로를 지원", depth: 2 },
{ text: "2. 식품·농업 분야 물 재이용 확대를 위해 기술 이전 및 평가 실시", depth: 2 },
{ text: "물 재이용 정책 수행을 위해 BOOST-INN 180만 유로를 지원", depth: 1 },
{ text: "프랑스, 이탈리아, 스페인 등 유럽 선진국 솔루션 공유", depth: 3 },
{ text: "EU산하 JRC(공동연구센터)에서 물 재이용 기술제공 지원" },
{ text: (
<>
‘물 재이용 회원 관리 실무 커뮤니티(Water Reuse Management Community of Practice)’ 시범사업 수행
<br />
&nbsp; 유럽 전역에 성공적인 모델 활용 및 혁신적인 물 재이용 솔루션을 확장할 수 있는 기술을 제공
</>
),
depth: 1
}
],
},
{
title: "특이사항",
items: [
"2019년 기준 EU 인구의 38%가 물 부족의 영향을 받을 것으로 예상됨. 이 흐름대로면 2030년까지 유럽 유역 전반에 물 부족 문제가 발생하게 될 것으로 예상. 인구 증가, 도시화의 확대, 염분화 및 기후변화들이 수자원 문제들의 주요 요인이며, 식습관 변화와 산업용수 수요 증가 등으로 악화되고 있는 상황",
"유럽의 물 부족 상황은 지역에 따라 다르게 나타나 스페인을 비롯한 남부유럽에서 특히 심각함. 전반적으로 북유럽에 비해 남유럽의 물 스트레스가 심하며, 특히 스페인과 포르투갈, 프랑스 동부지역의 물 스트레스가 심각함",
"유럽 연합에서도 물 부족 문제를 인식하고 2000년 EU Water Framework Directive를 개발, 이후 2020년 재이용에 관한 법률을 제정",
],
},
],
},
];
function DefaultGlobalSection({
item,
}: {
item: Extract<GlobalCase, { type: "default" }>;
}) {
return (
<>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[1240px]"
slides={item.slides}
/>
<ContentBlocks
blocks={item.blocks}
cards={item.cards}
table={item.table}
/>
</>
);
}
export default function GlobalCaseSection() {
const title = "해외 재이용 사례";
const title = "해외 재이용 사례";
return (
<div className="bg-white">
{/* 상단 타이틀 */}
<div className="inline-block">
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
<span className="relative z-10">{title}</span>
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
<h2 className="relative inline-block px-2 text-[28px] font-extrabold leading-none tracking-tight text-slate-900">
<span className="relative z-10">{title}</span>
<span className="absolute bottom-[-4px] left-0 z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
</h2>
</div>
<div className="mt-10 pb-[120px]">
<section id="example-global-overview" className="scroll-mt-[120px]">
<SectionTitle>사례 개요</SectionTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{
src: "/images/landing/business/globalCase/overview_01.png",
alt: "해외 재이용 사례 개요 1",
},
]}
/>
</section>
<div className="mt-10 pb-[120px]">
{globalCases.map((item, index) => (
<section key={item.id} id={item.id} className="scroll-mt-[120px]">
<SectionTitle>{item.title}</SectionTitle>
<Hr className="my-16" />
{item.type === "overview" ? (
<OverviewSection />
) : (
<DefaultGlobalSection item={item} />
)}
<section id="example-global-taiwan" className="scroll-mt-[120px]">
<SectionTitle>대만</SectionTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{
src: "/images/landing/business/globalCase/taiwan_01.png",
alt: "대만 사례 1",
},
]}
/>
</section>
<Hr className="my-16" />
<section id="example-global-australia" className="scroll-mt-[120px]">
<SectionTitle>호주</SectionTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{
src: "/images/landing/business/globalCase/australia_01.png",
alt: "호주 사례 1",
},
]}
/>
</section>
<Hr className="my-16" />
<section id="example-global-singapore" className="scroll-mt-[120px]">
<SectionTitle>싱가포르</SectionTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{
src: "/images/landing/business/globalCase/singapore_01.png",
alt: "싱가포르 사례 1",
},
]}
/>
</section>
<Hr className="my-16" />
<section id="example-global-eu" className="scroll-mt-[120px]">
<SectionTitle>EU</SectionTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{
src: "/images/landing/business/globalCase/eu_01.png",
alt: "EU 사례 1",
},
]}
/>
</section>
{index !== globalCases.length - 1 && <Hr className="my-20" />}
</section>
))}
</div>
</div>
);
@@ -12,7 +12,7 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
function BlockTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-2">
<span className="inline-block h-[14px] w-[24px] rounded-full bg-gradient-to-r from-sky-300 to-lime-300" />
<span className="relative inline-block h-[16px] w-[27px] align-middle"> <span className="absolute left-0 top-0 h-[16px] w-[16px] rounded-full bg-[#01ACE5]/40" /> <span className="absolute left-[7px] top-0 h-[16px] w-[16px] rounded-full bg-[#6DBB4F]/40" /></span>
<h4 className="text-[20px] font-semibold text-[#1A1A1A]">{children}</h4>
</div>
);
@@ -128,7 +128,7 @@ export default function GreywaterExampleSection() {
projectName="안산시 중수도 설치사업"
purpose="수자원을 효율적으로 활용하여 지속가능성 도모 및 물 절약 실현"
totalCost="81억원"
treatment="MBR공법"
treatment="MBLM공법"
/>
<SupplyTable
@@ -151,7 +151,7 @@ export default function GreywaterExampleSection() {
projectName="용인시 중수도 설치사업"
purpose="물 부족 등의 비상상황에 대한 대비, 하수라도 사용할 집결 등 수자원의 효율적 활용"
totalCost="40억원"
treatment="MBR공법"
treatment="MBLM공법"
/>
<SupplyTable
@@ -172,7 +172,7 @@ export default function GreywaterExampleSection() {
projectName="평택시 중수도 설치사업"
purpose="수자원을 효율적으로 활용하여 지속가능성 도모 및 물 절약 실현"
totalCost="21억원"
treatment="MBR공법"
treatment="MBLM공법"
/>
<SupplyTable
@@ -11,62 +11,215 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
);
}
// 구분선
function BlockTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-2">
<span className="relative inline-block h-[16px] w-[27px] align-middle">
<span className="absolute left-0 top-0 h-[16px] w-[16px] rounded-full bg-[#01ACE5]/40" />
<span className="absolute left-[7px] top-0 h-[16px] w-[16px] rounded-full bg-[#6DBB4F]/40" />
</span>
<h4 className="text-[20px] font-semibold text-[#1A1A1A]">{children}</h4>
</div>
);
}
function Hr({ className = "" }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
function InfoList({
projectName,
purpose,
details,
}: {
projectName: string;
purpose: string;
details: string[];
}) {
return (
<div className="mt-10 space-y-9">
<div>
<BlockTitle>사업명</BlockTitle>
<ul className="mt-4 list-disc pl-8 text-[16px] leading-[1.8] text-[#4F4F4F]">
<li>{projectName}</li>
</ul>
</div>
<div>
<BlockTitle>사업목적</BlockTitle>
<ul className="mt-4 list-disc pl-8 text-[16px] leading-[1.8] text-[#4F4F4F]">
<li>{purpose}</li>
</ul>
</div>
<div>
<BlockTitle>사업 주요 내용</BlockTitle>
<ul className="mt-4 list-disc pl-8 text-[16px] leading-[1.8] text-[#4F4F4F]">
{details.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
</div>
);
}
type RainTableRow = {
facility: string;
storage: string;
supply: string;
pipe: string;
};
function RainTable({ rows }: { rows: RainTableRow[] }) {
return (
<div className="mt-8 overflow-x-auto">
<table className="w-full min-w-[860px] border border-[#D8DDE3] text-center">
<thead>
<tr>
<th className="border border-[#D8DDE3] bg-[#14A9D9] px-4 py-3 text-[18px] font-medium text-white">
시설명
</th>
<th className="border border-[#D8DDE3] bg-[#39B2BD] px-4 py-3 text-[18px] font-medium text-white">
저류조용량(㎥)
</th>
<th className="border border-[#D8DDE3] bg-[#58B99A] px-4 py-3 text-[18px] font-medium text-white">
빗물공급기
</th>
<th className="border border-[#D8DDE3] bg-[#70BF4B] px-4 py-3 text-[18px] font-medium text-white">
빗물분사장치(m)
</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={`${row.facility}-${index}`}>
<td className="border border-[#D8DDE3] bg-white px-4 py-3 text-[16px] text-[#4F4F4F]">
{row.facility}
</td>
<td className="border border-[#D8DDE3] bg-white px-4 py-3 text-[16px] text-[#4F4F4F]">
{row.storage}
</td>
{index === 0 && (
<td
rowSpan={rows.length}
className="border border-[#D8DDE3] bg-white px-4 py-3 text-[16px] text-[#4F4F4F]"
>
{row.supply}
</td>
)}
<td className="border border-[#D8DDE3] bg-white px-4 py-3 text-[16px] text-[#4F4F4F]">
{row.pipe}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default function RainExampleSection() {
const title = "빗물 재이용 사례";
return (
<div className="bg-white">
{/* 상단 타이틀 */}
<div className="inline-block">
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
<span className="relative z-10">{title}</span>
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
<h2 className="relative inline-block px-2 text-[28px] font-extrabold leading-none tracking-tight text-slate-900">
<span className="relative z-10">{title}</span>
<span className="absolute bottom-[-4px] left-0 z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
</h2>
</div>
<section id="example-rain-suwon" className="mt-10 scroll-mt-[120px]">
<SectionTitle>경기도 수원시</SectionTitle>
<div className="mt-10 pb-[120px]">
<section id="example-rain-suwon" className="scroll-mt-[120px]">
<SectionTitle>경기도 수원시</SectionTitle>
<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",
},
]}
/>
</section>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[1240px]"
slides={[
{ src: "/images/landing/business/rain/suwon_01.png", alt: "수원시 빗물이용시설 사례 1" },
{ src: "/images/landing/business/rain/suwon_02.png", alt: "수원시 빗물이용시설 사례 2" },
{ src: "/images/landing/business/rain/suwon_03.png", alt: "수원시 빗물이용시설 사례 3" },
{ src: "/images/landing/business/rain/suwon_04.png", alt: "수원시 빗물이용시설 사례 4" },
{ src: "/images/landing/business/rain/suwon_05.png", alt: "수원시 빗물이용시설 사례 5" },
]}
/>
<Hr className="my-16" />
<InfoList
projectName="수원시 빗물이용시설 설치사업"
purpose="월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물공급기를 설치하여 축서기 도시 열섬효과 및 도로 비산먼지 저감"
details={[
"총사업비 : 20억원",
"위치 : 수원시 영통구, 팔달구 및 장안구 일원",
]}
/>
<section id="example-rain-jeju" className="scroll-mt-[120px]">
<SectionTitle>제주특별자치도</SectionTitle>
<div className="mt-9">
<BlockTitle>사업량</BlockTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{
src: "/images/landing/business/rain/jeju_01.png",
alt: "제주 사례 1",
},
{
src: "/images/landing/business/rain/jeju_02.png",
alt: "제주 사례 2",
},
]}
/>
</section>
<RainTable
rows={[
{
facility: "광교중학교",
storage: "4,500",
supply: "1대",
pipe: "160",
},
{
facility: "수원월드컵경기장",
storage: "12,000",
supply: "1대",
pipe: "451",
},
{
facility: "수원종합운동장",
storage: "10,000",
supply: "1대",
pipe: "231",
},
{
facility: "수원컨벤션센터",
storage: "3,600",
supply: "1대",
pipe: "460",
},
]}
/>
</div>
</section>
<Hr className="my-20" />
<section id="example-rain-jeju" className="scroll-mt-[120px]">
<SectionTitle>제주특별자치도</SectionTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[1240px]"
slides={[
{ src: "/images/landing/business/rain/jeju_01.png", alt: "제주특별자치도 빗물이용시설 사례 1" },
{ src: "/images/landing/business/rain/jeju_02.png", alt: "제주특별자치도 빗물이용시설 사례 2" },
{ src: "/images/landing/business/rain/jeju_03.png", alt: "제주특별자치도 빗물이용시설 사례 3" },
]}
/>
<InfoList
projectName="제주특별자치도 중규모 빗물이용시설 설치사업"
purpose="강우시 비닐하우스에서 발생하는 대체수자원인 빗물을 순환 활용하고자 하는 사업으로, 지하수 사용 저감과 집중호우시 도로와 농경지 침수피해 예방"
details={[
"총사업비 : 278억원",
"위치 : 제주특별자치도 서귀포시 남원읍 위미리 일원",
"사업량 : 저류조 7,000㎥, 배수갑 500㎡, 관로 21.7km",
"사업기간 : 2024. 5. ~ 2028. 12.",
]}
/>
</section>
</div>
</div>
);
}
@@ -2,6 +2,20 @@
import CaseImageSlider from "@/components/CaseImageSlider";
type SewageCase = {
id: string;
title: string;
slides: {
src: string;
alt: string;
}[];
projectName: string;
purpose: string[];
method?: string[];
details: string[];
note?: string[];
};
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
@@ -11,245 +25,342 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
);
}
// 구분선
function BlockTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-2">
<span className="relative inline-block h-[16px] w-[27px] align-middle">
<span className="absolute left-0 top-0 h-[16px] w-[16px] rounded-full bg-[#01ACE5]/40" />
<span className="absolute left-[7px] top-0 h-[16px] w-[16px] rounded-full bg-[#6DBB4F]/40" />
</span>
<h4 className="text-[20px] font-semibold text-[#1A1A1A]">{children}</h4>
</div>
);
}
function Hr({ className = "" }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
function InfoBlock({
title,
items,
}: {
title: string;
items?: string[];
}) {
if (!items || items.length === 0) return null;
return (
<div>
<BlockTitle>{title}</BlockTitle>
<ul className="mt-4 list-disc space-y-2 pl-8 text-[16px] leading-[1.8] text-[#4F4F4F]">
{items.map((item, index) => (
<li key={`${title}-${index}`}>{item}</li>
))}
</ul>
</div>
);
}
function InfoList({ item }: { item: SewageCase }) {
return (
<div className="mt-10 space-y-9">
<InfoBlock title="사업명" items={[item.projectName]} />
<InfoBlock title="사업목적" items={item.purpose} />
<InfoBlock title="사업방식" items={item.method} />
<InfoBlock title="사업 주요 내용" items={item.details} />
<InfoBlock title="특이사항" items={item.note} />
</div>
);
}
const sewageCases: SewageCase[] = [
{
id: "example-sewage-guri",
title: "경기도 구리시",
slides: [
{
src: "/images/landing/business/sewage/guri_01.png",
alt: "구리시 도로자동청소시스템",
},
{
src: "/images/landing/business/sewage/guri_02.png",
alt: "구리시 도로자동청소시스템 위치도",
},
{
src: "/images/landing/business/sewage/guri_03.png",
alt: "구리시 도로자동청소시스템 위치도2",
},
{
src: "/images/landing/business/sewage/guri_04.png",
alt: "구리시 도로자동청소시스템 원리",
},
],
projectName: "구리시 도로자동청소시스템 설치사업",
purpose: [
"고농도 미세먼지 발생 및 폭염의 급증에 따라 공공하수처리시설 발생 재이용수 일부를 도로자동청소시스템에 활용하여 미세먼지 저감 및 폭염 취약성 완화",
],
details: [
"총사업비 : 61억원",
"위치 : 구리시 경춘로 및 갈매중앙로 일대 등",
"관로 : 5.8km",
"공급량 : 178㎥/일",
],
},
{
id: "example-sewage-bucheon",
title: "경기도 부천시",
slides: [
{
src: "/images/landing/business/sewage/bucheon_01.png",
alt: "부천시 하수 재이용수 생산시설",
},
{
src: "/images/landing/business/sewage/bucheon_02.png",
alt: "부천시 하수처리수 재이용 사례",
},
],
projectName: "부천시 하수 재이용수 생산시설 설치 운영",
purpose: [
"굴포하수처리시설 하수처리수 재이용수 생산시설 설치 운영을 통해 여월천 등 부천 관내 하천의 건천화 방지, 하천 생태계 보존 및 주민들에게 친수 공간 제공",
],
details: [
"총사업비 : 212억원",
"위치 : 부천시 여월천, 삼정천, 심곡천 및 시민의 강",
"관로 : 23.6km",
"공급량 : 50,000㎥/일",
],
},
{
id: "example-sewage-suwon",
title: "경기도 수원시",
slides: [
{
src: "/images/landing/business/sewage/suwon_01.png",
alt: "수원시 황구지천1",
},
{
src: "/images/landing/business/sewage/suwon_02.png",
alt: "수원시 황구지천2",
},
{
src: "/images/landing/business/sewage/suwon_03.png",
alt: "수원시 황구지천 지도",
},
],
projectName: "수원시 황구지천 하수처리수 재이용사업",
purpose: [
"황구지천 공공하수처리시설 하수처리수 재이용사업으로 황구 호매실천 및 금곡천 건천화 방지로 주민들에게 친수 공간 제공 및 하천 생태계 보전",
],
details: [
"총사업비 : 110억원",
"위치 : 수원시 권선구 호매실천, 금곡천",
"관로 : 3.9km",
"공급량 : 25,000㎥/일(하수처리시설 용량 : 45,000㎥/일)",
],
},
{
id: "example-sewage-paju",
title: "경기도 파주시",
slides: [
{
src: "/images/landing/business/sewage/paju_01.png",
alt: "파주시 하수처리수 시설 전경1",
},
{
src: "/images/landing/business/sewage/paju_02.png",
alt: "파주시 하수처리수 시설 전경2",
},
],
projectName: "파주시 하수처리수 재이용사업",
purpose: [
"운정 및 금촌하수처리시설의 하수처리수를 막공법으로 재처리하여 파주LCD산업단지 내 3개 업체에 공업용수를 공급(LG디스플레이, 희성전자, 엔비텍)",
"하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례",
],
method: ["민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)"],
details: [
"총사업비 : 651억원",
"위치 : 파주시 금촌동 714번지 일원",
"관로 : 13.6km",
"공급량 : 40,000㎥/일(시설용량 : 41,200㎥/일)",
],
},
{
id: "example-sewage-pohang",
title: "경상북도 포항시",
slides: [
{
src: "/images/landing/business/sewage/pohang_01.png",
alt: "포항시 하수처리수 전경1",
},
{
src: "/images/landing/business/sewage/pohang_02.png",
alt: "포항시 하수처리수 전경2",
},
{
src: "/images/landing/business/sewage/pohang_03.png",
alt: "포항시 하수처리수 재이용 공급계통",
},
],
projectName: "포항시 하수처리수 재이용사업",
purpose: [
"포항시 내 공단 개발 확대로 지속적이고 안정적인 공업용수 공급 추진(포스코, 동국산업 등)",
],
method: ["민간투자사업(운영사 : ㈜포웰, 한국수자원공사)"],
details: [
"총사업비 : 1,400억원",
"위치 : 포항시 남구 상도동 125번지 일원",
"관로 : 11.7km",
"공급량 : 100,000㎥/일(시설용량 : 100,000㎥/일)",
"처리공법 : 전처리시설(U/F) + 주처리시설(R/O)",
],
},
{
id: "example-sewage-ulsan",
title: "울산광역시",
slides: [
{
src: "/images/landing/business/sewage/ulsan_01.png",
alt: "울산광역시 폐수처리수 전경1",
},
{
src: "/images/landing/business/sewage/ulsan_02.png",
alt: "울산광역시 폐수처리수 전경2",
},
{
src: "/images/landing/business/sewage/ulsan_03.png",
alt: "울산광역시 폐수처리수 시설1",
},
{
src: "/images/landing/business/sewage/ulsan_04.png",
alt: "울산광역시 폐수처리수 시설2",
},
],
projectName: "울산광역시 폐수처리수 재이용사업",
purpose: [
"’18년 갈수기시 낙동강 수질 악화로 울산석유화학단지 용수 공급 부족을 해소하기 위해 미포산단 내 한화케미칼 등 8개사 공업용수 공급",
],
method: ["민간투자사업(시행사 :울산화이텍워터(주))"],
details: [
"총사업비 : 1,729억원",
"위치 : 울산광역시 울주군 청량읍 상개로 14 일원",
"관로 : 11.7km",
"공급량 : 17,000㎥/일(시설용량 : 86,000㎥/일)",
"처리공법 : 전처리시설(U/F) + 주처리시설(R/O, EDI)",
],
},
{
id: "example-sewage-yeosu",
title: "전라남도 여수시",
slides: [
{
src: "/images/landing/business/sewage/yeosu_01.png",
alt: "여수시 하수처리수 재이용시설1",
},
{
src: "/images/landing/business/sewage/yeosu_02.png",
alt: "여수시 하수처리수 재이용시설2",
},
],
projectName: "여수시 하수처리수 재이용사업",
purpose: [
"수요자가 원하는 양질의 공업용수를 저렴하게 공급하여 한정된 수자원의 효율적 이용 및 지역적 물 부족 해소, 하수재이용을 통한 오염총량 저감 및 방류수역 수질개선을 위한 여수국가산단 내 공업용수 공급(GS칼텍스, YNCC 등)",
],
method: ["민간투자사업(시행사 : 여수그린허브(주), 운영사 : 테크로스)"],
details: [
"총사업비 : 750억원",
"위치 : 여수시 신월로 284 일원",
"관로 : 18.6km",
"공급량 : 50,000㎥/일(시설용량 : 50,000㎥/일)",
"처리공법 : 전처리시설(U/F) + 주처리시설(R/O)",
],
},
{
id: "example-sewage-boryeong",
title: "충청남도 보령시",
slides: [
{
src: "/images/landing/business/sewage/boryeong_01.png",
alt: "보령시 하수처리수 재이용시설1",
},
{
src: "/images/landing/business/sewage/boryeong_02.png",
alt: "보령시 하수처리수 재이용시설2",
},
],
projectName: "보령시 하수처리수 재이용사업",
purpose: [
"한국중부발전(주)의 보령 및 신보령화력발전소에 10,000㎥/일의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진",
"국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU(기후에너지환경부, 충청남도, 보령시, 한국중부발전(주))를 바탕으로 추진되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모",
],
method: ["재정사업(2017년 한국환경공단과 사업대행 위·수탁협약 체결)"],
details: [
"총사업비 : 315억원",
"위치 : 보령시 대천방조제로 322 일원",
"관로 : 8.7km",
"공급량 : 10,000㎥/일(시설용량 : 10,000㎥/일)",
"처리공법 : 전처리시설(U/F) + 주처리시설(R/O)",
],
note: [
"국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU(환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모",
],
},
{
id: "example-sewage-asan",
title: "충청남도 아산시",
slides: [
{
src: "/images/landing/business/sewage/asan_01.png",
alt: "아산시 하수처리수 재이용시설",
},
{
src: "/images/landing/business/sewage/asan_02.png",
alt: "아산시 하수처리수 재이용 공정도",
},
],
projectName: "아산시 하수처리수 재이용사업",
purpose: [
"신도시 개발에 따른 인구 급증으로 발생하는 하수를 재처리하여 탕정산업단지(삼성디스플레이)에 공업용수를 안정적으로 공급함으로써 지역기반 및 활성화를 도모",
],
method: ["민간투자사업(시행사 : 아산스마트원터, 운영사 : 한국수자원공사)"],
details: [
"총사업비 : 297억원",
"위치 : 아산시 탕정면 갈산리 807 일원",
"관로 : 4.2km",
"공급량 : 27,000㎥/일(시설용량 : 45,000㎥/일)",
"처리공법 : 전처리시설(U/F) + 주처리시설(R/O)",
],
},
];
export default function SewageExampleSection() {
const title = "하수처리수 재이용 사례";
const title = "하수처리 재이용 사례";
return (
<div className="bg-white">
{/* 상단 타이틀 */}
<div className="inline-block">
<h2 className="relative inline-block px-2 text-[28px] font-extrabold tracking-tight leading-none text-slate-900">
<span className="relative z-10">{title}</span>
<span className="absolute left-0 bottom-[-4px] z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
<h2 className="relative inline-block px-2 text-[28px] font-extrabold leading-none tracking-tight text-slate-900">
<span className="relative z-10">{title}</span>
<span className="absolute bottom-[-4px] left-0 z-0 h-3 w-full bg-gradient-to-r from-sky-300 to-green-300" />
</h2>
</div>
<div className="mt-10">
<section id="example-sewage-bolyeong" className="scroll-mt-[120px]">
<SectionTitle>충청남도 보령시</SectionTitle>
<div className="mt-10 pb-[120px]">
{sewageCases.map((item, index) => (
<section key={item.id} id={item.id} className="scroll-mt-[120px]">
<SectionTitle>{item.title}</SectionTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{
src: "/images/landing/business/sewage/bolyeong_01.png",
alt: "보령시 하수 재이용 1",
},
]}
className="mt-8"
maxWidthClassName="max-w-[1240px]"
slides={item.slides}
/>
<section className="mt-6">
<SectionTitle>사업명</SectionTitle>
<p className="mt-8 text-base text-gray-600">
충남 보령 하수재이용
<br />: 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해
보령하수처리시설의 하수처리수 재이용 사업 추진
</p>
</section>
<InfoList item={item} />
<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>
<SectionTitle>특이사항</SectionTitle>
<p className="mt-8 text-base text-gray-600">
국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권
가뭄 시 지역사회 기여 차원에서 기관간 MOU를 바탕으로 추진
</p>
</section>
</section>
<Hr className="my-16" />
<section id="example-sewage-cheongju" className="scroll-mt-[120px]">
<SectionTitle>청주시</SectionTitle>
<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>
<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 />
적용기술 : 분리막 UF, RO
</p>
</section>
</section>
<Hr className="my-16" />
<section id="example-sewage-guri" className="scroll-mt-[120px]">
<SectionTitle>구리시 도로자동청소시스템</SectionTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{
src: "/images/landing/business/sewage/guri_01.png",
alt: "구리시 도로자동청소시스템 1",
},
{
src: "/images/landing/business/sewage/guri_02.png",
alt: "구리시 도로자동청소시스템 2",
},
]}
/>
<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>
</section>
<Hr className="my-16" />
<section id="example-sewage-hwanggu" className="scroll-mt-[120px]">
<SectionTitle>수원시 황구지천</SectionTitle>
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
slides={[
{
src: "/images/landing/business/sewage/hwanggu_01.png",
alt: "수원시 황구지천 하수 재이용 1",
},
{
src: "/images/landing/business/sewage/hwanggu_02.png",
alt: "수원시 황구지천 하수 재이용 2",
},
{
src: "/images/landing/business/sewage/hwanggu_03.png",
alt: "수원시 황구지천 하수 재이용 3",
},
]}
/>
<section className="mt-6">
<SectionTitle>사업명</SectionTitle>
<p className="mt-8 text-base text-gray-600">
수원시 황구지천 하수처리수 재이용사업
</p>
</section>
</section>
<Hr className="my-16" />
<section id="example-sewage-paju" className="scroll-mt-[120px]">
<SectionTitle>파주시</SectionTitle>
<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 />
운정·금촌하수처리시설의 하수처리수를 막공법으로 재처리하여 파주 LCD
산업단지에 공업용수 공급
</p>
</section>
</section>
<Hr className="my-16" />
<section id="example-sewage-yeosu" className="scroll-mt-[120px]">
<SectionTitle>여수시</SectionTitle>
<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>
</section>
{index !== sewageCases.length - 1 && <Hr className="my-20" />}
</section>
))}
</div>
</div>
);
@@ -39,7 +39,7 @@ export default function GlobalCasePage() {
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
maxWidthClassName="max-w-[1240px]"
slides={[
{
src: "/images/landing/business/globalCase/overview_01.png",
@@ -242,7 +242,7 @@ export default function GlobalCasePage() {
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
maxWidthClassName="max-w-[1240px]"
slides={[
{
src: "/images/landing/business/globalCase/taiwan_01.png",
@@ -442,7 +442,7 @@ export default function GlobalCasePage() {
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
maxWidthClassName="max-w-[1240px]"
slides={[
{
src: "/images/landing/business/globalCase/australia_01.png",
@@ -556,7 +556,7 @@ export default function GlobalCasePage() {
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
maxWidthClassName="max-w-[1240px]"
slides={[
{
src: "/images/landing/business/globalCase/singapore_01.png",
@@ -727,7 +727,7 @@ export default function GlobalCasePage() {
<CaseImageSlider
className="mt-8"
maxWidthClassName="max-w-[760px]"
maxWidthClassName="max-w-[1240px]"
slides={[
{
src: "/images/landing/business/globalCase/eu_01.png",
+12 -12
View File
@@ -27,15 +27,18 @@ const menus: BusinessSubNavMenu[] = [
},
{
key: "example-sewage",
label: "하수처리수 재이용 사례",
label: "하수처리 재이용 사례",
defaultOpen: false,
sections: [
{ id: "example-sewage-bolyeong", label: "충청남도 보령시" },
{ id: "example-sewage-cheongju", label: "청주시" },
{ id: "example-sewage-guri", label: "구리시 도로자동청소시스템" },
{ id: "example-sewage-hwanggu", label: "수원시 황구지천" },
{ id: "example-sewage-paju", label: "파주시" },
{ id: "example-sewage-yeosu", label: "여수시" },
{ id: "example-sewage-guri", label: "경기도 구리시" },
{ id: "example-sewage-bucheon", label: "경기도 부천시" },
{ id: "example-sewage-suwon", label: "경기도 수원시" },
{ id: "example-sewage-paju", label: "경기도 파주시" },
{ id: "example-sewage-pohang", label: "경기도 포항시" },
{ id: "example-sewage-ulsan", label: "울산광역시" },
{ id: "example-sewage-yeosu", label: "전라남도 여수시" },
{ id: "example-sewage-boryeong", label: "충청남도 보령시" },
{ id: "example-sewage-asan", label: "충청남도 아산시" },
],
},
{
@@ -45,8 +48,8 @@ const menus: BusinessSubNavMenu[] = [
sections: [
{ id: "example-global-overview", label: "사례 개요" },
{ id: "example-global-taiwan", label: "대만" },
{ id: "example-global-australia", label: "호주" },
{ id: "example-global-singapore", label: "싱가포르" },
{ id: "example-global-australia", label: "호주" },
{ id: "example-global-eu", label: "EU" },
],
},
@@ -71,13 +74,10 @@ export default function ExamplePage() {
{/* 우측 앵커 네비 */}
<BusinessSubNav menus={menus} offsetTop={120} />
<div className="max-w-[1300px] pb-24">
<div className="max-w-[1320px] pb-24">
<RainExampleSection />
<Hr className="my-16" />
<GreywaterExampleSection />
<Hr className="my-16" />
<SewageExampleSection />
<Hr className="my-16" />
<GlobalCaseSection />
</div>
</div>
+1 -1
View File
@@ -75,7 +75,7 @@ export default function IntroPage() {
<BusinessSubNav menus={menus} offsetTop={120} />
{/* 본문 */}
<div className="max-w-[1300px] pb-24">
<div className="max-w-[1320px] pb-24">
<LawIntroSection />
<RainIntroSection />
<ReuseWaterIntroSection />
+1 -4
View File
@@ -1,7 +1,6 @@
import "../reuse.module.css";
import { StoreProvider } from "../store";
import TopNav from "@/components/TopNav";
import Lnb from "@/components/Lnb";
export default function WithLayout({ children }: { children: React.ReactNode }) {
return (
@@ -9,10 +8,8 @@ export default function WithLayout({ children }: { children: React.ReactNode })
<TopNav />
<div className="pt-[83px] flex">
{/* ✅ 좌측 LNB */}
<Lnb />
{/* ✅ 본문 */}
{/* 본문 */}
<main className="ml-[260px] w-full">
{children}
</main>
@@ -1,6 +1,7 @@
"use client";
import React from "react";
import Image from "next/image";
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
@@ -23,6 +24,87 @@ function SourceText() {
);
}
function SewageIntroSummary() {
const items = [
{
icon: "/images/landing/icon_cycle_water.png",
title: "안정적인 재이용률 유지",
desc: "연간 약 1,136백만m³의 하수처리수가 재이용되고 있으며, 약 15%에 달하는 높은 재이용률을 통해 수자원 선순환 체계를 구축하고 있습니다.",
},
{
icon: "/images/landing/icon_water_flow.png",
title: "메마른 하천을 살리는 물",
desc: "재이용되는 물의 가장 많은 부분이 하천유지용수로 사용되어 도심 속 하천의 생태계를 보호하고 쾌적한 수변 환경을 조성합니다.",
},
{
icon: "/images/landing/icon_location.png",
title: "지역별 균형 있는 활용",
desc: "전국적으로 고르게 시설이 확충되고 있으며, 특히 경기 지역을 중심으로 대규모 공업용수 공급망을 구축하여 지역 경제와 환경 보호를 동시에 실현하고 있습니다.",
},
];
return (
<div className="mb-[70px] px-[60px]">
<div className="grid grid-cols-[1fr_400px] items-center gap-[136px]">
{/* 좌측 */}
<div>
<h3 className="text-[40px] font-extrabold leading-[1.35] tracking-[-0.1em] text-neutral-900">
버려지는 하수에서 찾는 새로운 가치,
<br />
<span className="inline-block bg-gradient-to-b from-[#6DBB4F] to-[#01ACE5] bg-clip-text text-transparent">
하수처리수 재이용
</span>
이 도시를 숨쉬게 합니다.
</h3>
<p className="mt-5 text-[16px] leading-[1.8] text-neutral-600">
하수처리장에서 정화된 물은 하천의 수량을 조절하고
<br />
산업 현장에 용수를 공급하는 소중한 자원입니다.
</p>
<div className="mt-10 space-y-8">
{items.map((item) => (
<div key={item.title} className="flex gap-4">
<div className="mt-1 flex h-12 w-12 shrink-0 items-center justify-center">
<Image
src={item.icon}
alt=""
width={48}
height={48}
className="object-contain"
/>
</div>
<div>
<strong className="block text-[18px] font-extrabold text-neutral-900">
{item.title}
</strong>
<p className="mt-2 text-[16px] leading-[1.7] text-neutral-600">
{item.desc}
</p>
</div>
</div>
))}
</div>
</div>
{/* 우측 이미지 */}
<div className="flex items-center justify-center">
<Image
src="/images/landing/sewage_status_img.png"
alt="하수처리수 재이용 개념도"
width={420}
height={360}
className="h-auto w-full max-w-[420px] object-contain"
priority
/>
</div>
</div>
</div>
);
}
const tableStyles = {
container: "w-[1000px] mx-auto overflow-hidden",
table: "w-full table-fixed border-collapse text-center text-[16px]",
@@ -44,220 +126,224 @@ export default function SewageStatusSection() {
</h2>
</div>
<div className="mt-10">
<div className="mt-[70px] pb-[120px]">
{/* 1) 연도별 하수처리수 재이용 현황 */}
<section id="status-treated-year" className="mt-10 scroll-mt-[120px]">
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
<section>
{/* 인트로 */}
<SewageIntroSummary />
<div className="mt-10">
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: "130px" }} />
<col style={{ width: "180px" }} />
<col />
<col />
<col />
<col />
<col />
<col />
</colgroup>
<thead>
<tr>
<th
colSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
구분
</th>
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>
2018
</th>
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>
2019
</th>
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>
2020
</th>
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>
2021
</th>
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>
2022
</th>
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>
2023
</th>
</tr>
</thead>
<tbody>
{[
{
group: "전체",
rows: [
[
"개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
"671",
"681",
"698",
"704",
"714",
"717",
],
[
"하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"7,163.7",
"7,140.4",
"7,375.6",
"7,335.8",
"7,378.9",
"7,537.2",
],
[
"재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"1,112.7",
"1,148.7",
"1,148.7",
"1,127.0",
"1,136.8",
"1,137.5",
],
[
"재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>",
"17.4",
"16.5",
"15.5",
"14.1",
"14.3",
"14.4",
],
],
},
{
group: "법적 대상",
rows: [
[
"개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
"68",
"82",
"94",
"97",
"101",
"118",
],
[
"하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"761.2",
"839.0",
"935.3",
"1,158.7",
"1,220.6",
"1,473.6",
],
[
"재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"162.2",
"190.6",
"218.5",
"244.6",
"247.2",
"324.9",
],
[
"재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>",
"24.7",
"25.4",
"27.8",
"25.7",
"24.2",
"24.1",
],
],
},
{
group: "법적 미대상",
rows: [
[
"개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
"603",
"599",
"604",
"607",
"613",
"599",
],
[
"하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"6,402.5",
"6,301.4",
"6,440.3",
"6,177.1",
"6,158.3",
"6,063.5",
],
[
"재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"950.6",
"958.1",
"926.2",
"882.4",
"889.5",
"812.7",
],
[
"재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>",
"16.5",
"15.3",
"13.6",
"12.2",
"12.6",
"12.5",
],
],
},
].map((section, sIdx) => (
<React.Fragment key={sIdx}>
{section.rows.map((row, rIdx) => (
<tr key={`${sIdx}-${rIdx}`} className="bg-white">
{rIdx === 0 && (
<td
rowSpan={section.rows.length}
className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}
<div id="status-treated-year" className="scroll-mt-[120px]">
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-10">
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: "130px" }} />
<col style={{ width: "180px" }} />
<col />
<col />
<col />
<col />
<col />
<col />
</colgroup>
<thead>
<tr>
<th
colSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
{section.group}
</td>
)}
{row.map((val, i) => {
const isFirstCol = i === 0;
const isHighlightedCol = [1, 3, 5].includes(i);
return (
<td
key={i}
className={`${tableStyles.td} ${
isFirstCol ? "!font-bold !text-neutral-900" : ""
} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
dangerouslySetInnerHTML={{ __html: val }}
/>
);
})}
구분
</th>
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`}>
2018
</th>
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`}>
2019
</th>
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`}>
2020
</th>
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`}>
2021
</th>
<th className={`${tableStyles.th} bg-[#50B880] text-white`}>
2022
</th>
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`}>
2023
</th>
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>
</thead>
<tbody>
{[
{
group: "전체",
rows: [
[
"개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
"671",
"681",
"698",
"704",
"714",
"717",
],
[
"하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"7,163.7",
"7,140.4",
"7,375.6",
"7,335.8",
"7,378.9",
"7,537.2",
],
[
"재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"1,112.7",
"1,148.7",
"1,148.7",
"1,127.0",
"1,136.8",
"1,137.5",
],
[
"재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>",
"17.4",
"16.5",
"15.5",
"14.1",
"14.3",
"14.4",
],
],
},
{
group: "법적 대상",
rows: [
[
"개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
"68",
"82",
"94",
"97",
"101",
"118",
],
[
"하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"761.2",
"839.0",
"935.3",
"1,158.7",
"1,220.6",
"1,473.6",
],
[
"재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"162.2",
"190.6",
"218.5",
"244.6",
"247.2",
"324.9",
],
[
"재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>",
"24.7",
"25.4",
"27.8",
"25.7",
"24.2",
"24.1",
],
],
},
{
group: "법적 미대상",
rows: [
[
"개소수 <span class='text-[13px] font-normal text-[#767676]'>(개소)</span>",
"603",
"599",
"604",
"607",
"613",
"599",
],
[
"하수처리량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"6,402.5",
"6,301.4",
"6,440.3",
"6,177.1",
"6,158.3",
"6,063.5",
],
[
"재이용량 <span class='text-[13px] font-normal text-[#767676]'>(백만m³/년)</span>",
"950.6",
"958.1",
"926.2",
"882.4",
"889.5",
"812.7",
],
[
"재이용률 <span class='text-[13px] font-normal text-[#767676]'>(%)</span>",
"16.5",
"15.3",
"13.6",
"12.2",
"12.6",
"12.5",
],
],
},
].map((section, sIdx) => (
<React.Fragment key={sIdx}>
{section.rows.map((row, rIdx) => (
<tr key={`${sIdx}-${rIdx}`} className="bg-white">
{rIdx === 0 && (
<td
rowSpan={section.rows.length}
className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}
>
{section.group}
</td>
)}
{row.map((val, i) => {
const isFirstCol = i === 0;
const isHighlightedCol = [1, 3, 5].includes(i);
<SourceText />
</div>
return (
<td
key={i}
className={`${tableStyles.td} ${
isFirstCol ? "!font-bold !text-neutral-900" : ""
} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
dangerouslySetInnerHTML={{ __html: val }}
/>
);
})}
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>
<SourceText />
</div>
</div>
</section>
<Hr className="my-11" />
{/* 2) 용도별 하수처리수 재이용 현황 */}
{/* 2) 연도별 하수처리수 재이용 현황 */}
<section id="status-treated-usage" className="scroll-mt-[120px]">
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<div className={tableStyles.container}>
+2 -2
View File
@@ -58,7 +58,7 @@ const menus: BusinessSubNavMenu[] = [
},
{
id: "status-treated-usage",
label: "용도별 하수처리수 재이용 현황",
label: "연도별 하수처리수 재이용 현황",
},
{
id: "status-treated-facility",
@@ -86,7 +86,7 @@ export default function StatusPage() {
{/* 우측 앵커 네비 */}
<BusinessSubNav menus={menus} offsetTop={120} />
<div className="max-w-[1300px] pb-24">
<div className="max-w-[1320px] pb-24">
<RainStatusSection />
<GreywaterStatusSection />
<SewageStatusSection />
+1 -1
View File
@@ -28,7 +28,7 @@ export default function Cases() {
<div className="mt-14">
<a
href="/reuse/cases/rain"
href="/example"
className="
inline-flex items-center justify-center gap-3
w-[260px] h-[52px]
+1 -1
View File
@@ -11,7 +11,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>
<p className="text-xl font-medium leading-6 text-white/85 max-w-[720px] mx-auto">
<p className="text-2xl font-medium leading-8 text-white/85 max-w-[720px] mx-auto">
빗물, 오수, 하수처리수, 폐수처리수, 발전소 온배수를 재이용처리하여<br />
생활, 공업, 농업, 조경, 하천유지 등의 용도로 재이용합니다.
</p>
+5 -5
View File
@@ -20,25 +20,25 @@ const INTRO_CARDS: IntroCardItem[] = [
title: "물 재이용 법",
desc: "물의 재이용 촉진 및 지원에 관한 법률을 시행하고, 일부 법률은 개정되었습니다.",
image: card01,
href: "/reuse/intro/law",
href: "/intro#law-plan",
},
{
title: "친환경 수자원 빗물",
desc: "가뭄을 극복하고 홍수를 예방하기 위해 비가 내릴 때 빗물을 모아두었다가 사용합니다.",
image: card02,
href: "/reuse/intro/rain",
href: "/intro#rain-intro",
},
{
title: "알뜰하게 재이용되는 중수",
desc: "한번 사용한 물을 중수도 시설을 통해 재생하여 개별적 또는 지역적으로 재이용할 수 있습니다.",
image: card03,
href: "/reuse/intro/greywater",
href: "/intro#greywater-intro",
},
{
title: "하수처리수 재이용",
desc: "사용하고 버려진 공공하수처리장으로 모여진 물을 용도에 맞는 처리 후 재이용 할 수 있습니다.",
image: card04,
href: "/reuse/intro/treated",
href: "/intro#treatedwater-intro",
},
];
@@ -79,7 +79,7 @@ export default function IntroCards() {
<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">
<p className="mt-2 text-sm leading-relaxed text-white/60 line-clamp-3">
{card.desc}
</p>
</div>
+96 -113
View File
@@ -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>
);
}
}
+100 -70
View File
@@ -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
View File
@@ -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>
);
}
+4 -4
View File
@@ -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() {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 325 KiB

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 KiB

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 KiB

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 38 KiB