Merge branch 'iipbs/wr/dev' into iipbs/sn/dev

This commit is contained in:
2026-09-07 18:15:04 +09:00
38 changed files with 1753 additions and 2059 deletions
+2 -2
View File
@@ -8,8 +8,8 @@ NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11315
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
SERVER_API_URL = http://dev.onasys.co.kr:8000
CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service/api/v1
WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service
WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
+2 -2
View File
@@ -9,8 +9,8 @@ NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
#NEXT_PUBLIC_CM_LOGIN_PAGE_URL = https://devqueen.hasuon.or.kr/auth/login
SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1
WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1
FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service
WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service
NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
@@ -1,133 +1,99 @@
"use client";
'use client';
import React from "react";
import React from 'react';
// 공통 섹션 타이틀 컴포넌트
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
<div className='flex items-center gap-3'>
<span className='h-5 w-[3px] bg-sky-500' />
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
</div>
);
}
// 구분선
function Hr({ className = "" }: { className?: string }) {
function Hr({ className = '' }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
// 출처 텍스트
function SourceText() {
return (
<p className="mt-7 text-center text-[13px] text-neutral-500">
자료: 환경부(2024), 「2023 하수도 통계」
</p>
);
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 환경부(2024), 「2023 하수도 통계」</p>;
}
const tableStyles = {
container: "w-[1000px] mx-auto overflow-hidden",
table: "w-full table-fixed border-collapse text-center text-[16px]",
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
headerBg: "bg-[#E3E7EA]",
container: 'w-[1000px] mx-auto overflow-hidden',
table: 'w-full table-fixed border-collapse text-center text-[16px]',
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
headerBg: 'bg-[#E3E7EA]',
};
export default function RainStatusSection() {
const title = "빗물이용시설(현황)";
const title = '빗물이용시설(현황)';
return (
<div className="bg-white">
<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" />
<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>
</div>
<div className="mt-10 pb-[120px]">
<div className='mt-10 pb-[120px]'>
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
<section id="status-rain-building" className="mt-10 scroll-mt-[120px]">
<section id='status-rain-building' className='mt-10 scroll-mt-[120px]'>
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
<div className="mt-10">
<div className='mt-10'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<thead>
<tr>
<th
rowSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
style={{ width: "160px" }}
>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
구분
</th>
<th
colSpan={6}
className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}
>
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
빗물이용량 (천m³/년)
</th>
</tr>
<tr>
<th
className={`${tableStyles.th} bg-[#01ACE5] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`} style={{ width: '140px' }}>
2018
</th>
<th
className={`${tableStyles.th} bg-[#17AFC7] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#17AFC7] text-white`} style={{ width: '140px' }}>
2019
</th>
<th
className={`${tableStyles.th} bg-[#2CB2A9] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#2CB2A9] text-white`} style={{ width: '140px' }}>
2020
</th>
<th
className={`${tableStyles.th} bg-[#42B58B] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#42B58B] text-white`} style={{ width: '140px' }}>
2021
</th>
<th
className={`${tableStyles.th} bg-[#57B86D] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#57B86D] text-white`} style={{ width: '140px' }}>
2022
</th>
<th
className={`${tableStyles.th} bg-[#81C784] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#81C784] text-white`} style={{ width: '140px' }}>
2023
</th>
</tr>
</thead>
<tbody>
{[
["골프장", "6,771.8", "6,417.4", "8,371.6", "10,215.4", "11,360", "10,191.6"],
["공공시설", "364", "325.2", "201.9", "244.6", "209.9", "299.6"],
["공동주택", "291", "840.5", "804.2", "2,393.0", "2,386", "2,428.4"],
["대규모점포", "169.9", "214.4", "58.4", "427.2", "371", "293.3"],
["체육시설", "52.6", "44.7", "48.6", "75.3", "71.8", "77.6"],
["학교", "33", "25.5", "22", "155.8", "165.4", "32.6"],
["기타", "311.6", "901.2", "344.5", "537.8", "564", "621.2"],
['골프장', '6,771.8', '6,417.4', '8,371.6', '10,215.4', '11,360', '10,191.6'],
['공공시설', '364', '325.2', '201.9', '244.6', '209.9', '299.6'],
['공동주택', '291', '840.5', '804.2', '2,393.0', '2,386', '2,428.4'],
['대규모점포', '169.9', '214.4', '58.4', '427.2', '371', '293.3'],
['체육시설', '52.6', '44.7', '48.6', '75.3', '71.8', '77.6'],
['학교', '33', '25.5', '22', '155.8', '165.4', '32.6'],
['기타', '311.6', '901.2', '344.5', '537.8', '564', '621.2'],
].map((row, idx) => (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
{row.map((val, i) => (
<td
key={i}
className={`${tableStyles.td} ${
i === 0 ? "!font-bold !text-neutral-900" : ""
} ${[1, 3, 5].includes(i) ? "bg-[#EAF5F5]" : ""}`}
>
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 5].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
{val}
</td>
))}
@@ -141,129 +107,79 @@ export default function RainStatusSection() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 2) 빗물이용시설의 용도별 분포 (2023년 기준) */}
<section
id="status-rain-purpose-distribution"
className="scroll-mt-[120px]"
>
<section id='status-rain-purpose-distribution' className='scroll-mt-[120px]'>
<SectionTitle>빗물이용시설의 용도별 분포 (2023년 기준)</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<thead>
<tr>
<th
rowSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
style={{ width: "160px" }}
>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
용도
</th>
<th
rowSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
style={{ width: "84px" }}
>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
개소수
<br />
(개소)
</th>
<th
rowSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
style={{ width: "84px" }}
>
저류용량
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
저류조용량
<br />
(천m³)
</th>
<th
colSpan={8}
className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}
>
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
빗물이용량 (천m³/년)
</th>
</tr>
<tr>
<th
className={`${tableStyles.th} bg-[#01ACE5] py-2 text-[13px] text-white`}
style={{ width: "84px" }}
>
<th className={`${tableStyles.th} bg-[#01ACE5] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
총사용량
</th>
<th
className={`${tableStyles.th} bg-[#10AED0] py-2 text-[13px] text-white`}
style={{ width: "84px" }}
>
<th className={`${tableStyles.th} bg-[#10AED0] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
공공시설
</th>
<th
className={`${tableStyles.th} bg-[#20B0BA] py-2 text-[13px] text-white`}
style={{ width: "84px" }}
>
<th className={`${tableStyles.th} bg-[#20B0BA] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
공동주택
</th>
<th
className={`${tableStyles.th} bg-[#2FB2A5] py-2 text-[13px] text-white`}
style={{ width: "84px" }}
>
<th className={`${tableStyles.th} bg-[#2FB2A5] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
대규모점포
</th>
<th
className={`${tableStyles.th} bg-[#3FB58F] py-2 text-[13px] text-white`}
style={{ width: "84px" }}
>
<th className={`${tableStyles.th} bg-[#3FB58F] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
체육시설
</th>
<th
className={`${tableStyles.th} bg-[#4EB77A] py-2 text-[13px] text-white`}
style={{ width: "84px" }}
>
<th className={`${tableStyles.th} bg-[#4EB77A] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
학교
</th>
<th
className={`${tableStyles.th} bg-[#5EB964] py-2 text-[13px] text-white`}
style={{ width: "84px" }}
>
<th className={`${tableStyles.th} bg-[#5EB964] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
골프장
</th>
<th
className={`${tableStyles.th} bg-[#6DBB4F] py-2 text-[13px] text-white`}
style={{ width: "84px" }}
>
<th className={`${tableStyles.th} bg-[#6DBB4F] py-2 text-[13px] text-white`} style={{ width: '84px' }}>
기타
</th>
</tr>
</thead>
<tbody>
{[
["청소·환경정화용수", "195", "32.7", "108.8", "24.9", "5.8", "2.6", "4.4", "2.9", "-", "68.1"],
["세척·살수용수", "12", "1.4", "37", "35", "-", "-", "1.9", "-", "-", "0.2"],
["조경용수", "3,165", "7,440.8", "13,554.1", "168.1", "2,421.9", "290.7", "66.2", "29", "10,044", "534.3"],
["냉·난방수", "8", "1.6", "0.1", "-", "-", "-", "-", "0.1", "-", "-"],
["공업용수", "6", "14.9", "53.2", "52.8", "-", "-", "-", "-", "-", "0.4"],
["농업용수", "6.0", "0.5", "0.6", "-", "-", "-", "-", "-", "-", "0.6"],
["기타", "48", "306.1", "155.6", "2.8", "-", "-", "0.6", "0.5", "147.5", "4.2"],
["미기재", "103", "22.9", "34.8", "16.0", "0.6", "-", "4.6", "0.2", "-", "13.5"],
["합계", "3,543", "7,820.8", "13,944.7", "299.6", "2,428.4", "293.3", "77.6", "32.6", "10,191.6", "621.2"],
['청소·환경정화용수', '195', '32.7', '108.8', '24.9', '5.8', '2.6', '4.4', '2.9', '-', '68.1'],
['세척·살수용수', '12', '1.4', '37', '35', '-', '-', '1.9', '-', '-', '0.2'],
['조경용수', '3,165', '7,440.8', '13,554.1', '168.1', '2,421.9', '290.7', '66.2', '29', '10,044', '534.3'],
['냉·난방수', '8', '1.6', '0.1', '-', '-', '-', '-', '0.1', '-', '-'],
['공업용수', '6', '14.9', '53.2', '52.8', '-', '-', '-', '-', '-', '0.4'],
['농업용수', '6.0', '0.5', '0.6', '-', '-', '-', '-', '-', '-', '0.6'],
['기타', '48', '306.1', '155.6', '2.8', '-', '-', '0.6', '0.5', '147.5', '4.2'],
['미기재', '103', '22.9', '34.8', '16.0', '0.6', '-', '4.6', '0.2', '-', '13.5'],
['합계', '3,543', '7,820.8', '13,944.7', '299.6', '2,428.4', '293.3', '77.6', '32.6', '10,191.6', '621.2'],
].map((row, idx) => (
<tr
key={idx}
className={idx === 8 ? "bg-white font-bold" : "bg-white"}
>
<tr key={idx} className={idx === 8 ? 'bg-white font-bold' : 'bg-white'}>
{row.map((val, i) => {
const isHighlightedCol = [1, 2].includes(i);
return (
<td
key={i}
className={`${tableStyles.td} ${
i === 0 ? "!font-bold !text-neutral-900" : ""
} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
>
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${isHighlightedCol ? 'bg-[#EAF5F5]' : ''}`}>
{val}
</td>
);
@@ -278,91 +194,58 @@ export default function RainStatusSection() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
<section
id="status-rain-purpose-usage"
className="scroll-mt-[120px]"
>
<section id='status-rain-purpose-usage' className='scroll-mt-[120px]'>
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<thead>
<tr>
<th
rowSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
style={{ width: "160px" }}
>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
용도
</th>
<th
colSpan={6}
className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}
>
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
빗물이용량 (천m³/년)
</th>
</tr>
<tr>
<th
className={`${tableStyles.th} bg-[#01ACE5] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#01ACE5] text-white`} style={{ width: '140px' }}>
2018
</th>
<th
className={`${tableStyles.th} bg-[#14AFCD] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#14AFCD] text-white`} style={{ width: '140px' }}>
2019
</th>
<th
className={`${tableStyles.th} bg-[#28B2B4] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#28B2B4] text-white`} style={{ width: '140px' }}>
2020
</th>
<th
className={`${tableStyles.th} bg-[#3CB59A] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#3CB59A] text-white`} style={{ width: '140px' }}>
2021
</th>
<th
className={`${tableStyles.th} bg-[#50B880] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#50B880] text-white`} style={{ width: '140px' }}>
2022
</th>
<th
className={`${tableStyles.th} bg-[#6DBB4F] text-white`}
style={{ width: "140px" }}
>
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white`} style={{ width: '140px' }}>
2023
</th>
</tr>
</thead>
<tbody>
{[
["청소·화장실용수", "126.5", "134.1", "96.5", "153.9", "140.5", "108.8"],
["세척·살수용수", "14.8", "14.6", "9.5", "49.8", "49.7", "37.0"],
["조경용수", "7,629.8", "8,496.2", "9,415.4", "13,360.4", "14,466.3", "13,554.1"],
["친수용수", "1.0", "0.1", "0.1", "0.1", "0.1", "0.1"],
["공업용수", "152.0", "77.0", "77.0", "75.4", "0.4", "53.2"],
["농업용수", "4.4", "4.3", "4.3", "4.3", "0.6", "0.6"],
["기타", "37.7", "42.6", "230.7", "394.3", "463.2", "155.6"],
['청소·화장실용수', '126.5', '134.1', '96.5', '153.9', '140.5', '108.8'],
['세척·살수용수', '14.8', '14.6', '9.5', '49.8', '49.7', '37.0'],
['조경용수', '7,629.8', '8,496.2', '9,415.4', '13,360.4', '14,466.3', '13,554.1'],
['친수용수', '1.0', '0.1', '0.1', '0.1', '0.1', '0.1'],
['공업용수', '152.0', '77.0', '77.0', '75.4', '0.4', '53.2'],
['농업용수', '4.4', '4.3', '4.3', '4.3', '0.6', '0.6'],
['기타', '37.7', '42.6', '230.7', '394.3', '463.2', '155.6'],
].map((row, idx) => (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
{row.map((val, i) => (
<td
key={i}
className={`${tableStyles.td} ${
i === 0 ? "!font-bold !text-neutral-900" : ""
} ${[1, 3, 5].includes(i) ? "bg-[#EAF5F5]" : ""}`}
>
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 5].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
{val}
</td>
))}
@@ -378,4 +261,4 @@ export default function RainStatusSection() {
</div>
</div>
);
}
}
@@ -1,60 +1,56 @@
"use client";
'use client';
import React from "react";
import React from 'react';
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
<div className='flex items-center gap-3'>
<span className='h-5 w-[3px] bg-sky-500' />
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
</div>
);
}
function Hr({ className = "" }: { className?: string }) {
function Hr({ className = '' }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
function SourceText() {
return (
<p className="mt-7 text-center text-[13px] text-neutral-500">
자료: 환경부(2024), 「2023 하수도 통계」
</p>
);
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 환경부(2024), 「2023 하수도 통계」</p>;
}
const tableStyles = {
container: "w-[1000px] mx-auto overflow-hidden",
table: "w-full table-fixed border-collapse text-center text-[16px]",
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
headerBg: "bg-[#E3E7EA]",
container: 'w-[1000px] mx-auto overflow-hidden',
table: 'w-full table-fixed border-collapse text-center text-[16px]',
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
headerBg: 'bg-[#E3E7EA]',
};
export default function SewageStatusSection() {
const title = "하수처리수 재이용시설(현황)";
const title = '하수처리수 재이용시설(현황)';
return (
<div className="relative bg-white">
<div className='relative 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" />
<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>
</div>
<div className="mt-10">
<div className='mt-10'>
{/* 1) 연도별 하수처리수 재이용 현황 */}
<section id="status-treated-year" className="mt-10 scroll-mt-[120px]">
<section id='status-treated-year' className='mt-10 scroll-mt-[120px]'>
<SectionTitle>연도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-10">
<div className='mt-10'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: "130px" }} />
<col style={{ width: "180px" }} />
<col style={{ width: '130px' }} />
<col style={{ width: '180px' }} />
<col />
<col />
<col />
@@ -64,166 +60,52 @@ export default function SewageStatusSection() {
</colgroup>
<thead>
<tr>
<th
colSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
<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>
<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: "전체",
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",
],
["개소수 <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: "법적 대상",
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",
],
["개소수 <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: "법적 미대상",
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",
],
["개소수 <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">
<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`}
>
<td rowSpan={section.rows.length} className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}>
{section.group}
</td>
)}
@@ -234,9 +116,7 @@ export default function SewageStatusSection() {
return (
<td
key={i}
className={`${tableStyles.td} ${
isFirstCol ? "!font-bold !text-neutral-900" : ""
} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
className={`${tableStyles.td} ${isFirstCol ? '!font-bold !text-neutral-900' : ''} ${isHighlightedCol ? 'bg-[#EAF5F5]' : ''}`}
dangerouslySetInnerHTML={{ __html: val }}
/>
);
@@ -253,17 +133,17 @@ export default function SewageStatusSection() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 2) 용도별 하수처리수 재이용 현황 */}
<section id="status-treated-usage" className="scroll-mt-[120px]">
<section id='status-treated-usage' className='scroll-mt-[120px]'>
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: "180px" }} />
<col style={{ width: '180px' }} />
<col />
<col />
<col />
@@ -273,88 +153,59 @@ export default function SewageStatusSection() {
</colgroup>
<thead>
<tr>
<th 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>
<th 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>
{[
{ label: "총 재이용량", type: "total", values: ["1,112.7", "1,148.7", "1,144.7", "1,127.0", "1,136.8", "1,137.5"] },
{ label: "장내 재이용", type: "category", values: ["520.6", "519.8", "513.9", "520.3", "502.2", "490.0"] },
{ label: "세척수", type: "red-item", values: ["177.9", "174.4", "169.4", "180.9", "196.8", "191.2"] },
{ label: "냉각수", type: "red-item", values: ["45.5", "46.8", "50.4", "40.6", "38.3", "41.7"] },
{ label: "청소수", type: "red-item", values: ["17.6", "21.2", "21.2", "21.7", "20.8", "20.5"] },
{ label: "조경용수", type: "red-item", values: ["0.1", "0.1", "-", "-", "-", "-"] },
{ label: "회생용수", type: "red-item", values: ["15.8", "16.7", "17.8", "15.7", "14.4", "14.0"] },
{ label: "장내급수도", type: "red-item", values: ["2.4", "-", "-", "-", "-", "-"] },
{ label: "장내기타용수", type: "red-item", values: ["261.3", "260.6", "255.0", "261.1", "231.8", "222.6"] },
{ label: "장외 재이용", type: "category", values: ["592.1", "628.9", "630.8", "606.7", "634.6", "647.5"] },
{ label: "청소·화장실용수", type: "blue-item", values: ["0.6", "0.4", "0.4", "0.4", "0.4", "0.4"] },
{ label: "세척·살수용수", type: "blue-item", values: ["5.6", "9.9", "2.9", "2.1", "3.0", "3.6"] },
{ label: "조경용수", type: "blue-item", values: ["10.3", "16.4", "5.9", "4.5", "8.9", "9.5"] },
{ label: "친수용수", type: "blue-item", values: ["7.8", "3.1", "3.1", "3.0", "10.9", "10.5"] },
{ label: "하천유지용수", type: "blue-item", values: ["480.5", "482.5", "496.2", "442.7", "471.7", "484.0"] },
{ label: "농업용수", type: "blue-item", values: ["12.2", "11.9", "13.4", "10.7", "10.6", "11.4"] },
{ label: "지하수 충전", type: "blue-item", values: ["0.0", "0.0", "0.0", "13.4", "0.0", "0.0"] },
{ label: "공업용수", type: "blue-item", values: ["75.2", "104.6", "108.9", "129.9", "129.0", "128.1"] },
{ label: '총 재이용량', type: 'total', values: ['1,112.7', '1,148.7', '1,144.7', '1,127.0', '1,136.8', '1,137.5'] },
{ label: '장내 재이용', type: 'category', values: ['520.6', '519.8', '513.9', '520.3', '502.2', '490.0'] },
{ label: '세척수', type: 'red-item', values: ['177.9', '174.4', '169.4', '180.9', '196.8', '191.2'] },
{ label: '냉각수', type: 'red-item', values: ['45.5', '46.8', '50.4', '40.6', '38.3', '41.7'] },
{ label: '청소수', type: 'red-item', values: ['17.6', '21.2', '21.2', '21.7', '20.8', '20.5'] },
{ label: '조경용수', type: 'red-item', values: ['0.1', '0.1', '-', '-', '-', '-'] },
{ label: '회생용수', type: 'red-item', values: ['15.8', '16.7', '17.8', '15.7', '14.4', '14.0'] },
{ label: '장내급수도', type: 'red-item', values: ['2.4', '-', '-', '-', '-', '-'] },
{ label: '장내기타용수', type: 'red-item', values: ['261.3', '260.6', '255.0', '261.1', '231.8', '222.6'] },
{ label: '장외 재이용', type: 'category', values: ['592.1', '628.9', '630.8', '606.7', '634.6', '647.5'] },
{ label: '청소·화장실용수', type: 'blue-item', values: ['0.6', '0.4', '0.4', '0.4', '0.4', '0.4'] },
{ label: '세척·살수용수', type: 'blue-item', values: ['5.6', '9.9', '2.9', '2.1', '3.0', '3.6'] },
{ label: '조경용수', type: 'blue-item', values: ['10.3', '16.4', '5.9', '4.5', '8.9', '9.5'] },
{ label: '친수용수', type: 'blue-item', values: ['7.8', '3.1', '3.1', '3.0', '10.9', '10.5'] },
{ label: '하천유지용수', type: 'blue-item', values: ['480.5', '482.5', '496.2', '442.7', '471.7', '484.0'] },
{ label: '농업용수', type: 'blue-item', values: ['12.2', '11.9', '13.4', '10.7', '10.6', '11.4'] },
{ label: '지하수 충전', type: 'blue-item', values: ['0.0', '0.0', '0.0', '13.4', '0.0', '0.0'] },
{ label: '공업용수', type: 'blue-item', values: ['75.2', '104.6', '108.9', '129.9', '129.0', '128.1'] },
].map((row, idx) => {
const isSubItem = row.type === "red-item" || row.type === "blue-item";
const isSubItem = row.type === 'red-item' || row.type === 'blue-item';
const isLastInGroup = idx === 8 || idx === 17;
return (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
<td className={`${tableStyles.td} relative`}>
{isSubItem ? (
<>
<div className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
<div
className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${
isLastInGroup ? "bottom-[0px]" : "bottom-[-2px]"
className={`absolute left-[16px] w-[4px] z-20 ${row.type === 'red-item' ? 'bg-[#FF8B8B]' : 'bg-[#4ECBFF]'} top-[-2px] ${
isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'
}`}
/>
<div
className={`absolute left-[16px] w-[4px] z-20 ${
row.type === "red-item" ? "bg-[#FF8B8B]" : "bg-[#4ECBFF]"
} top-[-2px] ${
isLastInGroup ? "bottom-[0px]" : "bottom-[-2px]"
}`}
/>
<span className="text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]">
{row.label}
</span>
<span className='text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]'>{row.label}</span>
</>
) : (
<span className="font-bold text-neutral-900 block w-full text-center">
{row.label}
</span>
<span className='font-bold text-neutral-900 block w-full text-center'>{row.label}</span>
)}
</td>
{row.values.map((val, i) => (
<td
key={i}
className={`${tableStyles.td} ${
[0, 2, 4].includes(i) ? "bg-[#EAF5F5]" : ""
}`}
>
<td key={i} className={`${tableStyles.td} ${[0, 2, 4].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
{val}
</td>
))}
@@ -369,18 +220,18 @@ export default function SewageStatusSection() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 3) 하수처리시설 설치 및 이용 현황 */}
<section id="status-treated-facility" className="scroll-mt-[120px]">
<section id='status-treated-facility' className='scroll-mt-[120px]'>
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: "130px" }} />
<col style={{ width: "160px" }} />
<col style={{ width: '130px' }} />
<col style={{ width: '160px' }} />
<col />
<col />
<col />
@@ -390,139 +241,49 @@ export default function SewageStatusSection() {
</colgroup>
<thead>
<tr>
<th
colSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
<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>
<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: "전체",
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>",
"25,846",
"26,067",
"26,244",
"26,492",
"26,586",
"26,766",
],
[
"처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>",
"1,112.7",
"1,148.7",
"1,144.7",
"1,127.0",
"1,136.8",
"1,137.5",
],
["시설수 <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>", '25,846', '26,067', '26,244', '26,492', '26,586', '26,766'],
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '1,112.7', '1,148.7', '1,144.7', '1,127.0', '1,136.8', '1,137.5'],
],
},
{
group: "법적 대상",
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>",
"3,088.9",
"3,359.2",
"3,612.6",
"4,412.2",
"4,692.3",
"5,503.8",
],
[
"처리용량 <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>", '68', '82', '94', '97', '101', '118'],
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '3,088.9', '3,359.2', '3,612.6', '4,412.2', '4,692.3', '5,503.8'],
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '162.2', '190.6', '218.5', '244.6', '247.2', '324.9'],
],
},
{
group: "법적 미대상",
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>",
"22,757",
"22,708",
"22,631",
"22,080",
"21,894",
"21,262",
],
[
"처리용량 <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>", '603', '599', '604', '607', '613', '599'],
["시설용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '22,757', '22,708', '22,631', '22,080', '21,894', '21,262'],
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '950.6', '958.1', '926.2', '882.4', '889.5', '812.7'],
],
},
].map((section, sIdx) => (
<React.Fragment key={sIdx}>
{section.rows.map((row, rIdx) => (
<tr key={`${sIdx}-${rIdx}`} className="bg-white">
<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`}
>
<td rowSpan={section.rows.length} className={`${tableStyles.td} ${tableStyles.headerBg} !text-[18px] !font-bold !text-neutral-900`}>
{section.group}
</td>
)}
@@ -533,9 +294,7 @@ export default function SewageStatusSection() {
return (
<td
key={i}
className={`${tableStyles.td} ${
isFirstCol ? "!font-bold !text-neutral-900" : ""
} ${isHighlightedCol ? "bg-[#EAF5F5]" : ""}`}
className={`${tableStyles.td} ${isFirstCol ? '!font-bold !text-neutral-900' : ''} ${isHighlightedCol ? 'bg-[#EAF5F5]' : ''}`}
dangerouslySetInnerHTML={{ __html: val }}
/>
);
@@ -552,24 +311,19 @@ export default function SewageStatusSection() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 4) 시도별 하수처리수 재이용 현황 */}
<section
id="status-treated-region"
className="scroll-mt-[120px]"
>
<section id='status-treated-region' className='scroll-mt-[120px]'>
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<div className="mb-2 text-right text-[13px] text-neutral-600">
(단위: 백만m³/일)
</div>
<div className='mb-2 text-right text-[13px] text-neutral-600'>(단위: 백만m³/년)</div>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: "160px" }} />
<col style={{ width: '160px' }} />
<col />
<col />
<col />
@@ -583,110 +337,71 @@ export default function SewageStatusSection() {
</colgroup>
<thead>
<tr>
<th
rowSpan={3}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
시도
</th>
<th
rowSpan={3}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
연간
<br />
하수
<br />
처리량
</th>
<th
colSpan={7}
className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}
>
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
재이용현황
</th>
<th
rowSpan={3}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
재이용률
<br />
(%)
</th>
<th
rowSpan={3}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
비고
</th>
</tr>
<tr>
<th
rowSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
계
</th>
<th
rowSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
장내
</th>
<th
colSpan={5}
className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}
>
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}>
장외
</th>
</tr>
<tr>
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>
소계
</th>
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>
하천유지용수
</th>
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>
공업용수
</th>
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>
농업용수
</th>
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>
기타용수
</th>
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>소계</th>
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>하천유지용수</th>
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>공업용수</th>
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>농업용수</th>
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>기타용수</th>
</tr>
</thead>
<tbody>
{[
["합계", "7,537.2", "1,137.5", "490.0", "647.5", "484.0", "128.1", "11.4", "24.0", "15.1%", ""],
["서울특별시", "1,567.0", "114.9", "59.3", "55.6", "55.1", "-", "-", "0.5", "7.3%", ""],
["부산광역시", "491.5", "146.2", "120.7", "25.5", "25.3", "-", "-", "0.2", "29.7%", ""],
["대구광역시", "432.7", "102.4", "38.2", "64.2", "64.1", "-", "-", "0.1", "23.7%", ""],
["인천광역시", "302.3", "57.7", "11.6", "46.1", "29.2", "12.1", "0.5", "4.3", "19.1%", ""],
["광주광역시", "211.1", "28.7", "6.5", "22.3", "22.2", "-", "-", "0.1", "13.6%", ""],
["대전광역시", "244.6", "11.9", "11.9", "0.68", "0.0", "-", "-", "0.1", "4.9%", ""],
["울산광역시", "235.1", "36.1", "6.1", "29.9", "29.7", "0.2", "-", "-", "15.3%", ""],
["세종특별자치시", "38.5", "2.5", "2.0", "0.5", "0.4", "-", "0.2", "-", "6.6%", ""],
["경기도", "1,926.8", "313.3", "122.2", "191.0", "153.0", "33.3", "0.4", "4.3", "16.3%", ""],
["강원도", "230.4", "19.6", "15.4", "4.2", "2.1", "-", "-", "2.1", "8.5%", ""],
["충청북도", "190.9", "15.6", "6.3", "9.2", "7.4", "0.6", "-", "1.2", "8.2%", ""],
["충청남도", "241.5", "41.4", "9.8", "31.5", "17.7", "8.6", "4.5", "0.8", "17.1%", ""],
["전라북도", "292.4", "42.7", "7.5", "35.1", "33.5", "0.2", "1.0", "0.5", "14.6%", ""],
["전라남도", "254.0", "23.6", "12.3", "11.3", "8.7", "0.0", "1.0", "1.6", "9.3%", ""],
["경상북도", "378.4", "129.1", "33.7", "95.3", "20.8", "72.5", "0.8", "1.3", "34.1%", ""],
["경상남도", "411.3", "50.1", "25.0", "25.0", "14.7", "0.5", "3.0", "6.9", "12.2%", ""],
["제주특별자치도", "88.6", "1.7", "1.5", "0.12", "-", "-", "0.1", "-", "1.9%", ""],
['합계', '7,537.2', '1,137.5', '490.0', '647.5', '484.0', '128.1', '11.4', '24.0', '15.1%', ''],
['서울특별시', '1,567.0', '114.9', '59.3', '55.6', '55.1', '-', '-', '0.5', '7.3%', ''],
['부산광역시', '491.5', '146.2', '120.7', '25.5', '25.3', '-', '-', '0.2', '29.7%', ''],
['대구광역시', '432.7', '102.4', '38.2', '64.2', '64.1', '-', '-', '0.1', '23.7%', ''],
['인천광역시', '302.3', '57.7', '11.6', '46.1', '29.2', '12.1', '0.5', '4.3', '19.1%', ''],
['광주광역시', '211.1', '28.7', '6.5', '22.3', '22.2', '-', '-', '0.1', '13.6%', ''],
['대전광역시', '244.6', '11.9', '11.9', '0.68', '0.0', '-', '-', '0.1', '4.9%', ''],
['울산광역시', '235.1', '36.1', '6.1', '29.9', '29.7', '0.2', '-', '-', '15.3%', ''],
['세종특별자치시', '38.5', '2.5', '2.0', '0.5', '0.4', '-', '0.2', '-', '6.6%', ''],
['경기도', '1,926.8', '313.3', '122.2', '191.0', '153.0', '33.3', '0.4', '4.3', '16.3%', ''],
['강원도', '230.4', '19.6', '15.4', '4.2', '2.1', '-', '-', '2.1', '8.5%', ''],
['충청북도', '190.9', '15.6', '6.3', '9.2', '7.4', '0.6', '-', '1.2', '8.2%', ''],
['충청남도', '241.5', '41.4', '9.8', '31.5', '17.7', '8.6', '4.5', '0.8', '17.1%', ''],
['전라북도', '292.4', '42.7', '7.5', '35.1', '33.5', '0.2', '1.0', '0.5', '14.6%', ''],
['전라남도', '254.0', '23.6', '12.3', '11.3', '8.7', '0.0', '1.0', '1.6', '9.3%', ''],
['경상북도', '378.4', '129.1', '33.7', '95.3', '20.8', '72.5', '0.8', '1.3', '34.1%', ''],
['경상남도', '411.3', '50.1', '25.0', '25.0', '14.7', '0.5', '3.0', '6.9', '12.2%', ''],
['제주특별자치도', '88.6', '1.7', '1.5', '0.12', '-', '-', '0.1', '-', '1.9%', ''],
].map((row, idx) => (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
{row.map((val, i) => (
<td
key={i}
className={`${tableStyles.td} ${
i === 0 ? "!font-bold !text-neutral-900" : ""
} ${[1, 3, 9].includes(i) ? "bg-[#EAF5F5]" : ""}`}
>
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 9].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
{val}
</td>
))}
@@ -702,4 +417,4 @@ export default function SewageStatusSection() {
</div>
</div>
);
}
}
@@ -1,123 +1,92 @@
"use client";
'use client';
import React from "react";
import Image, { type StaticImageData } from "next/image";
import React from 'react';
import Image, { type StaticImageData } from 'next/image';
import lawPlan from "@/public/images/landing/intro/law_timeline.png";
import planPerformance from "@/public/images/landing/intro/performance_left.png";
import domesticChange from "@/public/images/landing/intro/domestic_textbox.png";
import policyHistory from "@/public/images/landing/intro/policy_timeline.png";
import lawRelation from "@/public/images/landing/intro/rel_note.png";
import domesticLinkage from "@/public/images/landing/intro/linkage.png";
import lawPlan from '@/public/images/landing/intro/law_timeline.png';
import planPerformance from '@/public/images/landing/intro/performance_left.png';
import domesticChange from '@/public/images/landing/intro/domestic_textbox.png';
import policyHistory from '@/public/images/landing/intro/policy_timeline.png';
import lawRelation from '@/public/images/landing/intro/rel_note.png';
import domesticLinkage from '@/public/images/landing/intro/linkage.png';
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
<div className='flex items-center gap-3'>
<span className='h-5 w-[3px] bg-sky-500' />
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
</div>
);
}
function Hr({ className = "" }: { className?: string }) {
function Hr({ className = '' }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
function ImgCard({ src, alt }: { src: StaticImageData; alt: string }) {
return (
<div className="w-full overflow-x-auto">
<Image
src={src}
alt={alt}
className="mx-auto block max-w-none"
style={{ width: "auto", height: "auto" }}
/>
<div className='w-full overflow-x-auto'>
<Image src={src} alt={alt} className='mx-auto block max-w-none' style={{ width: 'auto', height: 'auto' }} />
</div>
);
}
function SectionShell({
id,
title,
children,
className = "",
}: {
id: string;
title: string;
children: React.ReactNode;
className?: string;
}) {
function SectionShell({ id, title, children, className = '' }: { id: string; title: string; children: React.ReactNode; className?: string }) {
return (
<section id={id} className={`scroll-mt-[120px] ${className}`}>
<SectionTitle>{title}</SectionTitle>
<div className="mt-8">{children}</div>
<div className='mt-8'>{children}</div>
</section>
);
}
export default function LawIntroSection() {
const title = "물 재이용 법";
const title = '물 재이용 법';
return (
<div className="bg-white">
<div className="inline-block">
<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" />
<div className='bg-white'>
<div className='inline-block'>
<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">
<SectionShell
id="law-plan"
title="법률 제·개정 및 기본계획 수립"
className="mt-10"
>
<ImgCard src={lawPlan} alt="법률 제·개정 및 기본계획 수립" />
<div className='mt-10'>
<SectionShell id='law-plan' title='법률 제·개정 및 기본계획 수립' className='mt-10'>
<ImgCard src={lawPlan} alt='법률 제·개정 및 기본계획 수립' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell
id="plan-performance"
title="제2차 계획 대비 실적 현황"
>
<ImgCard src={planPerformance} alt="제2차 계획 대비 실적 현황" />
<SectionShell id='plan-performance' title='제2차 계획 대비 실적 현황'>
<ImgCard src={planPerformance} alt='제2차 계획 대비 실적 현황' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell id="domestic-change" title="우리나라의 수자원 현황 및 물이용 여건">
<ImgCard src={domesticChange} alt="우리나라의 수자원 현황 및 물이용 여건" />
<SectionShell id='domestic-change' title='우리나라의 수자원 현황 및 물이용 여건'>
<ImgCard src={domesticChange} alt='우리나라의 수자원 현황 및 물이용 여건' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell
id="policy-history"
title="물 재이용 정책 변화 추이"
>
<ImgCard src={policyHistory} alt="물 재이용 정책 변화 추이" />
<SectionShell id='policy-history' title='물 재이용 정책 변화 추이'>
<ImgCard src={policyHistory} alt='물 재이용 정책 변화 추이' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell
id="law-relation"
title="타법령과의 관계 검토"
>
<ImgCard src={lawRelation} alt="타법령과의 관계 검토" />
<SectionShell id='law-relation' title='타법령과의 관계 검토'>
<ImgCard src={lawRelation} alt='타법령과의 관계 검토' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell
id="domestic-linkage"
title="주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요"
>
<ImgCard src={domesticLinkage} alt="주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요" />
<SectionShell id='domestic-linkage' title='주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요'>
<ImgCard src={domesticLinkage} alt='주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요' />
</SectionShell>
</div>
</div>
);
}
}
+44 -52
View File
@@ -1,20 +1,20 @@
// apps/dm/app/(landing)/reuse/intro/law/page.tsx
"use client";
'use client';
import Image, { type StaticImageData } from "next/image";
import SubPageHeader from "../../_components/SubPageHeader";
import lawTimeline from "@/public/images/landing/intro/law_timeline.png";
import performanceLeft from "@/public/images/landing/intro/performance_left.png";
import domesticTextbox from "@/public/images/landing/intro/domestic_textbox.png";
import policyTimeline from "@/public/images/landing/intro/policy_timeline.png";
import relNote from "@/public/images/landing/intro/rel_note.png";
import linkage from "@/public/images/landing/intro/linkage.png";
import Image, { type StaticImageData } from 'next/image';
import SubPageHeader from '../../_components/SubPageHeader';
import lawTimeline from '@/public/images/landing/intro/law_timeline.png';
import performanceLeft from '@/public/images/landing/intro/performance_left.png';
import domesticTextbox from '@/public/images/landing/intro/domestic_textbox.png';
import policyTimeline from '@/public/images/landing/intro/policy_timeline.png';
import relNote from '@/public/images/landing/intro/rel_note.png';
import linkage from '@/public/images/landing/intro/linkage.png';
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
<div className='flex items-center gap-3'>
<span className='h-5 w-[3px] bg-sky-500' />
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
</div>
);
}
@@ -23,7 +23,7 @@ type HrProps = {
className?: string;
};
function Hr({ className = "" }: HrProps) {
function Hr({ className = '' }: HrProps) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
@@ -34,95 +34,87 @@ type ImgCardProps = {
function ImgCard({ src, alt }: ImgCardProps) {
return (
<div className="w-full overflow-x-auto">
<Image
src={src}
alt={alt}
className="block mx-auto max-w-none"
style={{ width: "auto", height: "auto" }}
/>
<div className='w-full overflow-x-auto'>
<Image src={src} alt={alt} className='block mx-auto max-w-none' style={{ width: 'auto', height: 'auto' }} />
</div>
);
}
export default function WaterReuseLawPage() {
return (
<div className="bg-white">
<div className='bg-white'>
<SubPageHeader
title="물 재이용 법"
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "물 재이용 소개", href: "/reuse/intro/law" },
{ label: "물 재이용 법" },
]}
title='물 재이용 법'
crumbs={[{ label: 'Home', href: '/reuse' }, { label: '물 재이용 소개', href: '/reuse/intro/law' }, { label: '물 재이용 법' }]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
wrapClassName='!pb-0'
/>
<Hr className="my-7" />
<Hr className='my-7' />
{/* 1) 법제도/계획 수립 타임라인 */}
<section className="mt-6">
<section className='mt-6'>
<SectionTitle>법제도 · 계획 및 기본계획 수립</SectionTitle>
<div className="mt-8">
<ImgCard src={lawTimeline} alt="법제도 타임라인" />
<div className='mt-8'>
<ImgCard src={lawTimeline} alt='법제도 타임라인' />
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 2) 제2차 계획 대비 실적 현황 (2컬럼) */}
<section>
<SectionTitle>제 2차 계획 대비 실적 현황</SectionTitle>
<div className="mt-8">
<ImgCard src={performanceLeft} alt="실적 현황 요약" />
<div className='mt-8'>
<ImgCard src={performanceLeft} alt='실적 현황 요약' />
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 3) 국내 관련 현황 */}
<section>
<SectionTitle>국내 관련 현황</SectionTitle>
<div className="mt-8">
<ImgCard src={domesticTextbox} alt="국내 관련 현황 설명" />
<div className='mt-8'>
<ImgCard src={domesticTextbox} alt='국내 관련 현황 설명' />
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 4) 정책 변천 추이 */}
<section>
<SectionTitle>물 재이용 정책 변천 추이</SectionTitle>
<div className="mt-8">
<ImgCard src={policyTimeline} alt="정책 변천 타임라인" />
<div className='mt-8'>
<ImgCard src={policyTimeline} alt='정책 변천 타임라인' />
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 5) 타 법령과의 관계도 */}
<section>
<SectionTitle>타법령과의 관계도</SectionTitle>
<div className="mt-8">
<ImgCard src={relNote} alt="관계도 설명" />
<div className='mt-8'>
<ImgCard src={relNote} alt='관계도 설명' />
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 6) 국내 연계 현황 */}
<section className="pb-[200px]">
<section className='pb-[200px]'>
<SectionTitle>국내 연계 현황</SectionTitle>
<div className="mt-8">
<div className="mx-[20px] mb-8">
<p className="text-[24px] font-semibold mb-2">주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요</p>
<p className="text-[#4F4F4F]">국가 상위계획의 정책 방향이 기후위기 대응 등 수자원의 공급 안전성을 위한 물 재이용 활성화로 정책 실효성 제고 필요<br />
• 물관리 계획(국가수도기본계획, 국가 및 유역하수도종합계획, 하천유역수자원관리계획 등)과의 연계성 확보 → 통합적인 관점에서 물이용 체계 구축 필요
<div className='mt-8'>
<div className='mx-[20px] mb-8'>
<p className='text-[24px] font-semibold mb-2'>주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요</p>
<p className='text-[#4F4F4F]'>
국가 상위계획의 정책 방향이 기후위기 대응 등 수자원의 공급 안전성을 위한 물 재이용 활성화로 정책 실효성 제고 필요
<br />• 물관리 계획(국가수도기본계획, 국가 및 유역하수도종합계획, 하천유역수자원관리계획 등)과의 연계성 확보 → 통합적인 관점에서 물이용 체계 구축 필요
</p>
</div>
<ImgCard src={linkage} alt="국내 연계 현황" />
<ImgCard src={linkage} alt='국내 연계 현황' />
</div>
</section>
</div>
@@ -1,4 +1,4 @@
"use client";
'use client';
export default function LawIntroSection() {
const contentsHtml = `
@@ -84,10 +84,5 @@ export default function LawIntroSection() {
</div>
`;
return (
<div
className="bg-white"
dangerouslySetInnerHTML={{ __html: contentsHtml }}
/>
);
return <div className='bg-white' dangerouslySetInnerHTML={{ __html: contentsHtml }} />;
}
+47 -89
View File
@@ -1,22 +1,22 @@
// DM 물 재이용 소개(물 재이용 법) 페이지
"use client";
'use client';
import Image, { type StaticImageData } from "next/image";
import SubPageHeader from "@/components/SubPageHeader";
import Image, { type StaticImageData } from 'next/image';
import SubPageHeader from '@/components/SubPageHeader';
// import BusinessSubNav from "@/components/BusinessSubNav";
import lawTimeline from "@/public/images/landing/intro/law_timeline.png";
import performanceLeft from "@/public/images/landing/intro/performance_left.png";
import domesticTextbox from "@/public/images/landing/intro/domestic_textbox.png";
import policyTimeline from "@/public/images/landing/intro/policy_timeline.png";
import relNote from "@/public/images/landing/intro/rel_note.png";
import linkage from "@/public/images/landing/intro/linkage.png";
import lawTimeline from '@/public/images/landing/intro/law_timeline.png';
import performanceLeft from '@/public/images/landing/intro/performance_left.png';
import domesticTextbox from '@/public/images/landing/intro/domestic_textbox.png';
import policyTimeline from '@/public/images/landing/intro/policy_timeline.png';
import relNote from '@/public/images/landing/intro/rel_note.png';
import linkage from '@/public/images/landing/intro/linkage.png';
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
<div className='flex items-center gap-3'>
<span className='h-5 w-[3px] bg-sky-500' />
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
</div>
);
}
@@ -25,7 +25,7 @@ type HrProps = {
className?: string;
};
function Hr({ className = "" }: HrProps) {
function Hr({ className = '' }: HrProps) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
@@ -36,14 +36,8 @@ type ImgCardProps = {
function ImgCard({ src, alt }: ImgCardProps) {
return (
<div className="w-full overflow-x-auto">
<Image
src={src}
alt={alt}
className="mx-auto block max-w-none"
style={{ width: "auto", height: "auto" }}
priority={false}
/>
<div className='w-full overflow-x-auto'>
<Image src={src} alt={alt} className='mx-auto block max-w-none' style={{ width: 'auto', height: 'auto' }} priority={false} />
</div>
);
}
@@ -55,109 +49,73 @@ type SectionShellProps = {
className?: string;
};
function SectionShell({ id, title, children, className = "" }: SectionShellProps) {
function SectionShell({ id, title, children, className = '' }: SectionShellProps) {
return (
<section
id={id}
className={[
"scroll-mt-[140px]",
className,
].join(" ")}
>
<section id={id} className={['scroll-mt-[140px]', className].join(' ')}>
<SectionTitle>{title}</SectionTitle>
<div className="mt-8">{children}</div>
<div className='mt-8'>{children}</div>
</section>
);
}
export default function WaterReuseLawPage() {
return (
<div className="relative bg-white">
<div className='relative bg-white'>
{/* <BusinessSubNav /> */}
<SubPageHeader
title="물 재이용 법"
crumbs={[
{ label: "Home", href: "/main" },
{ label: "물 재이용 소개", href: "/intro/law" },
{ label: "물 재이용 법" },
]}
title='물 재이용 법'
crumbs={[{ label: 'Home', href: '/main' }, { label: '물 재이용 소개', href: '/intro/law' }, { label: '물 재이용 법' }]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
wrapClassName='!pb-0'
/>
<div className="pr-0">
<Hr className="my-7" />
<div className='pr-0'>
<Hr className='my-7' />
<SectionShell
id="law-plan"
title="법률 제·개정 및 기본계획 수립"
className="mt-6"
>
<ImgCard src={lawTimeline} alt="법률 제·개정 및 기본계획 수립" />
<SectionShell id='law-plan' title='법률 제·개정 및 기본계획 수립' className='mt-6'>
<ImgCard src={lawTimeline} alt='법률 제·개정 및 기본계획 수립' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell
id="plan-performance"
title="제2차 계획 대비 실적 현황"
>
<ImgCard src={performanceLeft} alt="제2차 계획 대비 실적 현황" />
<SectionShell id='plan-performance' title='제2차 계획 대비 실적 현황'>
<ImgCard src={performanceLeft} alt='제2차 계획 대비 실적 현황' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell
id="domestic-change"
title="국내 여건 변화"
>
<ImgCard src={domesticTextbox} alt="국내 여건 변화" />
<SectionShell id='domestic-change' title='국내 여건 변화'>
<ImgCard src={domesticTextbox} alt='국내 여건 변화' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell
id="policy-history"
title="물 재이용 정책 변화 추이"
>
<ImgCard src={policyTimeline} alt="물 재이용 정책 변화 추이" />
<SectionShell id='policy-history' title='물 재이용 정책 변화 추이'>
<ImgCard src={policyTimeline} alt='물 재이용 정책 변화 추이' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell
id="law-relation"
title="타법령과의 관계 검토"
>
<ImgCard src={relNote} alt="타법령과의 관계 검토" />
<SectionShell id='law-relation' title='타법령과의 관계 검토'>
<ImgCard src={relNote} alt='타법령과의 관계 검토' />
</SectionShell>
<Hr className="my-11" />
<Hr className='my-11' />
<SectionShell
id="domestic-linkage"
title="국내 연계 현황"
className="pb-[250px]"
>
<div className="mx-[20px] mb-8">
<p className="mb-2 text-[24px] font-semibold">
주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요
</p>
<p className="text-[#4F4F4F]">
국가 상위계획의 정책 방향이 기후위기 대응 등 수자원의 공급 안전성을 위한
물 재이용 활성화로 정책 실효성 제고 필요
<br />
• 물관리 계획(국가수도기본계획, 국가 및 유역하수도종합계획,
하천유역수자원관리계획 등)과의 연계성 확보 → 통합적인 관점에서
물이용 체계 구축 필요
<SectionShell id='domestic-linkage' title='국내 연계 현황' className='pb-[250px]'>
<div className='mx-[20px] mb-8'>
<p className='mb-2 text-[24px] font-semibold'>주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요</p>
<p className='text-[#4F4F4F]'>
국가 상위계획의 정책 방향이 기후위기 대응 등 수자원의 공급 안전성을 위한 물 재이용 활성화로 정책 실효성 제고 필요
<br />• 물관리 계획(국가수도기본계획, 국가 및 유역하수도종합계획, 하천유역수자원관리계획 등)과의 연계성 확보 → 통합적인 관점에서 물이용 체계 구축 필요
</p>
</div>
<ImgCard src={linkage} alt="국내 연계 현황" />
<ImgCard src={linkage} alt='국내 연계 현황' />
</SectionShell>
</div>
</div>
);
}
}
@@ -1,30 +1,24 @@
"use client";
'use client';
import { useEffect, useMemo } from "react";
import { useEffect, useMemo } from 'react';
// import SubPageHeader from "@/components/SubPageHeader";
import Image from "next/image";
import { useDmStore } from "@/app/store";
import { formatUsage } from "@/app/common/transValueFormat";
import type {
DmStatusBildTpYearsType,
DmStatusUsgBildTpType,
DmStatusUsgYearsType,
TableHeaderType,
} from "@/app/store/dm-status/type";
import Image from 'next/image';
import { useDmStore } from '@/app/store';
import { formatUsage } from '@/app/common/transValueFormat';
import type { DmStatusBildTpYearsType, DmStatusUsgBildTpType, DmStatusUsgYearsType, TableHeaderType } from '@/app/store/dm-status/type';
// 공통 섹션 타이틀 컴포넌트
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
<div className='flex items-center gap-3'>
<span className='h-5 w-[3px] bg-sky-500' />
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
</div>
);
}
// 구분선
function Hr({ className = "" }: { className?: string }) {
function Hr({ className = '' }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
@@ -32,62 +26,49 @@ function Hr({ className = "" }: { className?: string }) {
function RainIntroSummary() {
const items = [
{
icon: "/images/landing/icon_chart.png",
title: "지속적인 이용량 증가",
desc: "2018년 약 797만m³이었던 빗물 이용량은 5년 사이 약 75% 증가하여 수자원 절약에 기여하고 있습니다.",
icon: '/images/landing/icon_chart.png',
title: '지속적인 이용량 증가',
desc: '2018년 약 797만m³이었던 빗물 이용량은 5년 사이 약 75% 증가하여 수자원 절약에 기여하고 있습니다.',
},
{
icon: "/images/landing/icon_hand.png",
title: "조경용수의 핵심 자원",
desc: "재이용되는 빗물의 약 97%가 조경용수로 활용되고 있으며 이는 공원, 아파트 단지 등의 녹지 관리에 중요한 역할을 합니다.",
icon: '/images/landing/icon_hand.png',
title: '조경용수의 핵심 자원',
desc: '재이용되는 빗물의 약 97%가 조경용수로 활용되고 있으며 이는 공원, 아파트 단지 등의 녹지 관리에 중요한 역할을 합니다.',
},
{
icon: "/images/landing/icon_building.png",
title: "민간·공공의 협력",
desc: "골프장과 같은 대규모 스포츠 시설은 물론, 최근에는 공동주택·아파트 등 내 이용량이 급증하며 일상생활 속 빗물 활용이 확산되는 추세입니다.",
icon: '/images/landing/icon_building.png',
title: '민간·공공의 협력',
desc: '골프장과 같은 대규모 스포츠 시설은 물론, 최근에는 공동주택·아파트 등 내 이용량이 급증하며 일상생활 속 빗물 활용이 확산되는 추세입니다.',
},
];
return (
<div className="mb-[70px] px-[160px]">
<div className="grid grid-cols-[1fr_400px] items-center gap-[60px]">
<div className='mb-[70px] px-[160px]'>
<div className='grid grid-cols-[1fr_400px] items-center gap-[60px]'>
{/* 좌측 */}
<div>
<h3 className="text-[40px] font-extrabold leading-[1.35] tracking-[-0.04em] text-neutral-900">
<h3 className='text-[40px] font-extrabold leading-[1.35] tracking-[-0.04em] text-neutral-900'>
기후위기 시대의 수자원 확보,
<br />
<span className="inline-block bg-gradient-to-b from-[#6DBB4F] to-[#01ACE5] bg-clip-text text-transparent">
빗물 재이용
</span>
이 앞장섭니다.
<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] tracking-[-0.04em] text-neutral-600">
<p className='mt-5 text-[16px] leading-[1.8] tracking-[-0.04em] text-neutral-600'>
2023년 기준 전국 3,500여 개 시설에서
<br />
연간 약 1,394만m³의 빗물을 소중한 자원으로 다시 활용하고 있습니다.
</p>
<div className="mt-10 space-y-8">
<div className='mt-10 space-y-8'>
{items.map((item) => (
<div key={item.title} className="flex gap-3 items-center">
<div className="flex h-12 w-12 shrink-0 items-center justify-center">
<Image
src={item.icon}
alt=""
width={48}
height={48}
className="object-contain"
/>
<div key={item.title} className='flex gap-3 items-center'>
<div className='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 tracking-tighter">
{item.desc}
</p>
<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 tracking-tighter'>{item.desc}</p>
</div>
</div>
))}
@@ -95,15 +76,8 @@ function RainIntroSummary() {
</div>
{/* 우측 이미지 */}
<div className="flex items-center justify-center">
<Image
src="/images/landing/rain_status_img.png"
alt="빗물 재이용 개념도"
width={400}
height={343}
className="h-auto w-full max-w-[400px] object-contain"
priority
/>
<div className='flex items-center justify-center'>
<Image src='/images/landing/rain_status_img.png' alt='빗물 재이용 개념도' width={400} height={343} className='h-auto w-full max-w-[400px] object-contain' priority />
</div>
</div>
</div>
@@ -111,31 +85,15 @@ function RainIntroSummary() {
}
const tableStyles = {
container: "w-[1000px] mx-auto overflow-hidden",
table: "w-full table-fixed border-collapse text-center text-[16px]",
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
headerBg: "bg-[#E3E7EA]",
container: 'w-[1000px] mx-auto overflow-hidden',
table: 'w-full table-fixed border-collapse text-center text-[16px]',
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
headerBg: 'bg-[#E3E7EA]',
};
const headerColors6 = [
"bg-[#01ACE5]",
"bg-[#17AFC7]",
"bg-[#2CB2A9]",
"bg-[#42B58B]",
"bg-[#57B86D]",
"bg-[#81C784]",
];
const headerColors8 = [
"bg-[#01ACE5]",
"bg-[#10AED0]",
"bg-[#20B0BA]",
"bg-[#2FB2A5]",
"bg-[#3FB58F]",
"bg-[#4EB77A]",
"bg-[#5EB964]",
"bg-[#6DBB4F]",
];
const headerColors6 = ['bg-[#01ACE5]', 'bg-[#17AFC7]', 'bg-[#2CB2A9]', 'bg-[#42B58B]', 'bg-[#57B86D]', 'bg-[#81C784]'];
const headerColors8 = ['bg-[#01ACE5]', 'bg-[#10AED0]', 'bg-[#20B0BA]', 'bg-[#2FB2A5]', 'bg-[#3FB58F]', 'bg-[#4EB77A]', 'bg-[#5EB964]', 'bg-[#6DBB4F]'];
export default function RainStatusSection() {
const getRainyByBildTpYears = useDmStore((state) => state.getRainyByBildTpYears);
const getRainyByUsgBildTp = useDmStore((state) => state.getRainyByUsgBildTp);
@@ -148,19 +106,11 @@ export default function RainStatusSection() {
const rainyByUsgBildTp = useDmStore((state) => state.rainy.rainyByUsgBildTp);
const rainyByUsgYearsHeader = useDmStore((state) => state.rainy.rainyByUsgYearsHeader);
const rainyByUsgYears = useDmStore((state) => state.rainy.rainyByUsgYears);
const partBildTpHeader = useMemo(
() => rainyByUsgBildTpHeader.slice(3).map((header) => header.title),
[rainyByUsgBildTpHeader]
);
const partBildTpHeader = useMemo(() => rainyByUsgBildTpHeader.slice(3).map((header) => header.title), [rainyByUsgBildTpHeader]);
// 출처 텍스트
function SourceText() {
return (
<p className="mt-7 text-center text-[13px] text-neutral-500">
자료: 하수도정보시스템 {selectYear} 하수도 통계
</p>
);
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 하수도정보시스템 {selectYear} 하수도 통계</p>;
}
useEffect(() => {
@@ -171,32 +121,22 @@ export default function RainStatusSection() {
const renderPartHeaders = (headers: string[], headerColors: string[]) => {
return headers.map((header, index) => (
<th
key={header}
className={`${tableStyles.th} ${headerColors[index] ?? "bg-[#81C784]"} text-white`}
style={{ width: "140px" }}
>
<th key={header} className={`${tableStyles.th} ${headerColors[index] ?? 'bg-[#81C784]'} text-white`} style={{ width: '140px' }}>
{header}
</th>
));
};
const renderBuildingUsageCells = (
row: DmStatusBildTpYearsType | DmStatusUsgYearsType | DmStatusUsgYearsType,
headers: TableHeaderType[]
) => {
const renderBuildingUsageCells = (row: DmStatusBildTpYearsType | DmStatusUsgYearsType | DmStatusUsgYearsType, headers: TableHeaderType[]) => {
const rowData = row as Record<string, string | number>;
const firstHeaderId = headers[0]?.id;
const rowKey = firstHeaderId ? String(rowData[firstHeaderId] ?? "") : "";
const rowKey = firstHeaderId ? String(rowData[firstHeaderId] ?? '') : '';
return headers.map((header, index) => {
const value = rowData[header.id];
return (
<td
key={`${rowKey}-${header.id}`}
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 3, 5].includes(index) ? "bg-[#EAF5F5]" : ""}`}
>
<td key={`${rowKey}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 5].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
{index === 0 ? value : formatUsage(value as string | number, 1000, 1)}
</td>
);
@@ -206,102 +146,99 @@ export default function RainStatusSection() {
const renderRainyByUsgBildTpCells = (row: DmStatusUsgBildTpType) => {
return rainyByUsgBildTpHeader.map((header, index) => {
const value = row[header.id as keyof DmStatusUsgBildTpType];
const displayValue =
index === 0
? value
: header.id === "CNT"
? Number(value).toLocaleString("ko-KR")
: formatUsage(value as string | number, 1000, 1);
const displayValue = index === 0 ? value : header.id === 'CNT' ? Number(value).toLocaleString('ko-KR') : formatUsage(value as string | number, 1000, 1);
return (
<td
key={`${row.USG_NM}-${header.id}`}
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 2].includes(index) ? "bg-[#EAF5F5]" : ""}`}
>
{displayValue == 0.0 ? "-" : displayValue}
<td key={`${row.USG_NM}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 2].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
{displayValue == 0.0 ? '-' : displayValue}
</td>
);
});
};
const title = "빗물이용시설(현황)";
const title = '빗물이용시설(현황)';
return (
<div className="bg-white">
<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" />
<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>
</div>
<div className="mt-[70px] pb-[120px]">
<div className='mt-[70px] pb-[120px]'>
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
<section>
{/* 인트로 영역 */}
<RainIntroSummary />
<div id="status-rain-building" className="scroll-mt-[120px]">
<div id='status-rain-building' className='scroll-mt-[120px]'>
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
<div className="mt-10">
<div className={tableStyles.container}>
<div className='mt-10'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<thead>
<thead>
<tr>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
구분
</th>
<th colSpan={rainyYears.length || 6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
</th>
<th colSpan={rainyYears.length || 6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
빗물이용량 (천m³/년)
</th>
</th>
</tr>
<tr>
{renderPartHeaders(rainyYears, headerColors6)}
</tr>
</thead>
<tbody>
<tr>{renderPartHeaders(rainyYears, headerColors6)}</tr>
</thead>
<tbody>
{rainyByBildTpYears.map((row, idx) => (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
{renderBuildingUsageCells(row, rainyByBildTpYearsHeader)}
</tr>
</tr>
))}
</tbody>
</tbody>
</table>
</div>
</div>
<SourceText />
<SourceText />
</div>
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 2) 빗물이용시설의 용도별 분포 (xxxx년 기준) */}
<section
id="status-rain-purpose-distribution"
className="scroll-mt-[120px]"
>
<section id='status-rain-purpose-distribution' className='scroll-mt-[120px]'>
<SectionTitle>빗물이용시설의 용도별 분포 ({selectYear}년 기준)</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<thead>
<tr>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>용도</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "84px" }}>개소수<br />(개소)</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "84px" }}>저류용량<br />(천m³)</th>
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>빗물이용량 (천m³/년)</th>
</tr>
<tr>
{renderPartHeaders(partBildTpHeader, headerColors8)}
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
용도
</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
개소수
<br />
(개소)
</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
저류조용량
<br />
(천m³)
</th>
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
빗물이용량 (천m³/년)
</th>
</tr>
<tr>{renderPartHeaders(partBildTpHeader, headerColors8)}</tr>
</thead>
<tbody>
{rainyByUsgBildTp.map((row, idx) => (
<tr key={`${row.USG_NM}-${idx}`} className="bg-white">
<tr key={`${row.USG_NM}-${idx}`} className='bg-white'>
{renderRainyByUsgBildTpCells(row)}
</tr>
))}
@@ -313,30 +250,29 @@ export default function RainStatusSection() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
<section
id="status-rain-purpose-usage"
className="scroll-mt-[120px]"
>
<section id='status-rain-purpose-usage' className='scroll-mt-[120px]'>
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<thead>
<tr>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>용도</th>
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>빗물이용량 (천m³/년)</th>
</tr>
<tr>
{renderPartHeaders(rainyYears, headerColors6)}
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
용도
</th>
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
빗물이용량 (천m³/년)
</th>
</tr>
<tr>{renderPartHeaders(rainyYears, headerColors6)}</tr>
</thead>
<tbody>
{rainyByUsgYears.map((row, idx) => (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
{renderBuildingUsageCells(row, rainyByUsgYearsHeader)}
</tr>
))}
@@ -350,4 +286,4 @@ export default function RainStatusSection() {
</div>
</div>
);
}
}
@@ -1,116 +1,97 @@
/**
* 하수처리수
*/
"use client";
'use client';
import React from "react";
import Image from "next/image";
import { useEffect } from "react";
import { useDmStore } from "@/app/store";
import { formatUsage } from "@/app/common/transValueFormat";
import type {
DmStatusLawYearsType,
DmStatusBildTpYearsType,
DmStatusUsgYearsType,
DmStatusSidoUsgType,
TableHeaderType,
} from "@/app/store/dm-status/type";
import { Col2Name } from "@/app/store/dm-status/type";
import React from 'react';
import Image from 'next/image';
import { useEffect } from 'react';
import { useDmStore } from '@/app/store';
import { formatUsage } from '@/app/common/transValueFormat';
import type { DmStatusLawYearsType, DmStatusBildTpYearsType, DmStatusUsgYearsType, DmStatusSidoUsgType, TableHeaderType } from '@/app/store/dm-status/type';
import { Col2Name } from '@/app/store/dm-status/type';
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
<div className='flex items-center gap-3'>
<span className='h-5 w-[3px] bg-sky-500' />
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
</div>
);
}
function Hr({ className = "" }: { className?: string }) {
function Hr({ className = '' }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
function SewageIntroSummary() {
const items = [
{
icon: "/images/landing/icon_cycle_water.png",
title: "안정적인 재이용률 유지",
icon: '/images/landing/icon_cycle_water.png',
title: '안정적인 재이용률 유지',
desc: (
<>
<>
연간 약 1,136백만m³의 하수처리수가 재이용되고 있으며,
<br/>
<br />
약 15%에 달하는 높은 재이용률을 통해
<br/>
<br />
수자원 선순환 체계를 구축하고 있습니다.
</>
</>
),
},
{
icon: "/images/landing/icon_water_flow.png",
title: "메마른 하천을 살리는 물",
icon: '/images/landing/icon_water_flow.png',
title: '메마른 하천을 살리는 물',
desc: (
<>
<>
재이용되는 물의 가장 많은 부분이 하천유지용수로 사용되어
<br/>
<br />
도심 속 하천의 생태계를 보호하고 쾌적한 수변 환경을 조성합니다.
</>
</>
),
},
{
icon: "/images/landing/icon_location.png",
title: "지역별 균형 있는 활용",
icon: '/images/landing/icon_location.png',
title: '지역별 균형 있는 활용',
desc: (
<>
<>
전국적으로 고르게 시설이 확충되고 있으며, 특히 경기 지역을 중심으로
<br/>
<br />
대규모 공업용수 공급망을 구축하여
<br/>
<br />
지역 경제와 환경 보호를 동시에 실현하고 있습니다.
</>
</>
),
},
];
return (
<div className="mb-[70px] px-[160px]">
<div className="grid grid-cols-[1fr_400px] items-center gap-0">
<div className='mb-[70px] px-[160px]'>
<div className='grid grid-cols-[1fr_400px] items-center gap-0'>
{/* 좌측 */}
<div>
<h3 className="text-[40px] font-extrabold leading-[1.35] tracking-[-0.14em] text-neutral-900">
<h3 className='text-[40px] font-extrabold leading-[1.35] tracking-[-0.14em] text-neutral-900'>
버려지는 하수에서 찾는 새로운 가치,
<br />
<span className="inline-block bg-gradient-to-b from-[#6DBB4F] to-[#01ACE5] bg-clip-text text-transparent">
하수처리수 재이용
</span>
이 도시를 숨쉬게 합니다.
<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">
<p className='mt-5 text-[16px] leading-[1.8] text-neutral-600'>
하수처리장에서 정화된 물은 하천의 수량을 조절하고
<br />
산업 현장에 용수를 공급하는 소중한 자원입니다.
</p>
<div className="mt-10 space-y-8">
<div className='mt-10 space-y-8'>
{items.map((item) => (
<div key={item.title} className="flex gap-4 items-center">
<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 key={item.title} className='flex gap-4 items-center'>
<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>
<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>
))}
@@ -118,15 +99,8 @@ function SewageIntroSummary() {
</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 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>
@@ -134,31 +108,24 @@ function SewageIntroSummary() {
}
const tableStyles = {
container: "w-[1000px] mx-auto overflow-hidden",
table: "w-full table-fixed border-collapse text-center text-[16px]",
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
headerBg: "bg-[#E3E7EA]",
container: 'w-[1000px] mx-auto overflow-hidden',
table: 'w-full table-fixed border-collapse text-center text-[16px]',
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
headerBg: 'bg-[#E3E7EA]',
};
const headerColors6 = [
"bg-[#01ACE5]",
"bg-[#17AFC7]",
"bg-[#2CB2A9]",
"bg-[#42B58B]",
"bg-[#57B86D]",
"bg-[#81C784]",
];
const headerColors6 = ['bg-[#01ACE5]', 'bg-[#17AFC7]', 'bg-[#2CB2A9]', 'bg-[#42B58B]', 'bg-[#57B86D]', 'bg-[#81C784]'];
export default function SewageStatusSection() {
const getPbnstByLawYears = useDmStore((state) => state.getPbnstByLawYears);
const getPbnstByUsgYears = useDmStore((state) => state.getPbnstByUsgYears);
const getPbnstByCpcYears = useDmStore((state) => state.getPbnstByCpcYears);
const getPbnstBySido = useDmStore((state) => state.getPbnstBySido);
const getPbnstByUsgYears = useDmStore((state) => state.getPbnstByUsgYears);
const getPbnstByCpcYears = useDmStore((state) => state.getPbnstByCpcYears);
const getPbnstBySido = useDmStore((state) => state.getPbnstBySido);
const pbnstYears = useDmStore((state) => state.pbnst.pbnstYears);
const selectYear = pbnstYears.slice(-1)[0];
const pbnstByLawYearsHeader = useDmStore((state) => state.pbnst.pbnstByLawYearsHeader);
const pbnstByLawYears = useDmStore((state) => state.pbnst.pbnstByLawYears);
const pbnstByLawYears = useDmStore((state) => state.pbnst.pbnstByLawYears);
const pbnstByUsgYearsHeader = useDmStore((state) => state.pbnst.pbnstByUsgYearsHeader);
const pbnstByUsgYears = useDmStore((state) => state.pbnst.pbnstByUsgYears);
const pbnstByCpcYearsHeader = useDmStore((state) => state.pbnst.pbnstByCpcYearsHeader);
@@ -168,15 +135,11 @@ export default function SewageStatusSection() {
// 출처 텍스트
function SourceText() {
return (
<p className="mt-7 text-center text-[13px] text-neutral-500">
자료: 하수도정보시스템 {selectYear} 하수도 통계
</p>
);
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 하수도정보시스템 {selectYear} 하수도 통계</p>;
}
useEffect(() => {
void getPbnstByLawYears();
void getPbnstByLawYears();
void getPbnstByUsgYears();
void getPbnstByCpcYears();
void getPbnstBySido();
@@ -184,10 +147,7 @@ export default function SewageStatusSection() {
const renderPartHeaders = (headers: string[], headerColors: string[]) => {
return headers.map((header, index) => (
<th
key={header}
className={`${tableStyles.th} ${headerColors[index] ?? "bg-[#81C784]"} text-white`}
>
<th key={header} className={`${tableStyles.th} ${headerColors[index] ?? 'bg-[#81C784]'} text-white`}>
{header}
</th>
));
@@ -198,10 +158,10 @@ export default function SewageStatusSection() {
row: DmStatusLawYearsType,
data: DmStatusLawYearsType[],
headers: TableHeaderType[],
unitSize: number = 1, // 단위 변경 1000 인 경우, 100,000 -> 100 으로 변경
unitSize: number = 1 // 단위 변경 1000 인 경우, 100,000 -> 100 으로 변경
) => {
const rowData = row as Record<string, string | number>;
const currentCol1 = String(rowData["COL1"] ?? "");
const currentCol1 = String(rowData['COL1'] ?? '');
// 이전 값
const prevCol1 = rowIdx > 0 ? data[rowIdx - 1]?.COL1 : null;
@@ -220,56 +180,38 @@ export default function SewageStatusSection() {
const value = rowData[header.id];
// COL1 처리 (rowspan 핵심)
if (header.id === "COL1") {
if (header.id === 'COL1') {
// 이전과 같으면 렌더링 안함
if (currentCol1 === prevCol1) return null;
return (
<td
key={`${rowIdx}-${header.id}`}
rowSpan={getRowSpan()}
className={`${tableStyles.td} !font-bold !text-neutral-900`}
>
<td key={`${rowIdx}-${header.id}`} rowSpan={getRowSpan()} className={`${tableStyles.td} !font-bold !text-neutral-900`}>
{value}
</td>
);
} else if (header.id === "COL2") {
return (
<td
key={`${rowIdx}-${header.id}`}
className={`${tableStyles.td} !font-bold !text-neutral-900`}
dangerouslySetInnerHTML={{ __html: Col2Name[value as string] ?? "" }}
/>
);
} else if (header.id === 'COL2') {
return <td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} !font-bold !text-neutral-900`} dangerouslySetInnerHTML={{ __html: Col2Name[value as string] ?? '' }} />;
}
return (
<td
key={`${rowIdx}-${header.id}`}
className={`${tableStyles.td} ${[0, 2, 4].includes(index) ? "bg-[#EAF5F5]" : ""}`}
>
{(String(rowData["COL2"]).indexOf("CNT") > -1)
? Number(value).toLocaleString("ko-KR")
: ((String(rowData["COL2"]).indexOf("REUSERT") > -1)
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} ${[0, 2, 4].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
{String(rowData['COL2']).indexOf('CNT') > -1
? Number(value).toLocaleString('ko-KR')
: String(rowData['COL2']).indexOf('REUSERT') > -1
? formatUsage(value as string | number, 1, 1)
: formatUsage(value as string | number, unitSize, 1))
}
: formatUsage(value as string | number, unitSize, 1)}
</td>
);
});
};
const renderPbnstUsgCells = (
rowIdx: number,
row: DmStatusBildTpYearsType | DmStatusUsgYearsType,
headers: TableHeaderType[]
) => {
const renderPbnstUsgCells = (rowIdx: number, row: DmStatusBildTpYearsType | DmStatusUsgYearsType, headers: TableHeaderType[]) => {
const rowData = row as Record<string, string | number>;
const isLastInGroup = rowIdx === 8 || rowIdx === 17;
const getType = (colNm:string) => {
const getType = (colNm: string) => {
const last3 = colNm.slice(-3); // 끝 3자리
const prefix = last3[0]; // S 또는 D
const prefix = last3[0]; // S 또는 D
if (prefix === 'S') {
const num = parseInt(last3.slice(1), 10); // 숫자
@@ -282,149 +224,117 @@ export default function SewageStatusSection() {
return headers.map((header, index) => {
const value = rowData[header.id];
const colNm = String(rowData["COL_NM"] ?? "");
const colNm = String(rowData['COL_NM'] ?? '');
const type = getType(colNm);
const isSubItem = type === "red-item" || type === "blue-item";
const isSubItem = type === 'red-item' || type === 'blue-item';
if (index === 0) {
// console.log(header.id, value);
return (
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} relative`}>
{isSubItem ? (
<>
<div
className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${
isLastInGroup ? "bottom-[0px]" : "bottom-[-2px]"
}`}
/>
<div
className={`absolute left-[16px] w-[4px] z-20 ${
type === "red-item" ? "bg-[#FF8B8B]" : "bg-[#4ECBFF]"
} top-[-2px] ${
isLastInGroup ? "bottom-[0px]" : "bottom-[-2px]"
}`}
/>
<span className="text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]">
{value}
</span>
<div className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
<div className={`absolute left-[16px] w-[4px] z-20 ${type === 'red-item' ? 'bg-[#FF8B8B]' : 'bg-[#4ECBFF]'} top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
<span className='text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]'>{value}</span>
</>
) : (
<span className="font-bold text-neutral-900 block w-full text-center">
{value}
</span>
<span className='font-bold text-neutral-900 block w-full text-center'>{value}</span>
)}
</td>
);
}
);
}
return (
<td
key={`${rowIdx}-${header.id}`}
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[0, 2, 4].includes(index) ? "bg-[#EAF5F5]" : ""}`}
>
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[0, 2, 4].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
{formatUsage(value as string | number, 1000, 1)}
</td>
);
});
};
const renderSidoUsgCells = (
rowIdx: number,
row: DmStatusSidoUsgType,
headers: TableHeaderType[]
) => {
const renderSidoUsgCells = (rowIdx: number, row: DmStatusSidoUsgType, headers: TableHeaderType[]) => {
const rowData = row as Record<string, string | number>;
return headers.map((header, index) => {
let value = rowData[header.id];
value = (index === 0||index ===9) ? value : formatUsage(value as string | number, 1000, 1);
value = value === "0.0"? "-" : value;
value = index === 0 || index === 9 ? value : formatUsage(value as string | number, 1000, 1);
value = value === '0.0' ? '-' : value;
return (
<td
key={`${rowIdx}-${header.id}`}
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 9].includes(index) ? "bg-[#EAF5F5]" : ""}`}
>
<td key={`${rowIdx}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 9].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
{value}
</td>
);
});
};
const title = "하수처리수 재이용시설(현황)";
const title = '하수처리수 재이용시설(현황)';
return (
<div className="bg-white">
<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" />
<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>
</div>
<div className="mt-[70px] pb-[120px]">
<div className='mt-[70px] pb-[120px]'>
{/* 1) 연도별 하수처리수 재이용 현황 */}
<section>
{/* 인트로 */}
<SewageIntroSummary />
{/* 인트로 */}
<SewageIntroSummary />
<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}`}
>
구분
</th>
{renderPartHeaders(pbnstYears, headerColors6)}
</tr>
</thead>
<tbody>
{pbnstByLawYears.map((row, idx, arr) => (
<tr key={idx}>
{renderCells(idx, row, arr, pbnstByLawYearsHeader, 1000)}
</tr>
))}
</tbody>
</table>
</div>
<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}`}>
구분
</th>
{renderPartHeaders(pbnstYears, headerColors6)}
</tr>
</thead>
<tbody>
{pbnstByLawYears.map((row, idx, arr) => (
<tr key={idx}>{renderCells(idx, row, arr, pbnstByLawYearsHeader, 1000)}</tr>
))}
</tbody>
</table>
</div>
<SourceText />
</div>
<SourceText />
</div>
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 2) 용도별 하수처리수 재이용 현황 */}
<section id="status-treated-usage" className="scroll-mt-[120px]">
<section id='status-treated-usage' className='scroll-mt-[120px]'>
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<div className="mb-2 text-right text-[13px] text-neutral-600">
(단위: 백만m³/일)
</div>
<div className='mb-2 text-right text-[13px] text-neutral-600'>(단위: 백만m³/년)</div>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: "180px" }} />
<col style={{ width: '180px' }} />
<col />
<col />
<col />
@@ -434,17 +344,15 @@ export default function SewageStatusSection() {
</colgroup>
<thead>
<tr>
<th className={`${tableStyles.th} ${tableStyles.headerBg}`}>
구분
</th>
<th className={`${tableStyles.th} ${tableStyles.headerBg}`}>구분</th>
{renderPartHeaders(pbnstYears, headerColors6)}
</tr>
</thead>
<tbody>
{pbnstByUsgYears.map((row, idx) => (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
{renderPbnstUsgCells(idx, row, pbnstByUsgYearsHeader)}
</tr>
</tr>
))}
</tbody>
</table>
@@ -454,18 +362,18 @@ export default function SewageStatusSection() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 3) 하수처리시설 설치 및 이용 현황 */}
<section id="status-treated-facility" className="scroll-mt-[120px]">
<section id='status-treated-facility' className='scroll-mt-[120px]'>
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: "130px" }} />
<col style={{ width: "160px" }} />
<col style={{ width: '130px' }} />
<col style={{ width: '160px' }} />
<col />
<col />
<col />
@@ -475,10 +383,7 @@ export default function SewageStatusSection() {
</colgroup>
<thead>
<tr>
<th
colSpan={2}
className={`${tableStyles.th} ${tableStyles.headerBg}`}
>
<th colSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
구분
</th>
{renderPartHeaders(pbnstYears, headerColors6)}
@@ -486,9 +391,7 @@ export default function SewageStatusSection() {
</thead>
<tbody>
{pbnstByCpcYears.map((row, idx, arr) => (
<tr key={idx}>
{renderCells(idx, row, arr, pbnstByCpcYearsHeader, 10000)}
</tr>
<tr key={idx}>{renderCells(idx, row, arr, pbnstByCpcYearsHeader, 10000)}</tr>
))}
</tbody>
</table>
@@ -498,24 +401,19 @@ export default function SewageStatusSection() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 4) 시도별 하수처리수 재이용 현황 */}
<section
id="status-treated-region"
className="scroll-mt-[120px]"
>
<section id='status-treated-region' className='scroll-mt-[120px]'>
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<div className="mb-2 text-right text-[13px] text-neutral-600">
(단위: 백만m³/일)
</div>
<div className='mb-2 text-right text-[13px] text-neutral-600'>(단위: 백만m³/년)</div>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: "160px" }} />
<col style={{ width: '160px' }} />
<col />
<col />
<col />
@@ -529,54 +427,53 @@ export default function SewageStatusSection() {
</colgroup>
<thead>
<tr>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}> 시도 </th>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
연간<br />하수<br />처리량
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
{' '}
시도{' '}
</th>
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`} >
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
연간
<br />
하수
<br />
처리량
</th>
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
재이용현황
</th>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
재이용률<br />(%)
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
재이용률
<br />
(%)
</th>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
비고
</th>
</tr>
<tr>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
계
</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} >
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
장내
</th>
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`} >
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}>
장외
</th>
</tr>
<tr>
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>
소계
</th>
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>
하천유지용수
</th>
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>
공업용수
</th>
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>
농업용수
</th>
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>
기타용수
</th>
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>소계</th>
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>하천유지용수</th>
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>공업용수</th>
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>농업용수</th>
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>기타용수</th>
</tr>
</thead>
<tbody>
{pbnstBySido.map((row, idx) => (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
{renderSidoUsgCells(idx, row, pbnstBySidoHeader)}
</tr>
</tr>
))}
</tbody>
</table>
+69 -113
View File
@@ -1,60 +1,36 @@
"use client";
'use client';
import { useEffect, useMemo } from "react";
import SubPageHeader from "@/components/SubPageHeader";
import { useDmStore } from "@/app/store";
import { formatUsage } from "@/app/common/transValueFormat";
import type {
DmStatusBildTpYearsType,
DmStatusUsgBildTpType,
DmStatusUsgYearsType,
TableHeaderType,
} from "@/app/store/dm-status/type";
import { useEffect, useMemo } from 'react';
import SubPageHeader from '@/components/SubPageHeader';
import { useDmStore } from '@/app/store';
import { formatUsage } from '@/app/common/transValueFormat';
import type { DmStatusBildTpYearsType, DmStatusUsgBildTpType, DmStatusUsgYearsType, TableHeaderType } from '@/app/store/dm-status/type';
// 공통 섹션 타이틀 컴포넌트
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
<div className='flex items-center gap-3'>
<span className='h-5 w-[3px] bg-sky-500' />
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
</div>
);
}
// 구분선
function Hr({ className = "" }: { className?: string }) {
function Hr({ className = '' }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
const tableStyles = {
container: "w-[1000px] mx-auto overflow-hidden",
table: "w-full table-fixed border-collapse text-center text-[16px]",
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
headerBg: "bg-[#E3E7EA]",
container: 'w-[1000px] mx-auto overflow-hidden',
table: 'w-full table-fixed border-collapse text-center text-[16px]',
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
headerBg: 'bg-[#E3E7EA]',
};
const headerColors6 = [
"bg-[#01ACE5]",
"bg-[#17AFC7]",
"bg-[#2CB2A9]",
"bg-[#42B58B]",
"bg-[#57B86D]",
"bg-[#81C784]",
];
const headerColors8 = [
"bg-[#01ACE5]",
"bg-[#10AED0]",
"bg-[#20B0BA]",
"bg-[#2FB2A5]",
"bg-[#3FB58F]",
"bg-[#4EB77A]",
"bg-[#5EB964]",
"bg-[#6DBB4F]",
];
const headerColors6 = ['bg-[#01ACE5]', 'bg-[#17AFC7]', 'bg-[#2CB2A9]', 'bg-[#42B58B]', 'bg-[#57B86D]', 'bg-[#81C784]'];
const headerColors8 = ['bg-[#01ACE5]', 'bg-[#10AED0]', 'bg-[#20B0BA]', 'bg-[#2FB2A5]', 'bg-[#3FB58F]', 'bg-[#4EB77A]', 'bg-[#5EB964]', 'bg-[#6DBB4F]'];
export default function RainwaterStatusPage() {
const getRainyByBildTpYears = useDmStore((state) => state.getRainyByBildTpYears);
@@ -68,19 +44,11 @@ export default function RainwaterStatusPage() {
const rainyByUsgBildTp = useDmStore((state) => state.rainy.rainyByUsgBildTp);
const rainyByUsgYearsHeader = useDmStore((state) => state.rainy.rainyByUsgYearsHeader);
const rainyByUsgYears = useDmStore((state) => state.rainy.rainyByUsgYears);
const partBildTpHeader = useMemo(
() => rainyByUsgBildTpHeader.slice(3).map((header) => header.title),
[rainyByUsgBildTpHeader]
);
const partBildTpHeader = useMemo(() => rainyByUsgBildTpHeader.slice(3).map((header) => header.title), [rainyByUsgBildTpHeader]);
// 출처 텍스트
function SourceText() {
return (
<p className="mt-7 text-center text-[13px] text-neutral-500">
자료: 환경부(2024), 「{selectYear} 하수도 통계」
</p>
);
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 환경부(2024), 「{selectYear} 하수도 통계」</p>;
}
useEffect(() => {
@@ -91,32 +59,22 @@ export default function RainwaterStatusPage() {
const renderPartHeaders = (headers: string[], headerColors: string[]) => {
return headers.map((header, index) => (
<th
key={header}
className={`${tableStyles.th} ${headerColors[index] ?? "bg-[#81C784]"} text-white`}
style={{ width: "140px" }}
>
<th key={header} className={`${tableStyles.th} ${headerColors[index] ?? 'bg-[#81C784]'} text-white`} style={{ width: '140px' }}>
{header}
</th>
));
};
const renderBuildingUsageCells = (
row: DmStatusBildTpYearsType | DmStatusUsgYearsType | DmStatusUsgYearsType,
headers: TableHeaderType[]
) => {
const renderBuildingUsageCells = (row: DmStatusBildTpYearsType | DmStatusUsgYearsType | DmStatusUsgYearsType, headers: TableHeaderType[]) => {
const rowData = row as Record<string, string | number>;
const firstHeaderId = headers[0]?.id;
const rowKey = firstHeaderId ? String(rowData[firstHeaderId] ?? "") : "";
const rowKey = firstHeaderId ? String(rowData[firstHeaderId] ?? '') : '';
return headers.map((header, index) => {
const value = rowData[header.id];
return (
<td
key={`${rowKey}-${header.id}`}
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 3, 5].includes(index) ? "bg-[#EAF5F5]" : ""}`}
>
<td key={`${rowKey}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 5].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
{index === 0 ? value : formatUsage(value as string | number, 1000, 1)}
</td>
);
@@ -126,63 +84,49 @@ export default function RainwaterStatusPage() {
const renderRainyByUsgBildTpCells = (row: DmStatusUsgBildTpType) => {
return rainyByUsgBildTpHeader.map((header, index) => {
const value = row[header.id as keyof DmStatusUsgBildTpType];
const displayValue =
index === 0
? value
: header.id === "CNT"
? Number(value).toLocaleString("ko-KR")
: formatUsage(value as string | number, 1000, 1);
const displayValue = index === 0 ? value : header.id === 'CNT' ? Number(value).toLocaleString('ko-KR') : formatUsage(value as string | number, 1000, 1);
return (
<td
key={`${row.USG_NM}-${header.id}`}
className={`${tableStyles.td} ${index === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 2].includes(index) ? "bg-[#EAF5F5]" : ""}`}
>
{displayValue == 0.0 ? "-" : displayValue}
<td key={`${row.USG_NM}-${header.id}`} className={`${tableStyles.td} ${index === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 2].includes(index) ? 'bg-[#EAF5F5]' : ''}`}>
{displayValue == 0.0 ? '-' : displayValue}
</td>
);
});
};
return (
<div className="bg-white">
<div className='bg-white'>
<SubPageHeader
title="빗물이용시설(현황)"
crumbs={[
{ label: "Home", href: "/reuse" },
{ label: "물 재이용 현황", href: "/reuse/cases/rain" },
{ label: "빗물이용시설(현황)" },
]}
title='빗물이용시설(현황)'
crumbs={[{ label: 'Home', href: '/reuse' }, { label: '물 재이용 현황', href: '/reuse/cases/rain' }, { label: '빗물이용시설(현황)' }]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
wrapClassName='!pb-0'
/>
<div className="pr-[100px]">
<Hr className="my-7" />
<div className='pr-[100px]'>
<Hr className='my-7' />
{/* 1) 빗물이용시설의 건축물별 빗물이용량 현황 */}
<section id="status-rain-building" className="mt-10 scroll-mt-[120px]">
<section id='status-rain-building' className='mt-10 scroll-mt-[120px]'>
<SectionTitle>빗물이용시설의 건축물별 빗물이용량 현황</SectionTitle>
<div className="mt-10">
<div className='mt-10'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<thead>
<tr>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
구분
</th>
<th colSpan={rainyYears.length || 6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
빗물이용량 (천m³/년)
</th>
</tr>
<tr>
{renderPartHeaders(rainyYears, headerColors6)}
</tr>
<tr>{renderPartHeaders(rainyYears, headerColors6)}</tr>
</thead>
<tbody>
{rainyByBildTpYears.map((row, idx) => (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
{renderBuildingUsageCells(row, rainyByBildTpYearsHeader)}
</tr>
))}
@@ -193,28 +137,38 @@ export default function RainwaterStatusPage() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 2) 빗물이용시설의 용도별 분포 (2023년 기준) */}
<section id="status-rain-purpose-distribution" className="scroll-mt-[120px]">
<section id='status-rain-purpose-distribution' className='scroll-mt-[120px]'>
<SectionTitle>빗물이용시설의 용도별 분포 ({selectYear}년 기준)</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<thead>
<tr>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>용도</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "84px" }}>개소수<br />(개소)</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "84px" }}>저류용량<br />(천m³)</th>
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>빗물이용량 (천m³/년)</th>
</tr>
<tr>
{renderPartHeaders(partBildTpHeader, headerColors8)}
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
용도
</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
개소수
<br />
(개소)
</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '84px' }}>
저류조용량
<br />
(천m³)
</th>
<th colSpan={8} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
빗물이용량 (천m³/년)
</th>
</tr>
<tr>{renderPartHeaders(partBildTpHeader, headerColors8)}</tr>
</thead>
<tbody>
{rainyByUsgBildTp.map((row, idx) => (
<tr key={`${row.USG_NM}-${idx}`} className="bg-white">
<tr key={`${row.USG_NM}-${idx}`} className='bg-white'>
{renderRainyByUsgBildTpCells(row)}
</tr>
))}
@@ -225,26 +179,28 @@ export default function RainwaterStatusPage() {
</div>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 3) 빗물이용시설의 용도별 빗물이용량 현황 */}
<section id="status-rain-purpose-usage" className="pb-[200px] scroll-mt-[120px]">
<section id='status-rain-purpose-usage' className='pb-[200px] scroll-mt-[120px]'>
<SectionTitle>빗물이용시설의 용도별 빗물이용량 현황</SectionTitle>
<div className="mt-8">
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<thead>
<tr>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: "160px" }}>용도</th>
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>빗물이용량 (천m³/년)</th>
</tr>
<tr>
{renderPartHeaders(rainyYears, headerColors6)}
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`} style={{ width: '160px' }}>
용도
</th>
<th colSpan={6} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
빗물이용량 (천m³/년)
</th>
</tr>
<tr>{renderPartHeaders(rainyYears, headerColors6)}</tr>
</thead>
<tbody>
{rainyByUsgYears.map((row, idx) => (
<tr key={idx} className="bg-white">
<tr key={idx} className='bg-white'>
{renderBuildingUsageCells(row, rainyByUsgYearsHeader)}
</tr>
))}
+372 -362
View File
@@ -1,30 +1,26 @@
"use client";
'use client';
import React from "react";
import SubPageHeader from "@/components/SubPageHeader";
import React from 'react';
import SubPageHeader from '@/components/SubPageHeader';
// 공통 섹션 타이틀 컴포넌트
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="flex items-center gap-3">
<span className="h-5 w-[3px] bg-sky-500" />
<h3 className="text-[24px] font-bold text-neutral-900">{children}</h3>
<div className='flex items-center gap-3'>
<span className='h-5 w-[3px] bg-sky-500' />
<h3 className='text-[24px] font-bold text-neutral-900'>{children}</h3>
</div>
);
}
// 구분선
function Hr({ className = "" }: { className?: string }) {
function Hr({ className = '' }: { className?: string }) {
return <div className={`h-[2px] w-full bg-black/10 ${className}`} />;
}
// 출처 텍스트
function SourceText() {
return (
<p className="mt-7 text-center text-[13px] text-neutral-500">
자료: 환경부(2024), 「2023 하수도 통계」
</p>
);
return <p className='mt-7 text-center text-[13px] text-neutral-500'>자료: 환경부(2024), 「2023 하수도 통계」</p>;
}
/**
@@ -33,11 +29,11 @@ function SourceText() {
* - 폰트 사이즈: 16px로 수정
*/
const tableStyles = {
container: "w-[1000px] mx-auto overflow-hidden",
table: "w-full table-fixed border-collapse text-center text-[16px]",
th: "border border-[#858F98] py-3 font-bold text-neutral-800",
td: "border border-[#858F98] py-3 text-neutral-700 font-medium break-keep",
headerBg: "bg-[#E3E7EA]",
container: 'w-[1000px] mx-auto overflow-hidden',
table: 'w-full table-fixed border-collapse text-center text-[16px]',
th: 'border border-[#858F98] py-3 font-bold text-neutral-800',
td: 'border border-[#858F98] py-3 text-neutral-700 font-medium break-keep',
headerBg: 'bg-[#E3E7EA]',
};
// export default function TreatedWaterPage() {
@@ -51,368 +47,382 @@ const tableStyles = {
export default function TreatedStatusPage() {
return (
<div className="relative bg-white">
<div className='relative bg-white'>
{/* <BusinessSubNav /> */}
<SubPageHeader
title="하수처리수 재이용시설(현황)"
crumbs={[
{ label: "Home", href: "/main" },
{ label: "물 재이용 현황", href: "/status/rain" },
{ label: "하수처리수 재이용시설(현황)" },
]}
title='하수처리수 재이용시설(현황)'
crumbs={[{ label: 'Home', href: '/main' }, { label: '물 재이용 현황', href: '/status/rain' }, { label: '하수처리수 재이용시설(현황)' }]}
hideFooterLine
hideGuideText
wrapClassName="!pb-0"
wrapClassName='!pb-0'
/>
<div className="pr-0">
<Hr className="my-7" />
<div className='pr-0'>
<Hr className='my-7' />
{/* 1) 연도별 하수처리수 재이용 현황 */}
<section className="mt-10">
<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}`}>구분</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`}
>
{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 }}
/>
);
})}
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
{/* 1) 연도별 하수처리수 재이용 현황 */}
<section className='mt-10'>
<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}`}>
구분
</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`}>
{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 }}
/>
);
})}
</tr>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>
<SourceText />
</div>
<SourceText />
</div>
</section>
</section>
<Hr className="my-11" />
<Hr className='my-11' />
{/* 2) 용도별 하수처리수 재이용 현황 */}
<section>
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: '180px' }} />
<col />
<col />
<col />
<col />
<col />
<col />
</colgroup>
<thead>
<tr>
<th 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>
{[
{ label: "총 재이용량", type: "total", values: ["1,112.7", "1,148.7", "1,144.7", "1,127.0", "1,136.8", "1,137.5"] },
{ label: "장내 재이용", type: "category", values: ["520.6", "519.8", "513.9", "520.3", "502.2", "490.0"] },
{ label: "세척수", type: "red-item", values: ["177.9", "174.4", "169.4", "180.9", "196.8", "191.2"] },
{ label: "냉각수", type: "red-item", values: ["45.5", "46.8", "50.4", "40.6", "38.3", "41.7"] },
{ label: "청소수", type: "red-item", values: ["17.6", "21.2", "21.2", "21.7", "20.8", "20.5"] },
{ label: "조경용수", type: "red-item", values: ["0.1", "0.1", "-", "-", "-", "-"] },
{ label: "회생용수", type: "red-item", values: ["15.8", "16.7", "17.8", "15.7", "14.4", "14.0"] },
{ label: "장내급수도", type: "red-item", values: ["2.4", "-", "-", "-", "-", "-"] },
{ label: "장내기타용수", type: "red-item", values: ["261.3", "260.6", "255.0", "261.1", "231.8", "222.6"] },
{ label: "장외 재이용", type: "category", values: ["592.1", "628.9", "630.8", "606.7", "634.6", "647.5"] },
{ label: "청소·화장실용수", type: "blue-item", values: ["0.6", "0.4", "0.4", "0.4", "0.4", "0.4"] },
{ label: "세척·살수용수", type: "blue-item", values: ["5.6", "9.9", "2.9", "2.1", "3.0", "3.6"] },
{ label: "조경용수", type: "blue-item", values: ["10.3", "16.4", "5.9", "4.5", "8.9", "9.5"] },
{ label: "친수용수", type: "blue-item", values: ["7.8", "3.1", "3.1", "3.0", "10.9", "10.5"] },
{ label: "하천유지용수", type: "blue-item", values: ["480.5", "482.5", "496.2", "442.7", "471.7", "484.0"] },
{ label: "농업용수", type: "blue-item", values: ["12.2", "11.9", "13.4", "10.7", "10.6", "11.4"] },
{ label: "지하수 충전", type: "blue-item", values: ["0.0", "0.0", "0.0", "13.4", "0.0", "0.0"] },
{ label: "공업용수", type: "blue-item", values: ["75.2", "104.6", "108.9", "129.9", "129.0", "128.1"] },
].map((row, idx) => {
const isSubItem = row.type === "red-item" || row.type === "blue-item";
const isLastInGroup = idx === 8 || idx === 17;
{/* 2) 용도별 하수처리수 재이용 현황 */}
<section>
<SectionTitle>용도별 하수처리수 재이용 현황</SectionTitle>
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: '180px' }} />
<col />
<col />
<col />
<col />
<col />
<col />
</colgroup>
<thead>
<tr>
<th 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>
{[
{ label: '총 재이용량', type: 'total', values: ['1,112.7', '1,148.7', '1,144.7', '1,127.0', '1,136.8', '1,137.5'] },
{ label: '장내 재이용', type: 'category', values: ['520.6', '519.8', '513.9', '520.3', '502.2', '490.0'] },
{ label: '세척수', type: 'red-item', values: ['177.9', '174.4', '169.4', '180.9', '196.8', '191.2'] },
{ label: '냉각수', type: 'red-item', values: ['45.5', '46.8', '50.4', '40.6', '38.3', '41.7'] },
{ label: '청소수', type: 'red-item', values: ['17.6', '21.2', '21.2', '21.7', '20.8', '20.5'] },
{ label: '조경용수', type: 'red-item', values: ['0.1', '0.1', '-', '-', '-', '-'] },
{ label: '회생용수', type: 'red-item', values: ['15.8', '16.7', '17.8', '15.7', '14.4', '14.0'] },
{ label: '장내급수도', type: 'red-item', values: ['2.4', '-', '-', '-', '-', '-'] },
{ label: '장내기타용수', type: 'red-item', values: ['261.3', '260.6', '255.0', '261.1', '231.8', '222.6'] },
{ label: '장외 재이용', type: 'category', values: ['592.1', '628.9', '630.8', '606.7', '634.6', '647.5'] },
{ label: '청소·화장실용수', type: 'blue-item', values: ['0.6', '0.4', '0.4', '0.4', '0.4', '0.4'] },
{ label: '세척·살수용수', type: 'blue-item', values: ['5.6', '9.9', '2.9', '2.1', '3.0', '3.6'] },
{ label: '조경용수', type: 'blue-item', values: ['10.3', '16.4', '5.9', '4.5', '8.9', '9.5'] },
{ label: '친수용수', type: 'blue-item', values: ['7.8', '3.1', '3.1', '3.0', '10.9', '10.5'] },
{ label: '하천유지용수', type: 'blue-item', values: ['480.5', '482.5', '496.2', '442.7', '471.7', '484.0'] },
{ label: '농업용수', type: 'blue-item', values: ['12.2', '11.9', '13.4', '10.7', '10.6', '11.4'] },
{ label: '지하수 충전', type: 'blue-item', values: ['0.0', '0.0', '0.0', '13.4', '0.0', '0.0'] },
{ label: '공업용수', type: 'blue-item', values: ['75.2', '104.6', '108.9', '129.9', '129.0', '128.1'] },
].map((row, idx) => {
const isSubItem = row.type === 'red-item' || row.type === 'blue-item';
const isLastInGroup = idx === 8 || idx === 17;
return (
<tr key={idx} className="bg-white">
<td className={`${tableStyles.td} relative`}>
{isSubItem ? (
<>
<div className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
<div className={`absolute left-[16px] w-[4px] z-20 ${row.type === 'red-item' ? 'bg-[#FF8B8B]' : 'bg-[#4ECBFF]'} top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
<span className="text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]">{row.label}</span>
</>
) : (
<span className="font-bold text-neutral-900 block w-full text-center">{row.label}</span>
)}
</td>
{row.values.map((val, i) => (
<td key={i} className={`${tableStyles.td} ${[0, 2, 4].includes(i) ? "bg-[#EAF5F5]" : ""}`}>
return (
<tr key={idx} className='bg-white'>
<td className={`${tableStyles.td} relative`}>
{isSubItem ? (
<>
<div className={`absolute left-[1px] w-[16px] bg-white z-10 top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`} />
<div
className={`absolute left-[16px] w-[4px] z-20 ${row.type === 'red-item' ? 'bg-[#FF8B8B]' : 'bg-[#4ECBFF]'} top-[-2px] ${isLastInGroup ? 'bottom-[0px]' : 'bottom-[-2px]'}`}
/>
<span className='text-[#4f4f4f] font-bold block w-full text-center relative z-30 translate-x-[8px]'>{row.label}</span>
</>
) : (
<span className='font-bold text-neutral-900 block w-full text-center'>{row.label}</span>
)}
</td>
{row.values.map((val, i) => (
<td key={i} className={`${tableStyles.td} ${[0, 2, 4].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
{val}
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
<SourceText />
</div>
</section>
<Hr className='my-11' />
{/* 3) 하수처리시설 설치 및 이용 현황 */}
<section>
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
<div className='mt-8'>
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: '130px' }} />
<col style={{ width: '160px' }} />
<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>", '25,846', '26,067', '26,244', '26,492', '26,586', '26,766'],
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '1,112.7', '1,148.7', '1,144.7', '1,127.0', '1,136.8', '1,137.5'],
],
},
{
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>", '3,088.9', '3,359.2', '3,612.6', '4,412.2', '4,692.3', '5,503.8'],
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '162.2', '190.6', '218.5', '244.6', '247.2', '324.9'],
],
},
{
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>", '22,757', '22,708', '22,631', '22,080', '21,894', '21,262'],
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", '950.6', '958.1', '926.2', '882.4', '889.5', '812.7'],
],
},
].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);
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>
</section>
<Hr className='my-11' />
{/* 4) 시도별 하수처리수 재이용 현황 */}
<section className='pb-[200px]'>
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
<div className='mt-8'>
<div className={tableStyles.container}>
<div className='text-right text-[13px] mb-2 text-neutral-600'>(단위: 백만m³/년)</div>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: '160px' }} />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
</colgroup>
<thead>
<tr>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
시도
</th>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
연간
<br />
하수
<br />
처리량
</th>
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>
재이용현황
</th>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
재이용률
<br />
(%)
</th>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
비고
</th>
</tr>
<tr>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
계
</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>
장내
</th>
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}>
장외
</th>
</tr>
<tr>
{/* 💡 하천유지용수는 14px, 나머지 하위 헤더는 16px로 적용 */}
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>소계</th>
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>하천유지용수</th>
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>공업용수</th>
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>농업용수</th>
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>기타용수</th>
</tr>
</thead>
<tbody>
{[
['합계', '7,537.2', '1,137.5', '490.0', '647.5', '484.0', '128.1', '11.4', '24.0', '15.1%', ''],
['서울특별시', '1,567.0', '114.9', '59.3', '55.6', '55.1', '-', '-', '0.5', '7.3%', ''],
['부산광역시', '491.5', '146.2', '120.7', '25.5', '25.3', '-', '-', '0.2', '29.7%', ''],
['대구광역시', '432.7', '102.4', '38.2', '64.2', '64.1', '-', '-', '0.1', '23.7%', ''],
['인천광역시', '302.3', '57.7', '11.6', '46.1', '29.2', '12.1', '0.5', '4.3', '19.1%', ''],
['광주광역시', '211.1', '28.7', '6.5', '22.3', '22.2', '-', '-', '0.1', '13.6%', ''],
['대전광역시', '244.6', '11.9', '11.9', '0.68', '0.0', '-', '-', '0.1', '4.9%', ''],
['울산광역시', '235.1', '36.1', '6.1', '29.9', '29.7', '0.2', '-', '-', '15.3%', ''],
['세종특별자치시', '38.5', '2.5', '2.0', '0.5', '0.4', '-', '0.2', '-', '6.6%', ''],
['경기도', '1,926.8', '313.3', '122.2', '191.0', '153.0', '33.3', '0.4', '4.3', '16.3%', ''],
['강원도', '230.4', '19.6', '15.4', '4.2', '2.1', '-', '-', '2.1', '8.5%', ''],
['충청북도', '190.9', '15.6', '6.3', '9.2', '7.4', '0.6', '-', '1.2', '8.2%', ''],
['충청남도', '241.5', '41.4', '9.8', '31.5', '17.7', '8.6', '4.5', '0.8', '17.1%', ''],
['전라북도', '292.4', '42.7', '7.5', '35.1', '33.5', '0.2', '1.0', '0.5', '14.6%', ''],
['전라남도', '254.0', '23.6', '12.3', '11.3', '8.7', '0.0', '1.0', '1.6', '9.3%', ''],
['경상북도', '378.4', '129.1', '33.7', '95.3', '20.8', '72.5', '0.8', '1.3', '34.1%', ''],
['경상남도', '411.3', '50.1', '25.0', '25.0', '14.7', '0.5', '3.0', '6.9', '12.2%', ''],
['제주특별자치도', '88.6', '1.7', '1.5', '0.12', '-', '-', '0.1', '-', '1.9%', ''],
].map((row, idx) => (
<tr key={idx} className='bg-white'>
{/* 💡 연간하수처리량(index 1), 장내(index 3), 재이용률(index 9) 컬러 하이라이트 적용 */}
{row.map((val, i) => (
<td key={i} className={`${tableStyles.td} ${i === 0 ? '!font-bold !text-neutral-900' : ''} ${[1, 3, 9].includes(i) ? 'bg-[#EAF5F5]' : ''}`}>
{val}
</td>
))}
</tr>
);
})}
</tbody>
</table>
))}
</tbody>
</table>
</div>
<SourceText />
</div>
<SourceText />
</div>
</section>
<Hr className="my-11" />
{/* 3) 하수처리시설 설치 및 이용 현황 */}
<section>
<SectionTitle>하수처리시설 설치 및 이용 현황</SectionTitle>
<div className="mt-8">
<div className={tableStyles.container}>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: '130px' }} />
<col style={{ width: '160px' }} />
<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>", "25,846", "26,067", "26,244", "26,492", "26,586", "26,766"],
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", "1,112.7", "1,148.7", "1,144.7", "1,127.0", "1,136.8", "1,137.5"],
]
},
{
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>", "3,088.9", "3,359.2", "3,612.6", "4,412.2", "4,692.3", "5,503.8"],
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", "162.2", "190.6", "218.5", "244.6", "247.2", "324.9"],
]
},
{
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>", "22,757", "22,708", "22,631", "22,080", "21,894", "21,262"],
["처리용량 <span class='text-[13px] font-normal text-[#767676]'>(만m³/일)</span>", "950.6", "958.1", "926.2", "882.4", "889.5", "812.7"],
]
}
].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);
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>
</section>
<Hr className="my-11" />
{/* 4) 시도별 하수처리수 재이용 현황 */}
<section className="pb-[200px]">
<SectionTitle>시도별 하수처리수 재이용 현황</SectionTitle>
<div className="mt-8">
<div className={tableStyles.container}>
<div className="text-right text-[13px] mb-2 text-neutral-600">(단위: 백만m³/일)</div>
<table className={tableStyles.table}>
<colgroup>
<col style={{ width: '160px' }} />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col />
</colgroup>
<thead>
<tr>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>시도</th>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>연간<br/>하수<br/>처리량</th>
<th colSpan={7} className={`${tableStyles.th} ${tableStyles.headerBg} py-2`}>재이용현황</th>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>재이용률<br/>(%)</th>
<th rowSpan={3} className={`${tableStyles.th} ${tableStyles.headerBg}`}>비고</th>
</tr>
<tr>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>계</th>
<th rowSpan={2} className={`${tableStyles.th} ${tableStyles.headerBg}`}>장내</th>
<th colSpan={5} className={`${tableStyles.th} ${tableStyles.headerBg} py-1`}>장외</th>
</tr>
<tr>
{/* 💡 하천유지용수는 14px, 나머지 하위 헤더는 16px로 적용 */}
<th className={`${tableStyles.th} bg-[#01ACE5] text-white py-2 text-[16px]`}>소계</th>
<th className={`${tableStyles.th} bg-[#1FB1C2] text-white py-2 text-[13px]`}>하천유지용수</th>
<th className={`${tableStyles.th} bg-[#3EB59F] text-white py-2 text-[16px]`}>공업용수</th>
<th className={`${tableStyles.th} bg-[#5DB97C] text-white py-2 text-[16px]`}>농업용수</th>
<th className={`${tableStyles.th} bg-[#6DBB4F] text-white py-2 text-[16px]`}>기타용수</th>
</tr>
</thead>
<tbody>
{[
["합계", "7,537.2", "1,137.5", "490.0", "647.5", "484.0", "128.1", "11.4", "24.0", "15.1%", ""],
["서울특별시", "1,567.0", "114.9", "59.3", "55.6", "55.1", "-", "-", "0.5", "7.3%", ""],
["부산광역시", "491.5", "146.2", "120.7", "25.5", "25.3", "-", "-", "0.2", "29.7%", ""],
["대구광역시", "432.7", "102.4", "38.2", "64.2", "64.1", "-", "-", "0.1", "23.7%", ""],
["인천광역시", "302.3", "57.7", "11.6", "46.1", "29.2", "12.1", "0.5", "4.3", "19.1%", ""],
["광주광역시", "211.1", "28.7", "6.5", "22.3", "22.2", "-", "-", "0.1", "13.6%", ""],
["대전광역시", "244.6", "11.9", "11.9", "0.68", "0.0", "-", "-", "0.1", "4.9%", ""],
["울산광역시", "235.1", "36.1", "6.1", "29.9", "29.7", "0.2", "-", "-", "15.3%", ""],
["세종특별자치시", "38.5", "2.5", "2.0", "0.5", "0.4", "-", "0.2", "-", "6.6%", ""],
["경기도", "1,926.8", "313.3", "122.2", "191.0", "153.0", "33.3", "0.4", "4.3", "16.3%", ""],
["강원도", "230.4", "19.6", "15.4", "4.2", "2.1", "-", "-", "2.1", "8.5%", ""],
["충청북도", "190.9", "15.6", "6.3", "9.2", "7.4", "0.6", "-", "1.2", "8.2%", ""],
["충청남도", "241.5", "41.4", "9.8", "31.5", "17.7", "8.6", "4.5", "0.8", "17.1%", ""],
["전라북도", "292.4", "42.7", "7.5", "35.1", "33.5", "0.2", "1.0", "0.5", "14.6%", ""],
["전라남도", "254.0", "23.6", "12.3", "11.3", "8.7", "0.0", "1.0", "1.6", "9.3%", ""],
["경상북도", "378.4", "129.1", "33.7", "95.3", "20.8", "72.5", "0.8", "1.3", "34.1%", ""],
["경상남도", "411.3", "50.1", "25.0", "25.0", "14.7", "0.5", "3.0", "6.9", "12.2%", ""],
["제주특별자치도", "88.6", "1.7", "1.5", "0.12", "-", "-", "0.1", "-", "1.9%", ""],
].map((row, idx) => (
<tr key={idx} className="bg-white">
{/* 💡 연간하수처리량(index 1), 장내(index 3), 재이용률(index 9) 컬러 하이라이트 적용 */}
{row.map((val, i) => (
<td
key={i}
className={`${tableStyles.td} ${i === 0 ? "!font-bold !text-neutral-900" : ""} ${[1, 3, 9].includes(i) ? "bg-[#EAF5F5]" : ""}`}
>
{val}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
<SourceText />
</div>
</section>
</section>
</div>
</div>
);
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 24 KiB

+33
View File
@@ -6,6 +6,39 @@
현재 목표는 리팩토링 전용 작업이 아니라, 하수관로 운영관리 시스템 2차사업 고도화를 수행하는 것이다.
즉, 기존 `sn` 화면과 흐름을 안정적으로 유지하면서 2차 요구사항에 해당하는 화면 개발, 기능 확장, 백엔드 연동 보완을 수행해야 한다.
## 사용자 작업 범위: UI/UX 및 퍼블리싱
현재 사용자와 진행하는 작업의 기본 범위는 UI/UX 및 프론트엔드 퍼블리싱으로 제한한다.
사용자가 별도로 명시하지 않는 한 아래 기준을 모든 후속 작업에 적용한다.
### 허용 범위
- 화면 레이아웃과 시각적 위계 조정
- JSX 마크업, Tailwind CSS, CSS 스타일 수정
- 색상, 여백, 간격, 크기, 타이포그래피, 아이콘, 테두리, 배경 조정
- 반응형 화면, 접근성, 포커스와 시각적 상태 개선
- 표시 전용 컴포넌트 구성과 퍼블리싱 품질 개선
- 기존 화면 내용과 동작을 유지하는 범위의 UI 구조 정리
### 기본 금지 범위
- 백엔드 코드, API 계약, 요청·응답 데이터 구조 변경
- `.env*`, middleware, 프록시, 인증과 권한 설정 변경
- 업무 로직, 상태 모델, 데이터 가공 및 저장 흐름 변경
- 데이터베이스, 서버 프로세스와 배포 설정 변경
- 패키지 설치·업데이트와 의존성 구조 변경
- 사용자 요청 없이 화면의 문구, 데이터 항목, 기능 동작 변경
금지 범위의 변경이 필요해 보이면 직접 수정하지 않고 원인과 필요한 작업만 사용자에게 설명한다.
### UI 검증 안전 기준
- 사용자가 실행한 개발 서버가 있는지 실제 앱 포트까지 먼저 확인한다.
- 개발 서버 실행 중에는 같은 앱의 `next build`를 실행하지 않는다.
- UI 작업의 기본 검증은 실행 중인 개발 서버의 HMR, 브라우저 화면 확인, `git diff --check`로 제한한다.
- 개발 서버 종료, `.next` 정리, 전체 빌드와 프로세스 재시작은 사용자가 명시적으로 승인한 경우에만 수행한다.
- UI 변경 전후에 기존 문구, 데이터, 이벤트와 업무 동작이 유지되는지 확인한다.
---
## 공통 시작 기준
@@ -54,23 +54,18 @@ export default function AgedPipeLayerSettingPanel({
};
const inputClass =
"h-[20px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
const numberInputClass = inputClass + " w-[40px]";
"h-[26px] !rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground";
const numberInputClass = inputClass + " min-w-0 flex-1";
const applyButtonClass =
"grid h-[16px] w-[16px] shrink-0 place-items-center rounded-full bg-primary text-[0.85rem] font-bold leading-none text-primary-foreground";
const indicator = (
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
);
"h-[26px] shrink-0 rounded border border-[#A9C5E5] bg-[#EDF4FC] px-2 py-0 text-[12px] font-medium leading-none text-[#2463A6]";
const settingRow = (
<div className="flex min-h-[26px] items-center gap-1 whitespace-nowrap text-[1.15rem] font-medium text-label">
<div className="flex min-w-0 items-center gap-1">
{indicator}
<div className="flex min-h-[26px] flex-col items-stretch gap-1.5 whitespace-nowrap text-[1.15rem] font-medium text-label">
<div className="flex min-w-0 items-center">
<label htmlFor="aged-pipe-elapsed-years" className="shrink-0">
노후관로 경과년도(년)
</label>
</div>
<div className="ml-auto flex shrink-0 items-center gap-1">
<div className="flex w-full items-center gap-1">
<input
id="aged-pipe-elapsed-years"
className={numberInputClass}
@@ -95,7 +90,7 @@ export default function AgedPipeLayerSettingPanel({
title="적용"
aria-label="경과년도 적용"
>
<span className="relative left-[1px]">✓</span>
확인
</button>
</div>
</div>
@@ -3,7 +3,6 @@
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
import {
ChartColumnBig,
ChevronDown,
ChevronUp,
PanelsTopLeft,
PencilRuler,
@@ -32,10 +31,10 @@ type MenuIconProps = {
isActive: boolean;
};
const InftwtMenuIcon = ({ isActive }: MenuIconProps) => (
const InftwtMenuIcon = () => (
<svg
viewBox="0 0 24 24"
className={`h-[15px] w-[15px] shrink-0 ${isActive ? "text-primary" : "text-label"}`}
className="h-[15px] w-[15px] shrink-0 text-current"
aria-hidden="true"
fill="none"
>
@@ -167,31 +166,44 @@ export default function AnalysisPopupMenu({
}, [initialActiveKey]);
return (
<div className="absolute right-[15px] top-[8px] z-[45]">
<div className="flex w-[154px] flex-col overflow-hidden rounded-[6px] border border-border bg-background shadow-[0_8px_18px_rgba(15,23,42,0.14)]">
<div className="flex h-[36px] shrink-0 items-center justify-between gap-2 border-b border-border bg-secondary pl-[10px] pr-[4px] text-[13px] font-bold text-foreground">
<div className="flex items-center gap-2">
<PanelsTopLeft className="h-4 w-4 text-primary" />
<span>분석도구</span>
<div className="pointer-events-none absolute right-[68px] top-[16px] z-[45] flex flex-col items-end gap-[8px]">
<button
type="button"
className={`pointer-events-auto flex h-[40px] w-[40px] shrink-0 items-center justify-center overflow-hidden rounded-[3px] p-0 shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)] ${
isOpen
? "bg-[#0279FA] text-white"
: "bg-white text-[#666666] hover:bg-[#0279FA] hover:text-white"
}`}
onClick={() => setIsOpen((value) => !value)}
title={isOpen ? "분석 메뉴 숨기기" : "분석도구"}
aria-label={isOpen ? "분석 메뉴 숨기기" : "분석 메뉴 보이기"}
aria-expanded={isOpen}
aria-controls="analysis-popup-menu"
>
<PanelsTopLeft className="h-[20px] w-[20px]" aria-hidden="true" />
</button>
{isOpen ? (
<div
id="analysis-popup-menu"
className="pointer-events-auto flex w-max min-w-[154px] flex-col overflow-hidden rounded-[6px] bg-background shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)]"
>
<div className="flex h-[36px] shrink-0 items-center justify-between gap-2 border-b border-border bg-white pl-[10px] pr-[4px] text-[13px] font-bold text-foreground">
<div className="flex items-center gap-2">
<PanelsTopLeft className="h-4 w-4 text-primary" />
<span>분석도구</span>
</div>
<button
type="button"
className="flex h-[28px] w-[28px] shrink-0 items-center justify-center rounded-[4px] text-label hover:bg-accent hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#0279FA]"
onClick={() => setIsOpen(false)}
title="분석 메뉴 숨기기"
aria-label="분석 메뉴 숨기기"
>
<ChevronUp className="h-4 w-4" aria-hidden="true" />
</button>
</div>
<button
type="button"
className="flex h-[28px] w-[28px] shrink-0 items-center justify-center rounded-[4px] text-label transition hover:bg-accent hover:text-primary"
onClick={() => setIsOpen((value) => !value)}
title={isOpen ? "분석 메뉴 숨기기" : "분석 메뉴 보이기"}
aria-label={isOpen ? "분석 메뉴 숨기기" : "분석 메뉴 보이기"}
aria-expanded={isOpen}
>
{isOpen ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</button>
</div>
{isOpen ? (
<div className="flex flex-col gap-[3px] p-[6px]">
{ANALYSIS_POPUP_ITEMS.map((item) => {
const Icon = item.icon;
@@ -208,7 +220,7 @@ export default function AnalysisPopupMenu({
<button
key={item.key}
type="button"
className={`relative flex h-[33px] items-center gap-[7px] rounded-[5px] border px-[8px] pl-[10px] text-left text-[12px] font-semibold transition ${
className={`relative flex h-[33px] items-center gap-[7px] rounded-[5px] border px-[8px] pl-[10px] text-left text-[12px] font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#0279FA] ${
isActive
? "border-primary bg-secondary font-bold text-primary before:absolute before:bottom-[7px] before:left-0 before:top-[7px] before:w-[3px] before:rounded-r-[2px] before:bg-primary"
: "border-transparent bg-background text-foreground hover:border-border hover:bg-accent hover:text-primary"
@@ -244,19 +256,17 @@ export default function AnalysisPopupMenu({
>
{item.renderIcon ? item.renderIcon({ isActive }) : null}
{Icon ? (
<Icon
className={`h-[15px] w-[15px] shrink-0 ${
isActive ? "text-primary" : "text-label"
}`}
/>
<Icon className="h-[15px] w-[15px] shrink-0 text-current" />
) : null}
<span className="whitespace-nowrap">{label}</span>
<span className="whitespace-nowrap" title={label}>
{label}
</span>
</button>
);
})}
</div>
) : null}
</div>
</div>
) : null}
</div>
);
}
@@ -30,7 +30,7 @@ export default function ExmnLayerSettingPanel({
});
};
const selectClass =
"h-[20px] w-[62px] rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground outline-none focus:border-primary";
"h-[26px] w-full rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground outline-none focus:border-primary";
const labelClass = "shrink-0 text-[1.15rem] font-medium text-label";
const title = jobType === "PPL" ? "관로조사" : "맨홀조사";
@@ -41,8 +41,8 @@ export default function ExmnLayerSettingPanel({
onEnabledChange={onEnabledChange}
onClose={onClose}
>
<div className="space-y-[1px]">
<label className="flex min-h-[24px] items-center justify-between ">
<div className="space-y-[6px]">
<label className="flex min-h-[24px] flex-col items-stretch gap-1">
<span className={`flex items-center gap-1 ${labelClass}`}>
<span
className={`h-2 w-2 rounded-full ${jobType === "PPL" ? "bg-[#facc15]" : "bg-[#8b5cf6]"}`}
@@ -59,7 +59,7 @@ export default function ExmnLayerSettingPanel({
onChange={(event) => updateFilter("bizCycl", event.target.value)}
/>
</label>
<label className="flex min-h-[24px] items-center justify-between ">
<label className="flex min-h-[24px] flex-col items-stretch gap-1">
<span className={labelClass}>구조적 상태등급</span>
<select
className={selectClass}
@@ -75,7 +75,7 @@ export default function ExmnLayerSettingPanel({
))}
</select>
</label>
<label className="flex min-h-[24px] items-center justify-between ">
<label className="flex min-h-[24px] flex-col items-stretch gap-1">
<span className={labelClass}>운영적 상태등급</span>
<select
className={selectClass}
@@ -6,6 +6,7 @@ import {
normalizeFloodHistoryYearRange,
type FloodHistoryYearRange,
} from "./fldngHstryTypes";
import LayerSettingCard from "../layerSetting/LayerSettingCard";
type Props = {
yearRange: FloodHistoryYearRange;
@@ -36,7 +37,7 @@ export default function FldngHstryLayerSettingPanel({
.filter((year) => Number(year) >= Number(normalizedRange.startYear))
.map((year) => ({ val: year, text: year }));
const yearSelectClass =
"h-[20px] w-[62px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
"!h-[26px] w-full !rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground";
const changeYear = (key: keyof FloodHistoryYearRange, value: string) => {
onYearRangeChange(
@@ -48,41 +49,16 @@ export default function FldngHstryLayerSettingPanel({
};
return (
<div className="w-full overflow-visible rounded-[6px] border border-border bg-background shadow-blur">
<div className="flex min-h-[28px] items-center justify-between border-b border-border px-2">
<span className="text-[1.15rem] font-semibold text-label">
침수흔적도
</span>
<div className="flex shrink-0 items-center gap-2">
<button
type="button"
role="switch"
aria-checked={enabled}
aria-label="침수년도 필터 적용 여부"
title={enabled ? "침수년도 필터 적용 중" : "침수년도 필터 미적용"}
className={`relative h-[16px] w-[28px] rounded-full border transition-colors ${enabled ? "border-primary bg-primary" : "border-[#94a3b8] bg-[#cbd5e1]"}`}
onClick={() => onEnabledChange(!enabled)}
>
<span
className={`absolute top-1/2 h-[12px] w-[12px] -translate-y-1/2 rounded-full bg-white shadow-sm transition-[left] ${enabled ? "left-[14px]" : "left-[2px]"}`}
/>
</button>
<button
type="button"
aria-label="침수년도 설정 닫기"
title="닫기"
className="grid h-[20px] w-[20px] place-items-center bg-transparent text-[1.3rem] leading-none text-muted-foreground hover:text-foreground"
onClick={onClose}
>
×
</button>
</div>
</div>
<div className="flex min-h-[30px] items-center gap-1.5 px-2 py-1 text-[1.15rem] font-medium text-label">
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
<LayerSettingCard
title="침수흔적도"
enabled={enabled}
onEnabledChange={onEnabledChange}
onClose={onClose}
>
<div className="flex flex-col gap-1.5 font-medium text-label">
<span className="shrink-0">침수년도</span>
<div className="ml-auto flex shrink-0 items-center gap-1.5">
<div className="w-[62px]">
<div className="flex w-full items-center gap-1.5">
<div className="min-w-0 flex-1">
<SelectBox
className="w-full"
selectClass={yearSelectClass}
@@ -97,7 +73,7 @@ export default function FldngHstryLayerSettingPanel({
<span className="px-[1px] text-[0.95rem] font-normal text-muted-foreground">
~
</span>
<div className="w-[62px]">
<div className="min-w-0 flex-1">
<SelectBox
className="w-full"
selectClass={yearSelectClass}
@@ -111,6 +87,6 @@ export default function FldngHstryLayerSettingPanel({
</div>
</div>
</div>
</div>
</LayerSettingCard>
);
}
@@ -55,7 +55,7 @@ export default function GisEditToolbar({
data-testid="gis-edit-toolbar"
className="absolute bottom-4 left-1/2 z-[60] flex w-[min(980px,calc(100%-2rem))] -translate-x-1/2 flex-col"
>
<div className="card card-border !m-0 flex !bg-background !p-[0.8rem] shadow-[0_8px_24px_rgba(23,42,70,0.18)]">
<div className="card !m-0 flex !border-0 !bg-background !p-[0.8rem] shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)]">
<div className="flex w-full items-center gap-3 overflow-hidden">
<div className="flex w-[168px] shrink-0 flex-col justify-center border-r border-border pr-3">
<span className="whitespace-nowrap text-[1.3rem] font-semibold leading-5 text-label">
@@ -22,8 +22,16 @@ export default function LayerSettingCard({
}: Props) {
return (
<div className="w-full overflow-visible rounded-[6px] border border-border bg-background shadow-blur">
<div className="flex min-h-[28px] items-center justify-between border-b border-border px-2">
<span className="text-[1.15rem] font-semibold text-label">{title}</span>
<div className="flex min-h-[28px] items-center justify-between border-b border-border pl-[0.8rem] pr-2">
<div className="flex min-w-0 items-center gap-2">
<span
aria-hidden="true"
className="h-[7px] w-[7px] shrink-0 rotate-45 bg-[#7CCE5C]"
/>
<span className="truncate text-[12px] font-semibold text-label">
{title}
</span>
</div>
<div className="flex shrink-0 items-center gap-2">
{showToggle ? (
<button
@@ -52,7 +60,9 @@ export default function LayerSettingCard({
</div>
</div>
{children ? (
<div className="px-2 py-1 text-[1.15rem] text-label">{children}</div>
<div className="flex flex-col gap-[6px] p-[0.8rem] text-[1.15rem] text-label">
{children}
</div>
) : null}
</div>
);
@@ -101,7 +101,7 @@ export default function SinkholeLayerSettingPanel({
const currentYear = new Date().getFullYear();
const yearSelectClass =
"h-[20px] !w-[58px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
"!h-[26px] !w-full !rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground";
const startYearOptions = Array.from(
{ length: currentYear - SINKHOLE_MIN_SEARCH_YEAR + 1 },
(_, index) => String(currentYear - index),
@@ -124,24 +124,19 @@ export default function SinkholeLayerSettingPanel({
.map((year) => ({ val: year, text: year }));
const inputClass =
"h-[20px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
const numberInputClass = inputClass + " w-[40px]";
"h-[26px] !rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground";
const numberInputClass = inputClass + " min-w-0 flex-1";
const applyButtonClass =
"grid h-[16px] w-[16px] shrink-0 place-items-center rounded-full bg-primary text-[0.85rem] font-bold leading-none text-primary-foreground";
const indicator = (
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
);
"h-[26px] shrink-0 rounded border border-[#A9C5E5] bg-[#EDF4FC] px-2 py-0 text-[12px] font-medium leading-none text-[#2463A6]";
const settingRows = (
<div className="space-y-1.5 text-[1.15rem] font-medium text-label">
<div className="flex min-h-[26px] items-center gap-1.5 whitespace-nowrap">
<div className="flex min-w-0 items-center gap-1">
{indicator}
<div className="flex min-h-[26px] flex-col items-stretch gap-1.5 whitespace-nowrap">
<div className="flex min-w-0 items-center">
<label htmlFor="sinkhole-radius" className="shrink-0">
지반침하반경(m)
</label>
</div>
<div className="ml-auto flex shrink-0 items-center gap-1">
<div className="flex w-full items-center gap-1">
<input
id="sinkhole-radius"
className={numberInputClass}
@@ -165,19 +160,18 @@ export default function SinkholeLayerSettingPanel({
title="적용"
aria-label="반경 적용"
>
<span className="relative left-[1px]">✓</span>
확인
</button>
</div>
</div>
<div className="flex min-h-[26px] items-center gap-1.5 whitespace-nowrap">
<div className="flex min-w-0 items-center gap-1">
{indicator}
<div className="flex min-h-[26px] flex-col items-stretch gap-1.5 whitespace-nowrap">
<div className="flex min-w-0 items-center">
<label htmlFor="sinkhole-start-year" className="shrink-0">
발생년도
</label>
</div>
<div className="ml-auto flex shrink-0 items-center gap-1.5">
<div id="sinkhole-start-year" className="w-[62px]">
<div className="flex w-full items-center gap-1.5">
<div id="sinkhole-start-year" className="min-w-0 flex-1">
<SelectBox
className="w-full"
selectClass={yearSelectClass}
@@ -194,7 +188,7 @@ export default function SinkholeLayerSettingPanel({
<span className="px-[1px] text-[0.95rem] font-normal text-muted-foreground">
~
</span>
<div id="sinkhole-end-year" className="w-[62px]">
<div id="sinkhole-end-year" className="min-w-0 flex-1">
<SelectBox
className="w-full"
selectClass={yearSelectClass}
@@ -215,7 +209,7 @@ export default function SinkholeLayerSettingPanel({
title="적용"
aria-label="연도범위 적용"
>
<span className="relative left-[1px]">✓</span>
확인
</button>
</div>
</div>
+50 -18
View File
@@ -18,6 +18,7 @@
"use client";
import dynamic from "next/dynamic";
import { ListTree } from "lucide-react";
import {
Fragment,
useCallback,
@@ -6764,17 +6765,17 @@ const Map = () => {
showToggle={false}
>
{isWaterDepthLegendEnabled ? (
<div className="space-y-1">
<div className="space-y-[6px]">
{waterDepthLegendItems.map((item) => (
<div key={item.key} className="flex items-center gap-2">
<span
className="inline-block h-4 w-10 shrink-0"
className="inline-block h-6 w-10 shrink-0"
style={{
backgroundColor: item.color,
border: `1px solid ${item.borderColor}`,
}}
/>
<span className="text-[1rem] font-medium text-label">
<span className="text-[11px] font-medium text-label">
{item.label}
</span>
</div>
@@ -6852,16 +6853,19 @@ const Map = () => {
if (activeLayerSettingItems.length === 0) return null;
return (
<div className="mt-3 flex h-[34vh] max-h-[32rem] min-h-[24rem] flex-col overflow-hidden rounded-sm border border-border bg-background shadow-[0_4px_14px_rgba(15,23,42,0.08)]">
<div className="border-b border-border bg-secondary px-3 py-2">
<span className="text-[1.2rem] font-semibold text-label">
<div className="flex h-[34vh] max-h-[32rem] min-h-[24rem] flex-col overflow-hidden rounded-[6px] border border-border bg-background shadow-[0_4px_14px_rgba(15,23,42,0.08)]">
<div className="flex h-[38px] shrink-0 items-center gap-2 border-b border-border px-3">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm bg-[#EDF8EA] text-[#65B84A]">
<ListTree size={14} strokeWidth={2} />
</span>
<span className="text-[1.4rem] font-bold text-foreground">
선택 레이어 설정
</span>
</div>
<div
ref={restoreActiveLayerSettingScroll}
onScroll={handleActiveLayerSettingScroll}
className="min-h-0 flex-1 overflow-y-scroll px-1 py-2 [scrollbar-gutter:stable] [scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]"
className="min-h-0 flex-1 overflow-y-scroll px-2 py-2 [scrollbar-gutter:stable] [scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]"
>
<div className="flex flex-col gap-2">
{activeLayerSettingItems.map((item) => (
@@ -9379,7 +9383,7 @@ const Map = () => {
>
<GisEditSessionPanel
{...gisEditSessionSnapshot}
className="max-h-[calc(50dvh-82px)]"
className="max-h-[calc(50dvh-82px)] !border-0 !shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)]"
variant="inline"
onOpenPopup={handleOpenGisEditSessionPopup}
onRemoveFocusedChange={gisEdit.removeFocusedChange}
@@ -9423,10 +9427,13 @@ const Map = () => {
setIsSearchModalOpen={setIsSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
onOpenPopup={handleOpenLayerPopup}
searchBarContainerClassName="!top-[16px] ml-[12px]"
alwaysVisibleMapTabs={true}
searchBar={
<ProcSearchBar
gisMaps={gisMaps}
facilityTypes={facilityTypes}
integrated
sidoSearchZoom={
isAgedPipeChecked ? AGED_PIPE_GUIDE_MIN_ZOOM : undefined
}
@@ -9435,15 +9442,17 @@ const Map = () => {
}
waterDepth={undefined}
layerSettingView={ActiveLayerSettingView}
treeSectionClassName="rounded-sm border border-border bg-background px-1 py-2 text-foreground"
treeSectionClassName="px-1 py-2 text-foreground"
detachedTreeToggleButton={true}
showLayerPanelTitle={false}
sectionedTreePanel={true}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying as any}
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
checkInterval={2500}
checkedLayerChangeDelay={0}
showZoomLevel={true}
toolBarIconSize={20}
showZoomScaleInfo={true}
showFloatingSearchButton={false}
treeTogglePosition="center"
@@ -9497,9 +9506,19 @@ const Map = () => {
)}
<style jsx global>{`
.sn-map-control-layout .mapbtn {
top: auto;
right: 15px;
bottom: 439px;
top: 16px;
right: 120px;
bottom: auto;
}
.sn-map-control-layout .map-type-tabs,
.sn-map-control-layout .tool-bar ul,
.sn-map-control-layout .zoom-level-panel,
.sn-map-control-layout .zoom-level-summary {
border: 0;
box-shadow:
0 1px 2px rgba(60, 64, 67, 0.3),
0 1px 3px 1px rgba(60, 64, 67, 0.15);
}
.sn-map-control-layout .custom-toolbar {
@@ -9507,19 +9526,32 @@ const Map = () => {
}
.sn-map-control-layout .tool-bar {
top: auto;
right: 15px;
bottom: 230px;
top: 16px;
right: 16px;
bottom: auto;
}
.sn-map-control-layout .tool-bar-vector-icons ul li div {
display: flex;
align-items: center;
justify-content: center;
background-image: none !important;
color: #666;
line-height: 1;
}
.sn-map-control-layout .tool-bar-vector-icons ul li div:hover {
color: #fff;
}
.sn-map-control-layout .zoom-level-control {
right: 15px;
bottom: 36px;
right: 16px;
bottom: 16px;
}
.sn-map-control-layout .zoom-level-summary {
right: 56px;
bottom: -15px;
bottom: 0;
}
.sn-map-control-layout .ol-scale-line {
@@ -0,0 +1,79 @@
# FE-MAP 레이어 패널 헤더 구현 기록
## 작업 목적
- 사용자 제공 기준 이미지에 맞춰 지도 좌측 패널을 `레이어 관리`와 `선택 레이어 설정` 두 영역으로 구분한다.
- 레이어 트리, 체크 상태, 선택 레이어별 설정 내용과 동작은 변경하지 않는다.
## 적용 기준
- 지도형 화면은 기존 `Gis` 조합을 유지한다.
- 사용자 지정 스타일은 패널 외곽 여백 `4px`, 영역 사이 간격 `6px`, 패널 배경 `#E5E5E5`이다.
- 두 영역은 흰색 카드와 제목 헤더로 구분한다.
## 수정 대상
- `apps/sn/app/(map-layout)/map/page.tsx`
- `packages/common/business_components/src/ui/map/Gis.js`
- `packages/common/business_components/src/ui/map/GisTree.js`
## 변경 내용
- 공통 GIS 패널에 기본값이 비활성인 섹션형 패널 옵션을 추가했다.
- `sn` 관망도에서만 옵션을 활성화해 다른 앱의 기존 GIS 패널에는 영향을 주지 않는다.
- `레이어 관리`와 `선택 레이어 설정`에 구분 아이콘과 고정 높이 헤더를 적용했다.
- 두 카드 모두 헤더와 스크롤 본문을 형제 영역으로 분리해 헤더 아래에서만 콘텐츠가 스크롤되도록 통일했다.
- 두 스크롤바는 카드 본문 오른쪽 끝에 배치하고 동일한 `4px` 두께와 안정적인 스크롤 여백을 적용했다.
- 레이어 트리의 목록 액션은 아이콘 전용 표시를 해제하고 기존 `목록보기` 텍스트 버튼으로 복원했다.
- 선택 레이어 설정 카드 제목 왼쪽에는 `#7CCE5C` 다이아몬드 표식을 공통 적용했다.
- 설정 카드의 필드 제목과 입력 컨트롤은 위아래 배치로 정리했다.
- 카드 제목의 다이아몬드는 `7px × 7px`, 카드 제목은 `13px`, 입력 텍스트는 `12px`로 조정했다.
- 적용용 원형 체크 버튼은 입력과 동일한 `20px` 높이의 `확인` 텍스트 버튼으로 변경하고, 입력과 함께 카드 본문 너비를 채우도록 배치했다.
- `sn` 레이어 관리 행의 마우스 호버 배경은 `#E8F0FD`로 적용했다.
- `확인` 버튼은 배경 `#EDF4FC`, 글자 `#2463A6`, `1px` `#A9C5E5` 테두리로 통일했다.
- 설정 카드 제목은 `12px`, 본문은 `8px` 패딩과 `6px` 간격으로 조정했다.
- 침수심 범례 항목 텍스트는 `11px`로 조정했다.
- 관망도 전용 패널 접기·열기 버튼은 너비를 `20px`로 유지하면서 아이콘을 `32px`로 확대하고 아이콘 색상을 `#666666`으로 적용했다.
- 패널에 맞닿는 버튼의 좌측 위·아래 모서리는 각지게 하고 우측 모서리만 `4px` 둥글게 처리했다.
## 지도 검색바 퍼블리싱
- 실제 관망도에서만 활성화되는 `integrated` 옵션으로 기존 검색 상태와 조회 동작을 유지했다.
- 검색바는 `656px × 44px`, 셀렉트 3개는 각각 `192px`, 조회 버튼은 `80px`로 구성했다.
- 셀렉트 사이 간격과 래퍼 패딩을 제거하고 첫 셀렉트 왼쪽과 조회 버튼 오른쪽에만 `8px` 모서리를 적용했다.
- 첫 번째와 두 번째 셀렉트 오른쪽 내부에 `24px` 높이의 `#D5DCE5` 구분선을 배치했다.
- 셀렉트 글자·화살표·말줄임과 조회 버튼 타이포그래피를 요청 사양에 맞추고 Radix Portal 드롭다운 구조는 유지했다.
- 선택값은 기본 `line-clamp` 표시 영향을 제거하고 `20px` 행간의 한 줄 말줄임으로 고정해 글자 세로 잘림을 방지했다.
- 셀렉트 포커스·열림 라인은 `#2094FA`로 적용하고, 포커스된 영역 옆 구분선은 숨겨 라인이 겹치지 않도록 했다.
- 전체 래퍼의 `8px` 모서리로 셀렉트와 조회 버튼을 함께 클리핑해 우측 라운드를 정확히 맞추고 조회 버튼 배경을 `#2094FA`로 적용했다.
- 셀렉트 포커스·열림 라인은 `2px`로 조정하고, 첫 번째 시도 셀렉트는 왼쪽 위·아래에 검색바와 동일한 `8px` 모서리를 적용했다.
- 세 셀렉트의 아래 방향 화살표는 모두 `32px × 32px`로 확대했다.
- GIS 편집 세션, 검색바, 분석도구의 상단 기준을 `8px`로 통일했다.
- 검색바는 편집 세션 노출 여부와 관계없이 오른쪽 `12px` 위치에 고정해 GIS 편집을 켜고 끌 때 움직이지 않도록 했다.
- 분리형 레이어 패널 토글은 상태별 버튼을 교체하지 않고 하나의 버튼을 유지하며, 패널과 같은 `300ms` 동안 `left` 위치를 이동해 패널 가장자리에 붙은 채 함께 열리고 닫히도록 했다.
- 관망도 섹션형 패널은 열고 닫을 때 투명도 변화를 제거하고 불투명 상태에서 좌우 이동만 적용했다.
- 패널과 토글 버튼 모두 동일한 `transform`, `300ms`, `ease-in-out` 방식으로 정확히 `250px` 이동시켜 애니메이션 중 버튼이 패널에서 떨어지지 않도록 맞췄다.
- 관망도 베이스맵 선택은 숨겨진 `sprite_gis viewer` 메뉴 대신 `일반지도`와 `위성지도`를 처음부터 보여주는 탭으로 변경하고 기존 레이어 전환 함수는 유지했다.
- 분석도구는 기존 지도 버튼과 같은 외형의 아이콘 버튼으로 축소하고, 클릭 시 버튼 아래 `8px` 간격으로 우측 정렬된 기존 메뉴가 열리도록 변경했다.
- 지도 위젯은 지도 가장자리에서 `16px` 떨어뜨리고, 우측 상단에 도구 모음 → 분석도구 → 지도 선택 탭 순서로 `12px` 간격을 두어 상단 정렬했다.
- 우측 상단 지도 선택·분석도구·도구 모음 버튼의 높이는 모두 `40px`로 통일했다.
- 조회 버튼과 지도 유형 활성 탭의 배경은 `#0279FA`로 통일하고, 지도 유형 탭 버튼은 외곽 안쪽에서 `4px` 떨어지도록 배치했다.
- 지도 위 검색바, 지도 선택, 분석도구, 도구 모음, 줌·축척 및 편집 위젯의 외곽 보더를 제거하고 Google Maps 계열의 2중 그림자로 통일했다.
- 분석도구 실행 버튼과 우측 확대·축소·거리측정·면적측정·리셋 아이콘은 버튼 크기를 유지하면서 `20px × 20px` 벡터 아이콘으로 통일했다.
- 분석도구 메뉴가 열린 동안 아이콘 버튼은 `#0279FA` 배경과 흰색 아이콘으로 표시한다.
- 분석도구 메뉴는 화면 진입 시 기본으로 펼치고, 상위 배치 영역은 클릭을 통과시켜 메뉴가 열린 상태에서도 일반지도·위성지도 탭을 조작할 수 있게 했다.
- 선택 레이어 설정의 드롭다운·입력·확인 버튼 높이는 `26px`로 통일하고 수직 패딩을 제거해 가운데 정렬했다.
- 지반침하정보·노후관로 등 공통 설정 카드 제목의 왼쪽 패딩과 본문 전체 패딩은 `0.8rem`으로 조정했다.
- 분석도구 하위 메뉴는 첨부 기준처럼 `15px` 아이콘과 `12px` 텍스트가 한 줄에 놓이는 세로 목록으로 복원하고, 긴 항목에 맞춰 너비가 자동 확장되도록 했다.
- 분석도구 제목은 흰색 콘텐츠 영역 안에 배치하고 하단 구분선과 접기 버튼을 유지했다.
- 분석도구 실행 버튼과 하위 메뉴에는 키보드 탐색 시에만 나타나는 `2px` `#0279FA` `focus-visible` 링을 적용했다.
- 검색바는 복잡한 지도 배경에서도 경계가 분명하도록 `0 2px 4px rgba(60, 64, 67, 0.3), 0 4px 10px rgba(60, 64, 67, 0.18)` 그림자로 강화했다.
- 줌 게이지와 줌레벨·축척 요약 박스는 지도 하단 `16px` 기준으로 아래맞춤 정렬했다.
- 기존 트리 행과 선택 레이어 설정 카드 본문은 유지했다.
## 검증 기준
- 두 카드의 외곽 여백이 상하좌우 `4px`인지 확인한다.
- 두 카드 사이 간격이 `6px`인지 확인한다.
- 패널 바깥 배경이 `#E5E5E5`인지 확인한다.
- 레이어 체크, 펼침/접힘, 설정 카드 입력과 스크롤이 기존과 동일하게 동작하는지 확인한다.
@@ -57,6 +57,10 @@ function Gis({
// 외부 업무 흐름에서 특정 시설물 레이어를 기존 체크 상태에 추가하도록 요청한다.
checkedLayerRequest = null,
searchBar: SearchBar,
// 화면별 상단 정렬과 인접 패널 회피를 위한 검색바 위치 전용 클래스다.
searchBarContainerClassName = "",
// sn 관망도처럼 베이스맵 선택지를 항상 노출하는 화면에서만 탭 형태를 사용한다.
alwaysVisibleMapTabs = false,
waterDepth: WaterDepth = null,
layerSettingView: LayerSettingView = null,
inlineLayerSettingView,
@@ -65,6 +69,8 @@ function Gis({
layerLegendIconVisible,
// 목록·액션 아이콘이 지도 레이어 체크를 선행 조건으로 요구하는지 여부다.
layerIconRequiresChecked = true,
// 목록 액션의 기능은 유지하면서 텍스트를 목록 아이콘으로 표시할지 여부다.
layerActionIconOnly = false,
isPlaying = false,
setIsPlaying = () => { },
targetLayerTypes = [],
@@ -79,11 +85,14 @@ function Gis({
viewExtent,
viewMinZoom,
showZoomLevel = false,
toolBarIconSize,
zoomGuide,
showFloatingSearchButton = true,
treeTogglePosition = "center",
detachedTreeToggleButton = false,
showLayerPanelTitle = true,
// sn 관망도처럼 트리와 선택 설정을 카드 두 개로 구분해야 하는 화면에서만 활성화한다.
sectionedTreePanel = false,
showZoomScaleInfo = false,
}) {
const mapRef = useRef();
@@ -322,6 +331,7 @@ function Gis({
layerIconVisible={layerIconVisible}
layerLegendIconVisible={layerLegendIconVisible}
layerIconRequiresChecked={layerIconRequiresChecked}
layerActionIconOnly={layerActionIconOnly}
sinkholeLegendIconVisible={sinkholeLegendIconVisible}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
@@ -334,6 +344,7 @@ function Gis({
toggleButtonPosition={treeTogglePosition}
detachedToggleButton={detachedTreeToggleButton}
showLayerPanelTitle={showLayerPanelTitle}
sectionedPanel={sectionedTreePanel}
/>
{typeof WaterDepth === "function" && (
@@ -363,15 +374,15 @@ function Gis({
)}
{/* 시도/시군구 조회 영역 */}
{SearchBar && (
<div className="absolute left-1/2 top-4 z-30 w-max -translate-x-1/2 rounded">
<div className={`absolute left-1/2 top-4 z-30 w-max -translate-x-1/2 rounded ${searchBarContainerClassName}`}>
{SearchBar}
</div>
)}
<GisToolBar />
<GisToolBar iconSize={toolBarIconSize} />
{showZoomLevel && (
<GisZoomLevel zoomGuide={zoomGuide} showZoomScaleInfo={showZoomScaleInfo} />
)}
<GisMapToggle />
<GisMapToggle alwaysVisibleTabs={alwaysVisibleMapTabs} />
<GisSelect
onFeatureSelect={handleFeatureSelect}
onMapBlankClick={onMapBlankClick}
@@ -1,30 +1,67 @@
import React, { useContext, useState } from 'react';
import { GisContext } from './Gis';
import React, { useContext, useState } from "react";
import { GisContext } from "./Gis";
export default React.memo(function MapToggleBtn() {
const { gisCore } = useContext(GisContext);
const [activeMap, setActiveMap] = useState({ label: '일반', type: 'base' }); // 기본 선택값
export default React.memo(function MapToggleBtn({ alwaysVisibleTabs = false }) {
const { gisCore } = useContext(GisContext);
const [activeMap, setActiveMap] = useState({ label: "일반", type: "base" }); // 기본 선택값
const handleSelect = mapObj => {
setActiveMap(mapObj);
gisCore.changeBaseLayer(mapObj.type);
};
const handleSelect = (mapObj) => {
setActiveMap(mapObj);
gisCore.changeBaseLayer(mapObj.type);
};
return (
<div className="mapbtn">
<button className="sprite_gis viewer" title="지도선택">지도 선택</button>
<ul className="choice_map">
<li className={activeMap.type === 'base' ? 'on' : ''}>
<button className="map1" id="base" title="일반지도" onClick={() => handleSelect({ label: '일반', type: 'base' })}>
<div>일반지도</div>
</button>
</li>
<li className={activeMap.type === 'satellite' ? 'on' : ''}>
<button className="map2" id="satelite" title="위성지도" onClick={() => handleSelect({ label: '위성', type: 'satellite' })}>
<div>위성지도</div>
</button>
</li>
</ul>
</div>
);
if (alwaysVisibleTabs) {
return (
<div className="mapbtn mapbtn-tabs">
<div className="map-type-tabs" role="group" aria-label="지도 유형 선택">
<button
type="button"
aria-pressed={activeMap.type === "base"}
className={activeMap.type === "base" ? "on" : ""}
onClick={() => handleSelect({ label: "일반", type: "base" })}
>
일반지도
</button>
<button
type="button"
aria-pressed={activeMap.type === "satellite"}
className={activeMap.type === "satellite" ? "on" : ""}
onClick={() => handleSelect({ label: "위성", type: "satellite" })}
>
위성지도
</button>
</div>
</div>
);
}
return (
<div className="mapbtn">
<button className="sprite_gis viewer" title="지도선택">
지도 선택
</button>
<ul className="choice_map">
<li className={activeMap.type === "base" ? "on" : ""}>
<button
className="map1"
id="base"
title="일반지도"
onClick={() => handleSelect({ label: "일반", type: "base" })}
>
<div>일반지도</div>
</button>
</li>
<li className={activeMap.type === "satellite" ? "on" : ""}>
<button
className="map2"
id="satelite"
title="위성지도"
onClick={() => handleSelect({ label: "위성", type: "satellite" })}
>
<div>위성지도</div>
</button>
</li>
</ul>
</div>
);
});
@@ -1,71 +1,102 @@
import React from 'react';
import GisApp from './lib/gis.core';
import GisOlMeasure from './lib/gis.measure';
import { useContext } from 'react';
import { useGisContext } from './Gis';
export default React.memo(function GisToolBar() {
const { gisMap } = useGisContext();
if (!gisMap) return <></>;
const gisMeasuer = new GisOlMeasure(gisMap);
GisApp.Module.measure = gisMeasuer;
const zoomIn = () => {
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
gisMap.getView().animate({
zoom: gisMap.getView().getZoom() + 1,
duration: 250,
});
}
};
const zoomOut = () => {
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
gisMap.getView().animate({
zoom: gisMap.getView().getZoom() - 1,
duration: 250,
});
}
};
import React from "react";
import { RotateCcw, Ruler, Scan, ZoomIn, ZoomOut } from "lucide-react";
import GisApp from "./lib/gis.core";
import GisOlMeasure from "./lib/gis.measure";
import { useContext } from "react";
import { useGisContext } from "./Gis";
export default React.memo(function GisToolBar({ iconSize }) {
const { gisMap } = useGisContext();
const useVectorIcons = Number.isFinite(iconSize) && iconSize > 0;
if (!gisMap) return <></>;
const gisMeasuer = new GisOlMeasure(gisMap);
GisApp.Module.measure = gisMeasuer;
const zoomIn = () => {
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
gisMap.getView().animate({
zoom: gisMap.getView().getZoom() + 1,
duration: 250,
});
}
};
const zoomOut = () => {
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
gisMap.getView().animate({
zoom: gisMap.getView().getZoom() - 1,
duration: 250,
});
}
};
const measure = type => {
gisMeasuer.addInteraction(type);
};
const measure = (type) => {
gisMeasuer.addInteraction(type);
};
const reset = () => {
gisMeasuer.removeFeature();
};
return (
<>
<div className="custom-toolbar">
<div className="tool-bar">
<h3>toolbar 영역</h3>
<ul>
<li>
<div className="btn_control_zoomin" title="확대" onClick={zoomIn}>
<span>확대</span>
</div>
</li>
<li>
<div className="btn_control_zoomout" title="축소" onClick={zoomOut}>
<span>축소</span>
</div>
</li>
<li>
<div className="btn_control_ruler btn_control_measure" title="거리 측정" onClick={() => measure(GisApp.measure.LineString)}>
<span>거리측정</span>
</div>
</li>
<li>
<div className="btn_control_area btn_control_measure" title="면적 측정" onClick={() => measure(GisApp.measure.Polygon)}>
<span>면적측정</span>
</div>
</li>
<li>
<div className="sprite_gis icon4 btn_select_cancel" title="리셋" onClick={() => reset()}>
<span>리셋</span>
</div>
</li>
</ul>
</div>
</div>
</>
);
const reset = () => {
gisMeasuer.removeFeature();
};
return (
<>
<div className="custom-toolbar">
<div
className={`tool-bar ${useVectorIcons ? "tool-bar-vector-icons" : ""}`}
>
<h3>toolbar 영역</h3>
<ul>
<li>
<div className="btn_control_zoomin" title="확대" onClick={zoomIn}>
{useVectorIcons && (
<ZoomIn size={iconSize} aria-hidden="true" />
)}
<span>확대</span>
</div>
</li>
<li>
<div
className="btn_control_zoomout"
title="축소"
onClick={zoomOut}
>
{useVectorIcons && (
<ZoomOut size={iconSize} aria-hidden="true" />
)}
<span>축소</span>
</div>
</li>
<li>
<div
className="btn_control_ruler btn_control_measure"
title="거리 측정"
onClick={() => measure(GisApp.measure.LineString)}
>
{useVectorIcons && <Ruler size={iconSize} aria-hidden="true" />}
<span>거리측정</span>
</div>
</li>
<li>
<div
className="btn_control_area btn_control_measure"
title="면적 측정"
onClick={() => measure(GisApp.measure.Polygon)}
>
{useVectorIcons && <Scan size={iconSize} aria-hidden="true" />}
<span>면적측정</span>
</div>
</li>
<li>
<div
className="sprite_gis icon4 btn_select_cancel"
title="리셋"
onClick={() => reset()}
>
{useVectorIcons && (
<RotateCcw size={iconSize} aria-hidden="true" />
)}
<span>리셋</span>
</div>
</li>
</ul>
</div>
</div>
</>
);
});
@@ -4,7 +4,7 @@ import {
getBackendOptions,
DndProvider,
} from "@minoru/react-dnd-treeview";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { ChevronLeft, ChevronRight, Layers } from "lucide-react";
import React, { useEffect, useLayoutEffect, useState, useRef, useCallback } from "react";
import { showBusinessUserMessage } from "../../lib/user-message";
@@ -33,7 +33,9 @@ import GisApp from "./lib/gis.core";
* - layerIconVisible: (선택) 레이어 행 우측 액션 아이콘 노출 여부를 결정하는 함수
* - layerLegendIconVisible: (선택) 레이어 행 범례 아이콘 노출 여부를 결정하는 함수
* - layerIconRequiresChecked: (선택) 액션 실행 전에 레이어 체크를 요구할지 여부
* - layerActionIconOnly: (선택) 목록 액션을 텍스트 대신 목록 아이콘으로 표시할지 여부
* - detachedToggleButton: (선택) 접기/펼치기 버튼을 패널 밖 고정 핸들로 노출할지 여부
* - sectionedPanel: (선택) 트리와 선택 레이어 설정을 회색 배경 위의 독립 카드로 표시할지 여부
*/
export default React.memo(function GisTree({
checkedNodes = [],
@@ -53,12 +55,14 @@ export default React.memo(function GisTree({
layerIconVisible,
layerLegendIconVisible = () => true,
layerIconRequiresChecked = true,
layerActionIconOnly = false,
sinkholeLegendIconVisible = false,
inlineLayerSettingView,
treeSectionClassName = "",
toggleButtonPosition = "center",
showLayerPanelTitle = true,
detachedToggleButton = false,
sectionedPanel = false,
}) {
const { gisCore, treeData } = useGisContext();
@@ -584,38 +588,57 @@ export default React.memo(function GisTree({
// 렌더링
// =====================
if (!treeData || treeData.length === 0) return null;
const toggleButtonPositionClass =
toggleButtonPosition === "top" ? "top-4" : "top-1/2 -translate-y-1/2";
const thinScrollbarClass =
"[scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]";
const treeSectionClassNames = [treeSectionClassName, thinScrollbarClass].filter(Boolean).join(" ");
"[scrollbar-gutter:stable] [scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]";
const treeSectionClassNames = [treeSectionClassName].filter(Boolean).join(" ");
// 섹션형 패널은 지도 트리와 선택 레이어 설정을 각각 독립된 카드로 구분한다.
const panelShellClassName = sectionedPanel
? "gap-[6px] border-r-0 bg-[#E5E5E5] p-1"
: "border-r border-border bg-background";
const treePanelClassName = sectionedPanel
? "min-h-0 grow flex-col overflow-hidden rounded-[6px] border border-border bg-background shadow-none"
: "min-h-0 grow overflow-hidden border-none bg-background shadow-none";
const treeContentScrollClassName = sectionedPanel
? `min-h-0 flex-1 overflow-y-scroll ${thinScrollbarClass}`
: `h-full overflow-y-auto p-2 ${showLayerPanelTitle ? "pt-0" : "pt-2"} ${thinScrollbarClass}`;
return (
<>
{/* 사이드 패널 */}
<div
className={`gis-layer-panel absolute left-0 top-0 z-40 flex h-full w-[250px] flex-col overflow-hidden border-r border-border bg-background text-foreground transition-all duration-300 ${visible
? "translate-x-0 opacity-100"
: "pointer-events-none -translate-x-full opacity-0"
className={`gis-layer-panel absolute left-0 top-0 z-40 flex h-full w-[250px] flex-col overflow-hidden text-foreground duration-300 ${sectionedPanel ? "transition-transform ease-in-out" : "transition-all"} ${panelShellClassName} ${visible
? sectionedPanel ? "translate-x-0" : "translate-x-0 opacity-100"
: sectionedPanel ? "pointer-events-none -translate-x-full" : "pointer-events-none -translate-x-full opacity-0"
}`}
>
<div
ref={layerPanelScrollRef}
onScroll={handleLayerPanelScroll}
className={`grow overflow-y-auto border-none p-2 ${showLayerPanelTitle ? "pt-0" : "pt-2"} shadow-none ${activeTab === "layers" ? "block" : "hidden"
className={`${treePanelClassName} ${activeTab === "layers" ? (sectionedPanel ? "flex" : "block") : "hidden"
}`}
>
{showLayerPanelTitle && (
<>
<h3 className="gis-layer-panel-title mx-2 my-4 text-center text-3xl font-bold text-label">시설물</h3>
<hr className="mb-4 border-border" />
</>
{sectionedPanel && (
<div className="gis-layer-section-header flex h-[38px] shrink-0 items-center gap-2 border-b border-border bg-background px-3">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm bg-[#EAF3FF] text-[#168CFF]">
<Layers size={14} strokeWidth={2} />
</span>
<h3 className="text-[1.4rem] font-bold text-foreground">레이어 관리</h3>
</div>
)}
{/* DnD Tree */}
<div className={treeSectionClassNames || undefined}>
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<div
ref={layerPanelScrollRef}
onScroll={handleLayerPanelScroll}
className={treeContentScrollClassName}
>
{showLayerPanelTitle && (
<>
<h3 className="gis-layer-panel-title mx-2 my-4 text-center text-3xl font-bold text-label">시설물</h3>
<hr className="mb-4 border-border" />
</>
)}
{/* DnD Tree */}
<div className={treeSectionClassNames || undefined}>
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree
tree={nodes}
rootId={null}
@@ -642,7 +665,7 @@ export default React.memo(function GisTree({
return (
<div style={{ display: isVisible ? "block" : "none" }}>
<div
className="gis-layer-row flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-accent"
className={`gis-layer-row flex w-full items-center gap-2 rounded px-2 py-1 ${sectionedPanel ? "hover:bg-[#f5f5f5]" : "hover:bg-accent"}`}
style={{
marginInlineStart: depth * 16,
width: `calc(100% - ${depth * 16}px)`,
@@ -671,21 +694,39 @@ export default React.memo(function GisTree({
>
{node.text}
</span>
{showLayerActionButton && (
<button
type="button"
className="gis-layer-action-button ml-auto shrink-0 rounded-sm border border-primary bg-background px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-primary transition-colors hover:bg-accent"
onClick={() =>
handleLayerImgClick(
{showLayerActionButton && (
<button
type="button"
className={layerActionIconOnly
? "gis-layer-action-button ml-auto grid h-[24px] w-[24px] shrink-0 place-items-center rounded-sm bg-transparent p-0 text-[#168CFF] transition-colors hover:bg-accent"
: "gis-layer-action-button ml-auto shrink-0 rounded-sm border border-primary bg-background px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-primary transition-colors hover:bg-accent"}
onClick={() =>
handleLayerImgClick(
node.data.lyrTyNm,
node.id,
node
)
}
title={`${node.text} 목록보기`}
>
목록보기
</button>
)
}
title={`${node.text} 목록보기`}
aria-label={`${node.text} 목록보기`}
>
{layerActionIconOnly ? (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-[18px] w-[18px]"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
>
<path d="M9 6h11M9 12h11M9 18h11" />
<path d="M4 6h.01M4 12h.01M4 18h.01" strokeWidth="3" />
</svg>
) : (
"목록보기"
)}
</button>
)}
{!showLayerActionButton && showLayerActionIcon && (
<span className="shrink-0">
@@ -720,11 +761,16 @@ export default React.memo(function GisTree({
return aOrder - bOrder;
}}
/>
</DndProvider>
</DndProvider>
</div>
</div>
</div>
{LayerSettingView && (
<div className="gis-layer-setting shrink-0 bg-background px-2 pb-3">
<div
className={sectionedPanel
? "gis-layer-setting shrink-0 bg-transparent p-0"
: "gis-layer-setting shrink-0 bg-background px-2 pb-3"}
>
<LayerSettingView />
</div>
)}
@@ -737,24 +783,31 @@ export default React.memo(function GisTree({
</button>
)}
</div>
{detachedToggleButton && visible && (
{detachedToggleButton && (
<button
onClick={() => setVisible(false)}
className={`gis-layer-toggle absolute left-[250px] z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-border bg-background text-primary shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-accent ${toggleButtonPositionClass}`}
title="접기"
onClick={() => setVisible(!visible)}
className={`gis-layer-toggle absolute left-[250px] z-50 flex h-[36px] transform items-center justify-center border border-border bg-background shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition-transform duration-300 ease-in-out hover:bg-accent ${visible ? "translate-x-0" : "-translate-x-[250px]"} ${sectionedPanel
? "w-[20px] rounded-l-none rounded-r-[4px] text-[#666666]"
: "w-[20px] rounded-[4px] text-primary"
} ${toggleButtonPositionClass}`}
title={visible ? "접기" : "열기"}
aria-label={visible ? "레이어 패널 접기" : "레이어 패널 열기"}
aria-expanded={visible}
>
<ChevronLeft className="h-4 w-4" />
{visible ? (
<ChevronLeft className={sectionedPanel ? "h-8 w-8" : "h-4 w-4"} />
) : (
<ChevronRight className={sectionedPanel ? "h-8 w-8" : "h-4 w-4"} />
)}
</button>
)}
{!visible && (
{!visible && !detachedToggleButton && (
<button
onClick={() => setVisible(true)}
className={detachedToggleButton
? `gis-layer-toggle absolute left-0 z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-border bg-background text-primary shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-accent ${toggleButtonPositionClass}`
: `gis-layer-toggle absolute left-[-3px] z-50 transform rounded-r border border-r border-border bg-background px-1 py-2 text-foreground shadow-sm ${toggleButtonPositionClass}`}
className={`gis-layer-toggle absolute left-[-3px] z-50 transform rounded-r border border-r border-border bg-background px-1 py-2 text-foreground shadow-sm ${toggleButtonPositionClass}`}
title="열기"
>
{detachedToggleButton ? <ChevronRight className="h-4 w-4" /> : <ChevronRight size={16} />}
<ChevronRight size={16} />
</button>
)}
</>
@@ -101,6 +101,56 @@
text-align: center;
}
.mapbtn.mapbtn-tabs {
width: auto;
}
.mapbtn .map-type-tabs {
display: flex;
width: 144px;
height: 40px;
padding: 2px;
overflow: hidden;
box-sizing: border-box;
background: #fff;
border: 1px solid #9aa7b8;
border-radius: 6px;
box-shadow: 0 0 5px #999;
}
.mapbtn .map-type-tabs button {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-width: 0;
height: 100%;
padding: 0 8px;
box-sizing: border-box;
background: #fff;
border-radius: 6px;
border: 0;
color: #666;
font-family: inherit;
font-size: 12px;
font-weight: 600;
line-height: 18px;
white-space: nowrap;
}
.mapbtn .map-type-tabs button.on {
background: #0279fa;
color: #fff;
}
.mapbtn .map-type-tabs button:focus-visible {
position: relative;
z-index: 1;
outline: 2px solid #2094fa;
outline-offset: -2px;
}
.tool-bar {
position: absolute;
top: 70px;
@@ -186,9 +236,9 @@
bottom: 6px;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 126px;
padding: 7px 10px;
gap: 8px;
min-width: 130px;
padding: 8px 10px;
border: 1px solid #9aa7b8;
border-radius: 6px;
background: rgba(255, 255, 255, 0.96);
@@ -32,6 +32,8 @@ interface SearchBarProps {
facilityTypes: { text: string; val: string }[];
sidoSearchZoom?: number;
facilityLayerFilter?: FacilityLayerFilter;
/** 관망도 전용 3개 셀렉트와 조회 버튼을 하나로 연결한 검색바 레이아웃이다. */
integrated?: boolean;
}
interface CenterSelectDataType extends SelectDataType {
@@ -43,6 +45,7 @@ export const ProcSearchBar = ({
facilityTypes,
sidoSearchZoom = 11,
facilityLayerFilter,
integrated = false,
}: SearchBarProps) => {
const [sidoValue, setSidoValue] = useState("0");
const [sigunValue, setSigunValue] = useState("0");
@@ -473,6 +476,75 @@ export const ProcSearchBar = ({
await searchLayer.showSearchResultsFromGeoJSON(layerNm, featureId);
};
if (integrated) {
const selectRootClass = "box-border !h-[44px] !w-full !p-0";
const selectTriggerClass =
"relative box-border !h-[44px] !w-full !border-0 !bg-white !px-4 !py-0 !text-[14px] !font-normal !leading-[20px] !text-[#374151] focus:!z-[2] focus:!ring-2 focus:!ring-inset focus:!ring-[#2094FA] data-[state=open]:!z-[2] data-[state=open]:!ring-2 data-[state=open]:!ring-inset data-[state=open]:!ring-[#2094FA] [&>span]:!block [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:text-ellipsis [&>span]:whitespace-nowrap [&>span]:!leading-[20px] [&>span]:pr-2 [&>svg]:!h-8 [&>svg]:!w-8 [&>svg]:shrink-0 [&>svg]:!text-[#6B7280] [&>svg]:!opacity-100";
return (
<div className="flex h-[44px] w-[656px] box-border items-center overflow-hidden rounded-[8px] bg-white p-0 shadow-[0_2px_4px_rgba(60,64,67,0.3),0_4px_10px_rgba(60,64,67,0.18)]">
<div className="group relative h-[44px] w-[192px] shrink-0 box-border">
<SelectBox
labelClass="hidden"
className={selectRootClass}
selectClass={`${selectTriggerClass} !rounded-none focus:!rounded-l-[8px] data-[state=open]:!rounded-l-[8px]`}
label=""
selectArray={sidoData}
onSelectValue={(val: any) =>
onSelectValue(val.toString(), "ctpvCd")
}
value={sidoValue}
selectContentClass="z-[101]"
/>
<span
aria-hidden="true"
className="pointer-events-none absolute right-0 top-1/2 z-[1] h-[24px] w-px -translate-y-1/2 bg-[#D5DCE5] group-focus-within:hidden"
/>
</div>
<div className="group relative h-[44px] w-[192px] shrink-0 box-border">
<SelectBox
labelClass="hidden"
className={selectRootClass}
selectClass={`${selectTriggerClass} !rounded-none`}
label=""
selectArray={filteredSigunData}
onSelectValue={(val: any) =>
onSelectValue(val.toString(), "sggCd")
}
value={sigunValue}
selectContentClass="z-[101]"
/>
<span
aria-hidden="true"
className="pointer-events-none absolute right-0 top-1/2 z-[1] h-[24px] w-px -translate-y-1/2 bg-[#D5DCE5] group-focus-within:hidden"
/>
</div>
<div className="h-[44px] w-[192px] shrink-0 box-border">
<SelectBox
labelClass="hidden"
className={selectRootClass}
selectClass={`${selectTriggerClass} !rounded-none`}
label=""
selectArray={facilityData}
onSelectValue={(val: any) =>
onSelectValue(val.toString(), "fcltCd")
}
value={facilityValue}
selectContentClass="z-[101]"
/>
</div>
<Button
color="primary"
className="box-border !h-[44px] !w-[80px] shrink-0 !rounded-none !bg-[#0279FA] !p-0 text-[14px] font-semibold leading-5 text-white"
size="default"
onClick={handleSearch}
>
조회
</Button>
</div>
);
}
return (
<div className="grid grid-cols-4 gap-4">
<div className="col-span-4">