Merge remote-tracking branch 'origin/iipbs/dm/dev'

This commit is contained in:
yschoi
2025-07-26 15:59:51 +09:00
156 changed files with 1951 additions and 11771 deletions
+11 -11
View File
@@ -1,19 +1,19 @@
NEXT_PUBLIC_SITE_ID = DM
NEXT_PUBLIC_PORT = 3000
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
NEXT_PUBLIC_PORT = 11035
#NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
NEXT_PUBLIC_PROXY_HOST = http://localhost:3000/
NEXT_PUBLIC_SERVER_API_URL = http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11082/api/v1
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
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
#NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
+2 -2
View File
@@ -1,6 +1,6 @@
NEXT_PUBLIC_SITE_ID = DM
NEXT_PUBLIC_PORT = 3000
NEXT_PUBLIC_PROXY_HOST = http://wr.hasuon.or.kr
NEXT_PUBLIC_PORT = 11035
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/api/v1
NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/operation-service/api/v1
NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000
+16
View File
@@ -0,0 +1,16 @@
"use client";
import Base from "../base";
import "../globals.css";
import { StoreProvider } from "../store";
export default function BaseLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<StoreProvider>
<Base>{children}</Base>
</StoreProvider>
);
}
@@ -9,6 +9,7 @@ import { Toaster } from 'react-hot-toast';
import SdmLogin from '../sdmLogin/page';
import type { DemandCompType } from '@/app/store/sdmMng';
import TransValueFormat from '@/app/common/transValueFormat';
import { useRouter } from 'next/navigation';
const TABLE_STYLES = {
header: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line",
@@ -22,6 +23,9 @@ type SdmApplyProps = {
}
export default function SdmApplyList({}: SdmApplyProps) {
const router = useRouter();
const {getSdmInfoStatusList, Sdm, Common} = useDmStore((state) => state);
const {successLogin, sdmInfoStatusList, upHeadSdmInfoStatus} = Sdm;
const {selectSidoData, selectSigunData} = Common;
@@ -185,7 +189,12 @@ export default function SdmApplyList({}: SdmApplyProps) {
body={<SdmLogin/>}
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
open={!isLogin}
onOpenChange={(open) => !open && setIsLogin(false)}
onOpenChange={(open) => {
if(!open){
setIsLogin(false)
router.push('/');
}
}}
noHeaderYn="Y"
noFooterYn="Y"
/>
@@ -6,14 +6,14 @@ import { SdmInfoRegForm } from "@/components/ui/sdmInfoInsertForm/component";
import { UiModal } from "@common/business_components/ui/dialog_ui";
import { Button } from "@common/components/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSearchParams } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useState } from "react";
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
import { Toaster } from "react-hot-toast";
import * as z from "zod";
import SdmLogin from "../sdmLogin/page";
const createFormSchema = () => z.object({
const sdmInfoFormSchema = z.object({
sdmNm: z.string()
.min(1, "수요처를 선택해주세요"),
demandYmd: z.date().optional(),
@@ -24,61 +24,64 @@ const createFormSchema = () => z.object({
money: z.string()
.regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"),
/**요구수질 */
reqWaterQlty : z.string().optional(),
reqWaterQlty: z.string().optional(),
grssClfg: z.string()
.min(1, "총대장균군수를 입력해주세요")
.regex(/^[0-9]+$/, "총대장균군수는 숫자만 입력 가능합니다"),
tn : z.string()
tn: z.string()
.min(1, "총질소를 입력해주세요")
.regex(/^[0-9]+$/, "총질소는 숫자만 입력 가능합니다"),
combResdch : z.string()
combResdch: z.string()
.min(1, "결합잔류염소를 입력해주세요")
.regex(/^[0-9]+$/, "결합잔류염소는 숫자만 입력 가능합니다"),
tph : z.string()
tph: z.string()
.min(1, "총인산물을 입력해주세요")
.regex(/^[0-9]+$/, "총인산물은 숫자만 입력 가능합니다"),
trbt : z.string()
trbt: z.string()
.min(1, "탁도를 입력해주세요")
.regex(/^[0-9]+$/, "탁도는 숫자만 입력 가능합니다"),
ph : z.string()
ph: z.string()
.min(1, "수소이온농도를 입력해주세요")
.regex(/^[0-9]+$/, "수소이온농도는 숫자만 입력 가능합니다"),
bod : z.string()
bod: z.string()
.min(1, "생물학적 산소요구량을 입력해주세요")
.regex(/^[0-9]+$/, "생물학적 산소요구량은 숫자만 입력 가능합니다"),
/**컬럼확인필요 */
clIonQnt : z.string()
clIonQnt: z.string()
.min(1, "염화물을 입력해주세요")
.regex(/^[0-9]+$/, "염화물은 숫자만 입력 가능합니다"),
smll : z.string()
smll: z.string()
.min(1, "냄새를 입력해주세요")
.regex(/^[0-9]+$/, "냄새는 숫자만 입력 가능합니다"),
cdtnv : z.string()
cdtnv: z.string()
.min(1, "전기전도도를 입력해주세요")
.regex(/^[0-9]+$/, "전기전도도는 숫자만 입력 가능합니다"),
crmty : z.string()
crmty: z.string()
.min(1, "색도를 입력해주세요")
.regex(/^[0-9]+$/, "색도는 숫자만 입력 가능합니다"),
rmrk : z.string().optional(),
rmrk: z.string().optional(),
/* telno: z.string()
.min(8, "전화번호는 8자 이상 입력해주세요")
.max(12, "전화번호는 12자 이내로 입력해주세요")
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */
candidateTitle: z.string().optional(),
candidateTitle: z.array(z.object({
ruseUtztnFcltNm: z.string(),
ruseUtztnFcltCd: z.string(),
})).optional(),
});
export type FormData = z.infer<ReturnType<typeof createFormSchema>>;
export type FormData = z.infer<typeof sdmInfoFormSchema>;
function SdmInfoInsertContent() {
const searchParams = useSearchParams();
const {insertSdmInfoData, Sdm} = useDmStore((state) => state);
const {successLogin} = Sdm;
const {map, insertSdmInfoData, Sdm } = useDmStore((state) => state);
const { successLogin } = Sdm;
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
const formSchema = createFormSchema();
const formSchema = sdmInfoFormSchema;
const [isLogin, setIsLogin] = useState(false);
const [facilityName, setFacilityName] = useState<string>('');
const [defaultValues, setDefaultValues] = useState<FormData>({
const [defaultValues, setDefaultValues] = useState<FormData>({
sdmNm: '',
demandYmd: new Date(),
demandQnty: '',
@@ -100,9 +103,15 @@ function SdmInfoInsertContent() {
crmty: '',
rmrk: '',
/** 후보지 제목 */
candidateTitle: '',
candidateTitle: [
{ ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' },
{ ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' },
{ ruseUtztnFcltNm: '', ruseUtztnFcltCd: '' },
],
});
useEffect(() => {
const user = sessionStorage.getItem('user');
setDefaultValues(prev => ({
@@ -115,7 +124,7 @@ function SdmInfoInsertContent() {
useEffect(() => {
const local = localStorage.getItem('user');
if(local) {
if (local) {
setIsLogin(true);
}
}, []);
@@ -126,6 +135,13 @@ function SdmInfoInsertContent() {
mode: "onChange",
});
useEffect(()=>{
if(map.subsInfo.length>0){
form.setValue('candidateTitle', map.subsInfo)
}
},[map.subsInfo, form])
// 로그인 성공 시 모달 닫기 및 폼 업데이트
useEffect(() => {
if (successLogin) {
@@ -152,11 +168,11 @@ function SdmInfoInsertContent() {
form.setValue('money', '');
}
}, [ismoneyDisabled, form]);
const router = useRouter();
const onSubmit: SubmitHandler<FormData> = async (data) => {
try {
// Transform form data to match API schema
const apiData : DemandCompType = {
const apiData: DemandCompType = {
dmdInfoId: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
dmdInfoFk: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdInfoId?.toString() || searchParams.get('dmdInfoId') + (searchParams.get('bznmTotNo') || ''),
dmdCoNm: JSON.parse(sessionStorage.getItem('user') || '{}')?.dmdCoNm?.toString() || data.sdmNm,
@@ -186,17 +202,22 @@ function SdmInfoInsertContent() {
index: 0,
reqDt: "",
dmdInfoSn: 0,
status : '',
status: '',
splyCndtList: data.candidateTitle || [],
};
await insertSdmInfoData(apiData);
} catch (error) {
console.error('Error submitting form:', error);
}
};
return (
<div className="min-h-screen bg-gray-50">
<div className="min-h-screen bg-[#F2F2F2]">
<div className="container mx-auto px-4 py-8">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 underline">
@@ -207,14 +228,14 @@ function SdmInfoInsertContent() {
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6">
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<SdmInfoRegForm
<SdmInfoRegForm
form={form}
onCheckboxChange={setIsmoneyDisabled}
facilityName={facilityName}
/>
<div className="flex justify-center pt-4">
<Button
type="submit"
<Button
type="submit"
className="w-40 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
>
수요정보 등록
@@ -225,16 +246,20 @@ function SdmInfoInsertContent() {
</div>
</div>
{!isLogin?
<UiModal
body={<SdmLogin/>}
{!isLogin ?
<UiModal
body={<SdmLogin />}
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
open={!isLogin}
onOpenChange={(open) => !open && setIsLogin(false)}
onOpenChange={(open) => {
if (!open) {
router.push("/");
}
}}
noHeaderYn="Y"
noFooterYn="Y"
/>
:null}
: null}
<Toaster position="top-center" />
</div>
);
@@ -8,7 +8,7 @@ import { useCallback, useEffect, useRef } from "react";
import { Toaster } from "react-hot-toast";
export default function MyForm() {
const {Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm} = useDmStore((state) => state);
const { Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm } = useDmStore((state) => state);
const { selectSidoData, selectSigunData } = Common;
const { demandCompList, successNextPage } = Sdm;
const router = useRouter();
@@ -21,6 +21,7 @@ export default function MyForm() {
useEffect(() => {
if (successNextPage && formDataRef.current) {
const data = formDataRef.current;
console.log(data);
const queryParams = new URLSearchParams({
dmdCoNm: data.dmdCoNm || '',
ctpvCd: data.ctpvCd || '',
@@ -32,10 +33,10 @@ export default function MyForm() {
router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`);
}
}, [successNextPage, router]);
const callback = useCallback((searchText: string, pageCnt: number) => {
searchText = searchText.trim();
if(searchText.length > 0) {
if (searchText.length > 0) {
getDemandCompList(searchText, pageCnt);
}
}, [getDemandCompList]);
@@ -54,15 +55,17 @@ export default function MyForm() {
}, [insertSdmData]);
return (
<div className="min-h-screen bg-gray-50">
<SdmInsertForm
selectSidoData={selectSidoData}
selectSigunData={selectSigunData}
dataList={demandCompList}
rtnValue={callback}
rtnInputData={handleInsertSdmData}
/>
<Toaster position="top-center" />
<div className="min-h-screen bg-[#F2F2F2]">
<div className="pt-12">
<SdmInsertForm
selectSidoData={selectSidoData}
selectSigunData={selectSigunData}
dataList={demandCompList}
rtnValue={callback}
rtnInputData={handleInsertSdmData}
/>
<Toaster position="top-center" />
</div>
</div>
);
}
@@ -0,0 +1,337 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
const NAVS = [
{ id: 'card1', label: '물재이용이란' },
{ id: 'card2', label: '물재이용 필요성' },
{ id: 'card3', label: '빗물 재이용' },
{ id: 'card4', label: '중수 재이용' },
{ id: 'card5', label: '하수 재이용' },
{ id: 'card6', label: '물재이용 목표' },
];
export default function WrAboutPage() {
const [activeIdx, setActiveIdx] = useState(0);
const navRefs = useRef<(HTMLLIElement | null)[]>([]);
const cardRefs = useRef<(HTMLElement | null)[]>([]);
// 클릭 시 이동
const handleNavClick = (idx: number) => {
setActiveIdx(idx);
const targetCard = cardRefs.current[idx];
if (targetCard) {
targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
// 키보드 이벤트 처리
const handleKeyDown = (e: React.KeyboardEvent, idx: number) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleNavClick(idx);
}
};
useEffect(() => {
const observerOptions = {
root: null,
rootMargin: '-200px 0px 0px 0px', // 상단에서 200px 지점을 기준으로
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const idx = cardRefs.current.findIndex(ref => ref === entry.target);
if (idx !== -1) {
setActiveIdx(idx);
}
}
});
}, observerOptions);
// 각 카드 요소를 관찰
cardRefs.current.forEach((card) => {
if (card) {
observer.observe(card);
}
});
return () => {
cardRefs.current.forEach((card) => {
if (card) {
observer.unobserve(card);
}
});
};
}, []);
return (
<div className='bg-[#F2F2F2]'>
<main className="relative w-[950px] mx-auto pt-[100px] font-sans">
{/* 사이드바 */}
<nav className="fixed left-[290px] top-[195px] w-[210px] z-50" aria-label="물재이용 정보 섹션 내비게이션">
<ul role="list" className="relative space-y-6">
{NAVS.map((nav, idx) => (
<li
key={nav.id}
ref={(el) => { navRefs.current[idx] = el; }}
role="listitem"
tabIndex={0}
aria-current={idx === activeIdx ? 'true' : 'false'}
aria-label={`현재 위치: ${nav.label}`}
onClick={() => handleNavClick(idx)}
onKeyDown={(e) => handleKeyDown(e, idx)}
className={`font-NotoSansKR nav-item relative pl-6 text-2xl font-bold text-[#383838] cursor-pointer transition-all duration-300
after:content-[''] after:absolute after:top-6 after:left-[0.5rem] after:w-[2px] after:h-[24px] after:bg-[#C5C5C5] last:after:hidden
${idx === activeIdx ? 'text-4xl font-bold text-[#383838]' : ''}`}
>
<span className={`absolute left-[0.5rem] top-1 w-4 h-4 rounded-full -translate-x-1/2 transition-all duration-300
${idx === activeIdx ? 'bg-blue-500 border-blue-500' : 'bg-white border-gray-400 border'}`}></span>
{nav.label}
</li>
))}
</ul>
</nav>
<article ref={(el) => { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title">
<h2 id="card1-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용이란
</h2>
<section aria-labelledby="card1-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card1-desc" className="font-NotoSansKR text-[#269DDA] text-5xl font-bold mb-8">물 재이용이 무엇인가요?</h1>
<p className="font-NotoSansKR text-[#231815] text-2xl mb-10 leading-relaxed">
물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를<br />
물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,<br />
하천 유지 등의 용도로 이용하는 것을 말합니다.
</p>
<Image
src="/img/wrAbout/img_01_1.png"
alt="이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-8 rounded"
/>
<p className="font-NotoSansKR text-center font-normal text-2xl">
환경부는 2011년에 &apos;물의 재이용 촉진 및 지원에 관한 법률&apos;을 제정하여<br />
한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
</p>
</section>
</article>
{/* 카드2 */}
<article ref={(el) => { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title">
<h2 id="card2-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용 필요성
</h2>
<section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card2-desc" className="font-NotoSansKR text-[#ED5534] text-5xl font-semibold mb-8">물 재이용, 왜 필요한가요?</h1>
<p className="font-NotoSansKR text-2xl font-normal mb-10 leading-relaxed">
빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br />
물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다.
</p>
<Image
src="/img/wrAbout/img_02_1.png"
alt="이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다."
width={800}
height={400}
className="w-[90%] mx-auto mb-8 w-[80%]"
/>
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">
- 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.<br />
- 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다.
</p>
<Image
src="/img/wrAbout/img_02_2.png"
alt="세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-8"
/>
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">
- 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.<br />
- 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다.
</p>
<Image
src="/img/wrAbout/img_02_3.png"
alt="물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다."
width={800}
height={400}
className="w-[93%] mx-auto"
/>
</section>
</article>
{/* 카드3 */}
<article ref={(el) => { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title">
<h2 id="card3-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
빗물 재이용
</h2>
<section aria-labelledby="card3-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card3-desc" className="font-NotoSansKR leading-snug text-[#269DDA] text-5xl font-bold mb-6">친환경 수자원 빗물,<br />편리하게 사용할 수 있어요!</h1>
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">가뭄을 극복하고 홍수를 예방하기 위해<br />비가 내릴 때 빗물을 모아두었다가 사용합니다.</p>
<Image
src="/img/wrAbout/img_03_1.png"
alt="주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-6"
/>
<Image
src="/img/wrAbout/img_03_2.png"
alt="빗물이용"
width={200}
height={100}
className="mb-5 w-[20%]"
/>
<p className="font-NotoSansKR text-2xl leading-relaxed">
- 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등<br />
- 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)<br />
- 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등
</p>
</section>
</article>
{/* 카드4 */}
<article ref={(el) => { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title">
<h2 id="card4-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
중수 재이용
</h2>
<section aria-labelledby="card4-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card4-desc" className="font-NotoSansKR leading-snug text-[#9771B0] text-5xl font-bold mb-6">요모조모 쓸모 많은 중수,<br />알뜰하게 재이용할 수 있어요!</h1>
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">한번 사용한 물을 중수도 시설을 통해 재생하여<br />개별적 또는 지역적으로 재이용할 수 있습니다.</p>
<Image
src="/img/wrAbout/img_04_1.png"
alt="욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-6"
/>
<Image
src="/img/wrAbout/img_04_2.png"
alt="중수재이용"
width={200}
height={100}
className="mb-5 w-[20%]"
/>
<p className="font-NotoSansKR text-2xl leading-relaxed">
- 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등<br />
- 의무 재이용량: 물 사용량의 10% 이상<br />
- 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등
</p>
</section>
</article>
{/* 카드5 */}
<article ref={(el) => { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title">
<h2 id="card5-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
하수 재이용
</h2>
<section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card5-desc" className="font-NotoSansKR leading-snug text-[#0099A4] text-5xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1>
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p>
<Image
src="/img/wrAbout/img_05_1.png"
alt="주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-6"
/>
<Image
src="/img/wrAbout/img_05_2.png"
alt="하수처리 재이용"
width={200}
height={100}
className="mb-5 w-[20%]"
/>
<p className="font-NotoSansKR text-2xl leading-relaxed">
- 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)<br />
- 의무 재이용량: 일 하수처리량의 10% 이상<br />
- 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등
</p>
</section>
</article>
{/* 카드6 */}
<article ref={(el) => { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title">
<h2 id="card6-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용 목표
</h2>
<section aria-labelledby="card6-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card6-desc" className="font-NotoSansKR leading-snug text-[#269DDA] text-5xl font-bold mb-6">2030년 물 재이용 목표를 위해<br />모두의 관심과 노력이 필요합니다.</h1>
<p className="font-NotoSansKR text-2xl mb-6 leading-relaxed">
환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여<br />한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
</p>
<Image
src="/img/wrAbout/img_06_1.png"
alt="지속가능한 물 재이용 정착으로 건전한 물순환 확산"
width={800}
height={400}
className="w-[80%] mx-auto mb-6"
/>
<div className="flex gap-3 flex-wrap justify-between bg-gray-100 p-6 rounded-lg mb-8">
<div className="flex-1 text-center">
<Image
src="/img/wrAbout/icon_01.png"
alt=""
width={70}
height={70}
className="mx-auto mb-4 w-[auto] h-[70px]"
/>
<h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">도시 물 순환이용<br />체계로의 전환</h3>
<ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1">
<li className="text-nowrap">- 지속가능한 물 순환이용체계 구축</li>
<li>- 물 재이용시설 관리체계 강화</li>
</ul>
</div>
<div className="flex-1 text-center">
<Image
src="/img/wrAbout/icon_02.png"
alt=""
width={70}
height={70}
className="mx-auto mb-4 w-[auto] h-[70px]"
/>
<h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">유역기반 하수처리수 재이용<br />수요확대 및 공급체계 구축</h3>
<ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1">
<li>- 유역기반 통합물관리 연계 강화</li>
<li>- 하수처리수 재이용 공업용수 공급 활성화</li>
<li>- 하수처리수 재이용을 활용한 물순환 촉진</li>
</ul>
</div>
<div className="flex-1 text-center">
<Image
src="/img/wrAbout/icon_03.png"
alt=""
width={70}
height={70}
className="mx-auto mb-4 w-[auto] h-[70px]"
/>
<h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">물 재이용 인식제고<br />및 산업발전 기반 마련</h3>
<ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1">
<li>- 물 재이용 교육 및 대국민 홍보 강화</li>
<li>- 핵심기술 육성 및 산업발전 지원 강화</li>
</ul>
</div>
</div>
<Image
src="/img/wrAbout/img_06_2.png"
alt="물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다."
width={800}
height={400}
className="w-[90%] mx-auto"
/>
</section>
</article>
</main>
</div>
);
}
@@ -2,49 +2,49 @@
export default function WrExamplePage() {
return (
<div className="bg-[#F1F6F9]">
<div className="bg-[#F2F2F2]">
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 rounded-full"></span>
물 재이용사례
</h2>
</div>
<div className="bg-[#F1F6F9] text-gray-800">
<div className="bg-[#F2F2F2] text-gray-800">
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
{/* 카드 1 */}
<div className="card-wrap mb-24" id="card1">
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
사업사례 1
</h2>
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
{/* 사업명 */}
<section>
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
경기권역 반도체 사업장 물 재이용 사업<br />
: 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등)
</p>
</section>
{/* 사업 배경 및 목적 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 배경 및 목적</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 배경 및 목적</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정
</p>
</section>
{/* 사업 주요 내용 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]<br />
2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)]
</p>
</section>
{/* 특이사항 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행
</p>
</section>
@@ -63,15 +63,15 @@ export default function WrExamplePage() {
{/* 카드2 */}
<div className="card-wrap mb-24" id="card2">
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
사업사례 2
</h2>
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
{/* 사업명 */}
<section>
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
경기도 파주시 하수재이용<br />
: 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급<br /><br />
*LG디스플레이(P10공장), 희성전자, 엔비텍 등
@@ -79,15 +79,15 @@ export default function WrExamplePage() {
</section>
{/* 사업 방식 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)
</p>
</section>
{/* 사업 주요 내용 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
공급량 : 4만m³/일<br />
관로 : 13.6km<br />
@@ -96,8 +96,8 @@ export default function WrExamplePage() {
</section>
{/* 특이사항 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례
</p>
<div className="grid grid-cols-1 gap-0 items-start">
@@ -124,29 +124,29 @@ export default function WrExamplePage() {
</colgroup>
<tbody>
<tr>
<td colSpan={3} className="bg-[#ffc000] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">하수처리수 공급</td>
<td colSpan={3} className="font-NotoSansKR bg-[#ffc000] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">하수처리수 공급</td>
</tr>
<tr className="text-xl h-[30px]">
<td className="text-xl h-[30px] border-black border p-1 font-bold">위치</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">금촌하수</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">운정하수</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">위치</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">금촌하수</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">운정하수</td>
</tr>
<tr className="text-sm">
<td className="text-xl h-[30px] border-black border p-1 font-bold">시설용량</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">44,000㎥/일</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">60,000㎥/일</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">시설용량</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">44,000㎥/일</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">60,000㎥/일</td>
</tr>
<tr className="text-sm">
<td className="text-xl h-[30px] border-black border p-1 font-bold">유입량</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">27,000㎥/일<br />자연유하</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">30,000㎥/일<br />이송펌프장</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">유입량</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">27,000㎥/일<br />자연유하</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">30,000㎥/일<br />이송펌프장</td>
</tr>
</tbody>
</table>
{/* 첫 화살표 (자연유하 / 관로이송) */}
<div className="flex h-[30px] justify-between items-center my-3 px-4">
<div className="flex text-xl items-center gap-1 text-orange-600 font-bold pl-6">
<span>자연유하: 161m</span>
<span className="font-NotoSansKR">자연유하: 161m</span>
<Image
src="/img/wrExample/img_arrow_down_1.png"
alt="오른쪽 화살표"
@@ -163,7 +163,7 @@ export default function WrExamplePage() {
height={24}
className="h-6 w-6"
/>
<span>관로이송: 3.2km</span>
<span className="font-NotoSansKR">관로이송: 3.2km</span>
</div>
</div>
</div>
@@ -177,26 +177,26 @@ export default function WrExamplePage() {
</colgroup>
<tbody>
<tr>
<td colSpan={2} className="bg-[#00b050] text-xl h-[30px] text-white font-semibold text-center pt-[6px] pb-[6px] px-0">물재이용 처리시설</td>
<td colSpan={2} className="font-NotoSansKR bg-[#00b050] text-xl h-[30px] text-white font-semibold text-center pt-[6px] pb-[6px] px-0">물재이용 처리시설</td>
</tr>
<tr className="text-sm">
<td className="text-xl h-[30px] border-black border p-1 font-bold">유입유량</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">57,000㎥/일</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">유입유량</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">57,000㎥/일</td>
</tr>
<tr className="text-sm">
<td className="text-xl h-[30px] border-black border p-1 font-bold">재이용시설</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">재이용시설</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
</tr>
<tr className="text-sm">
<td className="text-xl h-[30px] border-black border p-1 font-bold">농축폐수처리</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">17,000㎥/일</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">농축폐수처리</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">17,000㎥/일</td>
</tr>
</tbody>
</table>
{/* 두 번째 화살표 (공급관로) */}
<div className="flex flex-col items-center my-3">
<div className="flex h-[30px] items-center gap-1 text-xl text-blue-600 font-semibold">
<span>공급관로 L=10.4km</span>
<span className="font-NotoSansKR">공급관로 L=10.4km</span>
<Image
src="/img/wrExample/img_arrow_down_2.png"
alt="아래 화살표"
@@ -217,15 +217,15 @@ export default function WrExamplePage() {
</colgroup>
<tbody>
<tr>
<td colSpan={2} className="bg-[#3399ff] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">수요처 공급</td>
<td colSpan={2} className="font-NotoSansKR bg-[#3399ff] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">수요처 공급</td>
</tr>
<tr className="text-sm">
<td className="text-xl h-[30px] border-black border p-1 font-bold">수요처</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">LG디스플레이 외 2개사</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">수요처</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">LG디스플레이 외 2개사</td>
</tr>
<tr className="text-sm">
<td className="text-xl h-[30px] border-black border p-1 font-bold">수요량</td>
<td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">수요량</td>
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
</tr>
</tbody>
</table>
@@ -238,30 +238,30 @@ export default function WrExamplePage() {
{/* 카드3 */}
<div className="card-wrap mb-24" id="card3">
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
사업사례 3
</h2>
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
{/* 사업명 */}
<section>
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
충남 보령 하수재이용<br />
: 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진
</p>
</section>
{/* 사업 방식 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결
</p>
</section>
{/* 사업 주요 내용 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
공급량 : 1만m3/일<br />
관로 : 8.7km<br />
@@ -270,8 +270,8 @@ export default function WrExamplePage() {
</section>
{/* 특이사항 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU (환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕
으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모
</p>
@@ -290,22 +290,22 @@ export default function WrExamplePage() {
{/* 카드4 */}
<div className="card-wrap mb-24" id="card4">
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
사업사례 4
</h2>
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
{/* 사업명 */}
<section>
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
경기도 수원시 빗물이용과 중수도
</p>
</section>
{/* 사업 내용 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 내용</h2>
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 내용</h2>
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감<br />
&nbsp;&nbsp;※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(21.7월, 수원시청).<br />
&nbsp;&nbsp;다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란<br /><br />
@@ -320,7 +320,7 @@ export default function WrExamplePage() {
height={250}
className="w-[100%] mx-auto"
/>
<span className="text-xl block text-center font-semibold my-2"><월드컵경기장 인근도로 노면 살수></span>
<span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><월드컵경기장 인근도로 노면 살수></span>
</div>
<div>
<Image
@@ -330,7 +330,7 @@ export default function WrExamplePage() {
height={250}
className="w-[100%] mx-auto"
/>
<span className="text-xl block text-center font-semibold my-2"><주유기 모양의 빗물 공급기></span>
<span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><주유기 모양의 빗물 공급기></span>
</div>
<div>
<Image
@@ -340,7 +340,7 @@ export default function WrExamplePage() {
height={250}
className="w-[100%] mx-auto"
/>
<span className="text-xl block text-center font-semibold my-2"><빗물주유기에서 살수용수 공급></span>
<span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><빗물주유기에서 살수용수 공급></span>
</div>
<div>
<Image
@@ -350,7 +350,7 @@ export default function WrExamplePage() {
height={250}
className="w-[100%] mx-auto"
/>
<span className="text-xl block text-center font-semibold my-2"><빗물 공급기 및 빗물분사장치 위치></span>
<span className="font-NotoSansKR text-xl block text-center font-semibold my-2"><빗물 공급기 및 빗물분사장치 위치></span>
</div>
</div>
</section>
@@ -0,0 +1,113 @@
import Image from "next/image";
export default function WrOverviewPage() {
return (
<div className="bg-[#F2F2F2]">
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
{/* 카드 1 - 물재이용 사업 추진 절차 */}
<div className="card-wrap mb-24" id="card1">
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용 사업 추진 절차
</h2>
<div className="bg-white border border-gray-300 rounded-xl p-[60px] py-[50px]">
<table className="table-fixed w-full text-center text-sm">
<caption className="sr-only">물 재이용 추진 절차 상세 표</caption>
<colgroup>
<col style={{ width: "28%" }} />
<col style={{ width: "2%" }} />
<col style={{ width: "35%" }} />
<col style={{ width: "5%" }} />
<col style={{ width: "30%" }} />
</colgroup>
<tbody>
{/* 1단계 */}
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">타당성 조사</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 2단계 */}
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">매칭시스템 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">사업계획(안) 수립</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 3단계 */}
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">하수도정보시스템 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">국고지원 신청</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 4단계 */}
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">국고지원 확정 통보</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 5단계 */}
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">사업계획 수립(실시설계)</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 6단계 */}
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">재원협의(지방환경관서장)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 7단계 */}
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">설치승인(시도지사)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 8단계 */}
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">공사시행</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 9단계 */}
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">준공(국고정산)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
</tbody>
</table>
</div>
</div>
{/* 카드 2 - 물재이용 매칭 시스템 */}
<div className="mt-36 card-wrap mb-24" id="card2">
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용 매칭 시스템이란?
</h2>
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
{/* 개요 */}
<section>
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">개요</h2>
<p className="font-NotoSansKR text-2xl font-bold text-gray-800 leading-relaxed">
기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환
</p>
</section>
{/* 구성 */}
<section className="mt-8">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">구성</h2>
<ul className=" font-NotoSansKR list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
<li>공급자: 하수처리장, 빗물이용시설, 중수도 등</li>
<li>수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등</li>
<li>시스템: 수질, 수량, 거리 기반 매칭, 중개</li>
<li>운영주체: 한국환경공단</li>
</ul>
</section>
{/* 절차 */}
<section className="mt-8">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
<p className="font-NotoSansKR text-2xl font-bold text-gray-800 leading-relaxed">
① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행
</p>
</section>
{/* 기대효과 */}
<section className="mt-8">
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">기대효과</h2>
<ul className="font-NotoSansKR list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
<li>물 자원 수요-공급 조정을 통한 효율적 분배</li>
<li>물 재이용시설 데이터 수집으로 체계적 관리</li>
<li>지자체, 사업시행자 등 재이용 정보 획득 용이</li>
</ul>
<div className="text-center my-6">
<Image
src="/img/wrOverview/img_ov_01_1.png"
alt="하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함."
width={650}
height={500}
className="h-[500px] mx-auto mb-4 rounded"
/>
<p className="font-NotoSansKR text-2xl font-bold text-gray-800 mt-10">(하수 재이용 시스템 개요도)</p>
</div>
</section>
</div>
</div>
</div>
</div>
);
}
@@ -12,10 +12,10 @@ interface RegionData {
interface TableData {
region: string;
sewage: number;
rainwater: number;
reclaimed: number;
thermal: number;
sewage: string;
rainwater: string;
reclaimed: string;
thermal: string;
}
@@ -161,7 +161,7 @@ const WrStatusPage: React.FC = () => {
chartTitle : '',
chartClassName : "h-[300px] w-full",
graphSize : 40,
yFieldText : '개소수(건)',
yFieldText : '시설(개소)',
showLegend : true
}
@@ -193,24 +193,24 @@ const WrStatusPage: React.FC = () => {
};
const tableData: TableData[] = [
{ region: '서울특별시', sewage: 4, rainwater: 816, reclaimed: 371, thermal: 0 },
{ region: '부산광역시', sewage: 13, rainwater: 68, reclaimed: 20, thermal: 1 },
{ region: '대구광역시', sewage: 9, rainwater: 294, reclaimed: 14, thermal: 2 },
{ region: '인천광역시', sewage: 17, rainwater: 165, reclaimed: 35, thermal: 15 },
{ region: '광주광역시', sewage: 3, rainwater: 74, reclaimed: 15, thermal: 15 },
{ region: '대전광역시', sewage: 2, rainwater: 52, reclaimed: 11, thermal: 20 },
{ region: '울산광역시', sewage: 8, rainwater: 111, reclaimed: 19, thermal: 30 },
{ region: '세종특별자치시', sewage: 9, rainwater: 65, reclaimed: 0, thermal: 0 },
{ region: '경기도', sewage: 176, rainwater: 1009, reclaimed: 252, thermal: 40 },
{ region: '강원특별자치도', sewage: 69, rainwater: 69, reclaimed: 22, thermal: 0 },
{ region: '충청북도', sewage: 38, rainwater: 17, reclaimed: 91, thermal: 0 },
{ region: '충청남도', sewage: 67, rainwater: 54, reclaimed: 188, thermal: 0 },
{ region: '전라북도', sewage: 55, rainwater: 33, reclaimed: 220, thermal: 0 },
{ region: '전라남도', sewage: 83, rainwater: 16, reclaimed: 75, thermal: 0 },
{ region: '경상북도', sewage: 81, rainwater: 37, reclaimed: 70, thermal: 0 },
{ region: '경상남도', sewage: 75, rainwater: 27, reclaimed: 140, thermal: 0 },
{ region: '제주특별자치도', sewage: 8, rainwater: 47, reclaimed: 64, thermal: 0 },
{ region: '전국', sewage: '717(1,137,538)', rainwater: '3544(13,945)', reclaimed: '1018(222,476)', thermal: '123' },
{ region: '서울특별시', sewage: '4(114,908)', rainwater: '816(430)', reclaimed: '371(341)', thermal: '0' },
{ region: '부산광역시', sewage: '13(146,197)', rainwater: '68(335)', reclaimed: '20(512)', thermal: '1' },
{ region: '대구광역시', sewage: '9(102,380)', rainwater: '294(133)', reclaimed: '14(44)', thermal: '2' },
{ region: '인천광역시', sewage: '17(57,691)', rainwater: '165(642)', reclaimed: '35(7,859)', thermal: '15' },
{ region: '광주광역시', sewage: '3(28,738)', rainwater: '74(23)', reclaimed: '15(450)', thermal: '15' },
{ region: '대전광역시', sewage: '2(11,966)', rainwater: '52(2)', reclaimed: '11(492)', thermal: '20' },
{ region: '울산광역시', sewage: '8(36,076)', rainwater: '111(39)', reclaimed: '19(28,904)', thermal: '30' },
{ region: '세종특별자치시', sewage: '9(2,555)', rainwater: '65(16)', reclaimed: '0(0)', thermal: '0' },
{ region: '경기도', sewage: '176(313,298)', rainwater: '1009(4,735)', reclaimed: '252(77,096)', thermal: '40' },
{ region: '강원특별자치도', sewage: '69(19,677)', rainwater: '69(4)', reclaimed: '22(447)', thermal: '0' },
{ region: '충청북도', sewage: '38(15,607)', rainwater: '17(91)', reclaimed: '91(2,211)', thermal: '0' },
{ region: '충청남도', sewage: '67(41,368)', rainwater: '54(77)', reclaimed: '188(50,434)', thermal: '0' },
{ region: '전라북도', sewage: '55(42,671)', rainwater: '33(199)', reclaimed: '220(9,453)', thermal: '0' },
{ region: '전라남도', sewage: '83(23,560)', rainwater: '16(885)', reclaimed: '75(15,412)', thermal: '0' },
{ region: '경상북도', sewage: '81(129,081)', rainwater: '37(341)', reclaimed: '70(27,818)', thermal: '0' },
{ region: '경상남도', sewage: '75(50,101)', rainwater: '27(244)', reclaimed: '140(343)', thermal: '0' },
{ region: '제주특별자치도', sewage: '8(1,666)', rainwater: '47(5,746)', reclaimed: '64(660)', thermal: '0' },
];
const regionData: RegionData[] = [
@@ -395,7 +395,7 @@ const WrStatusPage: React.FC = () => {
}, [selectedRegion]);
return (
<div className="flex h-screen bg-[#F4F7FB] text-gray-800">
<div className="flex h-screen bg-[#F2F2F2] text-gray-800">
{/* 우측 콘텐츠 영역 */}
<main className="flex-1 p-6 overflow-auto">
{/* 상단 제목 */}
@@ -410,8 +410,8 @@ const WrStatusPage: React.FC = () => {
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden max-h-[900px]">
{/* 타이틀 영역 */}
<div className="border-b border-[#DBE2E6] px-4 py-3">
<h3 className="text-[12px] font-semibold">재이용시설개소수(2023)</h3>
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 개소수 조회</p>
<h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3>
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황(개소)</p>
</div>
{/* 본문 콘텐츠 */}
@@ -455,19 +455,19 @@ const WrStatusPage: React.FC = () => {
<div className="absolute bottom-2 right-2 bg-white rounded-lg shadow-md p-4 text-sm space-y-2">
<div className="flex items-center space-x-2">
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FF6B6B'}}></span>
<span className="text-[12px] text-gray-900">하수재이용시설 (총 717건)</span>
<span className="text-[12px] text-gray-900">하수재이용시설 (총 717개소)</span>
</div>
<div className="flex items-center space-x-2">
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#4ECDC4'}}></span>
<span className="text-[12px] text-gray-900">빗물이용시설 (총 3,730건)</span>
<span className="text-[12px] text-gray-900">빗물이용시설 (총 3,730개소)</span>
</div>
<div className="flex items-center space-x-2">
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#45B7D1'}}></span>
<span className="text-[12px] text-gray-900">중수도시설 (총 1,074건)</span>
<span className="text-[12px] text-gray-900">중수도시설 (총 1,074개소)</span>
</div>
<div className="flex items-center space-x-2">
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FED16A'}}></span>
<span className="text-[12px] text-gray-900">온배수시설 (총 201건)</span>
<span className="text-[12px] text-gray-900">온배수시설 (총 201개소)</span>
</div>
</div>
</div>
@@ -482,7 +482,7 @@ const WrStatusPage: React.FC = () => {
<div className="flex justify-between items-center border-b border-[#DBE2E6] px-4 py-3">
<div>
<h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3>
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 현황 비교</p>
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황(개소)</p>
</div>
</div>
<div className="p-4">
@@ -493,10 +493,10 @@ const WrStatusPage: React.FC = () => {
<tr>
<th className="text-[12px] font-semibold px-4 py-2">번호</th>
<th className="text-[12px] font-semibold px-4 py-2">시도</th>
<th className="text-[12px] font-semibold px-4 py-2">하수</th>
<th className="text-[12px] font-semibold px-4 py-2">빗물</th>
<th className="text-[12px] font-semibold px-4 py-2">중수도</th>
<th className="text-[12px] font-semibold px-4 py-2">온배수</th>
<th className="text-[12px] font-semibold px-4 py-2">하수(이용량,천m³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">빗물(이용량,천m³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">중수도(이용량,천m³/년)</th>
<th className="text-[12px] font-semibold px-4 py-2">온배수(이용량,천m³/년)</th>
</tr>
</thead>
<tbody className="divide-y">
@@ -522,7 +522,7 @@ const WrStatusPage: React.FC = () => {
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden">
<div className="border-b border-[#DBE2E6] px-4 py-3">
<h3 className="text-[12px] font-semibold">재이용통계(2023)</h3>
<p className="text-[12px] text-[#999] font-medium">항목별 통계 보기</p>
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 현황 개소수 그래프</p>
</div>
<div className="p-4 bg-[#f9fbfd]">
<div className="h-[305px] text-gray-500">
+11
View File
@@ -0,0 +1,11 @@
"use client";
import "../globals.css";
import { StoreProvider } from "../store";
export default function PopupLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <StoreProvider>{children}</StoreProvider>;
}
@@ -0,0 +1,170 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { useEffect, useRef, useState, useCallback } from "react";
import { mapService } from "../service";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { SearchBar } from "@/components/ui/map/searchBar/searchBar";
import { useDmStore } from "@/app/store";
// export type SubsInfo={
// ruseUtztnFcltNm: string; // 재이용시설명
// ruseUtztnFcltCd: string; // 재이용시설 코드
// }
const Map = () => {
//수요처 저장하는 함수 setSubsInfo
//setSubsInfo(subsInfo:SubsInfo[]):void
const { map,setSubsInfo } = useDmStore(
(state) => state
);
//저장된 수요처 리스트
const { subsInfo } = map;
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const gisMaps = useRef<any | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const [checkedLayers, setCheckedLayers] = useState<string[]>([]);
const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]);
const [selectDataPk, setSelectDataPk] = useState<string>("");
const [subs, setSubs] = useState<object[]>([]);
//적용 버튼 클릭 이벤트
const onApply = () => {
//setSubs([{'41110PB001R1':'수원제1,2 재이용1'},{'41110PB001R2':'수원제1,2 재이용2'}]);
console.log(subs)
};
const fetchData = async () => {
try {
const response = await mapService.getLyrStngs();
setTreeData(response); // API 응답 저장
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
// 팝업 닫기 핸들러를 useCallback으로 메모이제이션
const handlePopupClose = useCallback(() => {
setIsOpen(false);
setFeatureData(null);
}, []);
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
console.log("Selected Data Pk: ", selectDataPk);
}, [selectDataPk]);
// 체크된 레이어 정보를 받는 핸들러
const handleCheckedLayersChange = (
checkedIds: string[],
checkedNodesData: any[]
) => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
};
// SearchModal, GisContent 등에서 호출
const onFeatureSelect = (data: any) => {
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as any;
if (!select) return;
// 1. 기존 선택 초기화
select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
let foundFeature: any = null;
if (
data &&
typeof data.getGeometry === "function" &&
typeof data.setStyle === "function"
) {
// 이미 Feature 인스턴스라면 그대로 사용
foundFeature = data;
} else {
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
const featureId = data.fid || data.ftrIdn || data.id;
map
.getLayers()
.getArray()
.forEach((layer: any) => {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) foundFeature = feature;
}
});
}
// 3. 피처 선택
if (foundFeature) {
const geometry = foundFeature.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
constrainResolution: true,
nearest: true,
});
}
select.selectFeature(foundFeature, true);
} else {
console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
}
setFeatureData(data);
setIsOpen(true);
setIsSearchModalOpen(false);
};
return (
<>
<SearchBar
setHasuPk={(hasuPk: string) => {
if (hasuPk !== "") {
setSelectDataPk(hasuPk);
}
}}
onApply={onApply}
/>
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
isMiniMap={false}
searchView={undefined}
detailView={null}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
imgVisble={false}
setData={setSubs}
/>
</div>
</>
);
};
export default Map;
@@ -0,0 +1,147 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { useEffect, useRef, useState, useCallback } from "react";
import { mapService } from "../service";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { SearchBar } from "@/components/ui/map/searchBar/searchBar";
const Map = () => {
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const gisMaps = useRef<any | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const [checkedLayers, setCheckedLayers] = useState<string[]>([]);
const [checkedLayersData, setCheckedLayersData] = useState<any[]>([]);
const [selectDataPk, setSelectDataPk] = useState<string>("");
const fetchData = async () => {
try {
const response = await mapService.getLyrStngs();
setTreeData(response); // API 응답 저장
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
// 팝업 닫기 핸들러를 useCallback으로 메모이제이션
const handlePopupClose = useCallback(() => {
setIsOpen(false);
setFeatureData(null);
}, []);
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
console.log("Selected Data Pk: ", selectDataPk);
}, [selectDataPk]);
// 체크된 레이어 정보를 받는 핸들러
const handleCheckedLayersChange = (
checkedIds: string[],
checkedNodesData: any[]
) => {
setCheckedLayers(checkedIds);
setCheckedLayersData(checkedNodesData);
};
// SearchModal, GisContent 등에서 호출
const onFeatureSelect = (data: any) => {
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as any;
if (!select) return;
// 1. 기존 선택 초기화
select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
let foundFeature: any = null;
if (
data &&
typeof data.getGeometry === "function" &&
typeof data.setStyle === "function"
) {
// 이미 Feature 인스턴스라면 그대로 사용
foundFeature = data;
} else {
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
const featureId = data.fid || data.ftrIdn || data.id;
map
.getLayers()
.getArray()
.forEach((layer: any) => {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) foundFeature = feature;
}
});
}
// 3. 피처 선택
if (foundFeature) {
const geometry = foundFeature.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
constrainResolution: true,
nearest: true,
});
}
select.selectFeature(foundFeature, true);
} else {
console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
}
setFeatureData(data);
setIsOpen(true);
setIsSearchModalOpen(false);
};
return (
<>
<SearchBar
setHasuPk={(hasuPk: string) => {
if (hasuPk !== "") {
setSelectDataPk(hasuPk);
}
}}
/>
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
isMiniMap={false}
searchView={undefined}
detailView={null}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
imgVisble={false}
/>
</div>
</>
);
};
export default Map;
@@ -0,0 +1,94 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';
// 현재 문서의 스타일(CSS)을 새 창으로 복사하는 함수
const copyStyles = (sourceDoc: Document, targetDoc: Document) => {
Array.from(sourceDoc.styleSheets).forEach(styleSheet => {
try {
if (styleSheet.cssRules) { // <style> 태그로 주입된 CSS
const newStyleEl = sourceDoc.createElement('style');
Array.from(styleSheet.cssRules).forEach(cssRule => {
newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText));
});
targetDoc.head.appendChild(newStyleEl);
} else if (styleSheet.href) { // <link> 태그로 연결된 CSS 파일
const newLinkEl = sourceDoc.createElement('link');
newLinkEl.rel = 'stylesheet';
newLinkEl.href = styleSheet.href;
targetDoc.head.appendChild(newLinkEl);
}
} catch (e) {
console.warn('스타일시트를 복사할 수 없습니다:', e);
}
});
};
interface NewWindowPortalProps {
children: React.ReactNode;
onClose: () => void;
title?: string;
width?: number;
height?: number;
resizable?: string;
scrollbars?: string;
}
const NewWindowPortal: React.FC<NewWindowPortalProps> = ({ children, onClose, title = "" , width = 900, height = 800, resizable = "yes", scrollbars= "yes" }) => {
const [container, setContainer] = useState<HTMLElement | null>(null);
const newWindowRef = useRef<Window | null>(null);
const onCloseRef = useRef(onClose);
// onClose 함수를 ref로 저장하여 의존성 문제 해결
useEffect(() => {
onCloseRef.current = onClose;
}, [onClose]);
const handleUnload = useCallback(() => {
onCloseRef.current();
}, []);
useEffect(() => {
// 이미 창이 열려있으면 새로 열지 않음
if (newWindowRef.current && !newWindowRef.current.closed) return;
// 1. 새 창을 열고 참조를 저장합니다.
const popupOptions = `width=${width},height=${height},resizable=${resizable},scrollbars=${scrollbars}`;
newWindowRef.current = window.open('', title, popupOptions);
if (newWindowRef.current) {
newWindowRef.current.document.title = title;
// 2. React 컴포넌트를 마운트할 컨테이너 div를 생성하여 새 창의 body에 추가합니다.
const div = newWindowRef.current.document.createElement('div');
newWindowRef.current.document.body.appendChild(div);
setContainer(div);
// 3. 메인 창의 스타일을 새 창으로 복사합니다. (매우 중요)
copyStyles(document, newWindowRef.current.document);
// 4. 사용자가 창을 닫을 때 부모에게 알리기 위한 이벤트 리스너를 설정합니다.
// const handleUnload = () => {
// onClose();
// };
newWindowRef.current.addEventListener('unload', handleUnload);
// 5. 컴포넌트가 사라질 때 창을 닫는 클린업 함수를 반환합니다.
return () => {
if (newWindowRef.current) {
newWindowRef.current.removeEventListener('unload', handleUnload);
if (!newWindowRef.current.closed) {
newWindowRef.current.close();
}
}
};
} else {
alert('팝업이 차단되었습니다. 팝업 차단을 해제하고 다시 시도해주세요.');
onCloseRef.current();
}
}, []);
// `createPortal`을 사용해 children을 새 창의 container div에 렌더링합니다.
return container ? createPortal(children, container) : null;
};
export default NewWindowPortal;
@@ -0,0 +1 @@
export * from './map'
@@ -0,0 +1,46 @@
/**
* 관망도 API
*/
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map`
export type LyrStngProps = {
lyr_sn: string
lyr_nm: string
upper_lyr_sn: string
drawOrdr: string
flterSql: string
gobjTy: string
lyrTpNm: string
useYn: string
minViewSize: string
iconNm: string
lblAtrbNm: string
fontNm: string
fontIntnColor: string
fontExtnColor: string
fontExtnWdth: string
lnWdth: string
lnColor: string
polgnColor: string
initEprssYn: string
clickPsblYn: string
}
export const mapService = {
//레이어 설정 조회
getLyrStngs: async () => {
const response = await fetch(`${API_URL}/lyrStngs/DM_NETWORK`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
}
+4 -7
View File
@@ -1,7 +1,5 @@
"use client"
import Base from "./base";
import "./globals.css";
import { StoreProvider } from "./store";
export default function RootLayout({
children,
@@ -9,12 +7,11 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<html lang="ko">
<body>
<StoreProvider>
<Base>{children}</Base>
</StoreProvider>
{children}
</body>
</html>
);
}
}
+130
View File
@@ -0,0 +1,130 @@
import { NextRequest, NextResponse } from "next/server";
import path from "path";
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || "";
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
const DC_URL = process.env.NEXT_PUBLIC_DC_URL || "";
export function middleware(req: NextRequest) {
console.log("Middleware 진입:", req.nextUrl.pathname);
const pathname = req.nextUrl.pathname;
const searchParams = req.nextUrl.searchParams;
if (pathname.startsWith("/server")) {
const decodedParams = Array.from(searchParams.entries())
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
.join("&");
const newUrl = `${SERVER_API_URL}${pathname.replace("/server", "")}${
decodedParams ? `?${decodedParams}` : ""
}`;
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname.startsWith("/geoserver")) {
let queryParams = searchParams.toString();
const typename = searchParams.get("TYPENAME");
let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
if (
typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설
|| typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역
|| typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도
|| typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도
) {
newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
}
else if (
typename == "hasudo:tb_ms_swage_ppl" //관로
|| typename == "hasudo:tb_ms_swage_mhl" //맨홀
|| typename == "hasudo:tb_ms_swage_pmstn" //펌프장
|| typename == "hasudo:tb_ms_stofcbr" //우수토실
) {
newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
}
else if (
typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실
|| typename == "hasudo:om_wqlt_station" //관측소
|| typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설
|| typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설
|| typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설
|| typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설
|| typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처
|| typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처
){
const url = req.nextUrl.clone();
url.searchParams.set('TYPENAME', typename.replace("hasudo:",""));
queryParams = url.searchParams.toString();
newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`;
}
console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`);
return NextResponse.rewrite(new URL(newUrl));
}
if (
pathname.startsWith("/attach-service") ||
pathname.startsWith("/cms-service")
) {
const requestHeaders = new Headers(req.headers);
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
const newUrl = `${SERVER_API_URL}${pathname}`;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
console.log(
`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`,
);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
}
if (pathname.startsWith("/user-service")) {
const requestHeaders = new Headers(req.headers);
const newUrl = `${SERVER_API_URL}${pathname}`;
console.log(
`[Middleware] Rewriting from '/user-service' path to: ${newUrl}`,
);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
}
if (pathname.startsWith("/dc-service")) {
const requestHeaders = new Headers(req.headers);
const decodedParams = Array.from(searchParams.entries())
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
.join("&");
const newUrl = `${DC_URL}${pathname}${decodedParams ? `?${decodedParams}` : ""}`;
console.log(
`[Middleware] Rewriting from '/dc-service' path to: ${newUrl}`,
);
return NextResponse.rewrite(new URL(newUrl), {
headers: requestHeaders,
});
}
return NextResponse.next();
}
export const config = {
matcher: [
"/server/:path*",
"/geoserver/:path*",
"/attach-service/:path*",
"/user-service/:path*",
"/cms-service/:path*",
"/dc-service/:path*",
],
};
-335
View File
@@ -1,335 +0,0 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import Image from 'next/image';
const NAVS = [
{ id: 'card1', label: '물재이용이란' },
{ id: 'card2', label: '물재이용 필요성' },
{ id: 'card3', label: '빗물 재이용' },
{ id: 'card4', label: '중수 재이용' },
{ id: 'card5', label: '하수 재이용' },
{ id: 'card6', label: '물재이용 목표' },
];
export default function WrAboutPage() {
const [activeIdx, setActiveIdx] = useState(0);
const navRefs = useRef<(HTMLLIElement | null)[]>([]);
const cardRefs = useRef<(HTMLElement | null)[]>([]);
// 클릭 시 이동
const handleNavClick = (idx: number) => {
setActiveIdx(idx);
const targetCard = cardRefs.current[idx];
if (targetCard) {
targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
// 키보드 이벤트 처리
const handleKeyDown = (e: React.KeyboardEvent, idx: number) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleNavClick(idx);
}
};
useEffect(() => {
const observerOptions = {
root: null,
rootMargin: '-200px 0px 0px 0px', // 상단에서 200px 지점을 기준으로
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const idx = cardRefs.current.findIndex(ref => ref === entry.target);
if (idx !== -1) {
setActiveIdx(idx);
}
}
});
}, observerOptions);
// 각 카드 요소를 관찰
cardRefs.current.forEach((card) => {
if (card) {
observer.observe(card);
}
});
return () => {
cardRefs.current.forEach((card) => {
if (card) {
observer.unobserve(card);
}
});
};
}, []);
return (
<main className="relative w-[950px] mx-auto pt-[100px] font-sans">
{/* 사이드바 */}
<nav className="fixed left-[290px] top-[195px] w-[210px] z-50" aria-label="물재이용 정보 섹션 내비게이션">
<ul role="list" className="relative space-y-6">
{NAVS.map((nav, idx) => (
<li
key={nav.id}
ref={(el) => { navRefs.current[idx] = el; }}
role="listitem"
tabIndex={0}
aria-current={idx === activeIdx ? 'true' : 'false'}
aria-label={`현재 위치: ${nav.label}`}
onClick={() => handleNavClick(idx)}
onKeyDown={(e) => handleKeyDown(e, idx)}
className={`nav-item relative pl-6 text-2xl font-bold text-gray-700 cursor-pointer transition-all duration-300
after:content-[''] after:absolute after:top-6 after:left-[0.5rem] after:w-[2px] after:h-[24px] after:bg-[#C5C5C5]
${idx === activeIdx ? 'text-4xl font-bold text-gray-900' : ''}`}
>
<span className={`absolute left-[0.5rem] top-1 w-4 h-4 rounded-full -translate-x-1/2 transition-all duration-300
${idx === activeIdx ? 'bg-blue-500 border-blue-500' : 'bg-white border-gray-400 border'}`}></span>
{nav.label}
</li>
))}
</ul>
</nav>
<article ref={(el) => { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title">
<h2 id="card1-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용이란
</h2>
<section aria-labelledby="card1-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card1-desc" className="text-[#269DDA] text-4xl font-bold mb-8">물 재이용이 무엇인가요?</h1>
<p className="text-2xl font-bold mb-10 leading-relaxed">
물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를<br />
물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,<br />
하천 유지 등의 용도로 이용하는 것을 말합니다.
</p>
<Image
src="/img/wrAbout/img_01_1.png"
alt="이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-8 rounded"
/>
<p className="text-center">
환경부는 2011년에 &apos;물의 재이용 촉진 및 지원에 관한 법률&apos;을 제정하여<br />
한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
</p>
</section>
</article>
{/* 카드2 */}
<article ref={(el) => { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title">
<h2 id="card2-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용 필요성
</h2>
<section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card2-desc" className="text-[#EE7B62] text-4xl font-bold mb-8">물 재이용, 왜 필요한가요?</h1>
<p className="text-2xl font-bold mb-10 leading-relaxed">
빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br />
물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다.
</p>
<Image
src="/img/wrAbout/img_02_1.png"
alt="이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다."
width={800}
height={400}
className="w-[90%] mx-auto mb-8 w-[80%]"
/>
<p className="text-2xl font-bold mb-10 leading-relaxed">
- 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.<br />
- 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다.
</p>
<Image
src="/img/wrAbout/img_02_2.png"
alt="세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-8"
/>
<p className="text-2xl font-bold mb-10 leading-relaxed">
- 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.<br />
- 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다.
</p>
<Image
src="/img/wrAbout/img_02_3.png"
alt="물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다."
width={800}
height={400}
className="w-[93%] mx-auto"
/>
</section>
</article>
{/* 카드3 */}
<article ref={(el) => { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title">
<h2 id="card3-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
빗물 재이용
</h2>
<section aria-labelledby="card3-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card3-desc" className="text-[#269DDA] text-4xl font-bold mb-6">친환경 수자원 빗물,<br />편리하게 사용할 수 있어요!</h1>
<p className="text-2xl font-bold mb-10 leading-relaxed">가뭄을 극복하고 홍수를 예방하기 위해<br />비가 내릴 때 빗물을 모아두었다가 사용합니다.</p>
<Image
src="/img/wrAbout/img_03_1.png"
alt="주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-6"
/>
<Image
src="/img/wrAbout/img_03_2.png"
alt="빗물이용"
width={200}
height={100}
className="mb-5 w-[20%]"
/>
<p className="text-2xl font-bold leading-relaxed">
- 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등<br />
- 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)<br />
- 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등
</p>
</section>
</article>
{/* 카드4 */}
<article ref={(el) => { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title">
<h2 id="card4-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
중수 재이용
</h2>
<section aria-labelledby="card4-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card4-desc" className="text-[#9771B0] text-4xl font-bold mb-6">요모조모 쓸모 많은 중수,<br />알뜰하게 재이용할 수 있어요!</h1>
<p className="text-2xl font-bold mb-10 leading-relaxed">한번 사용한 물을 중수도 시설을 통해 재생하여<br />개별적 또는 지역적으로 재이용할 수 있습니다.</p>
<Image
src="/img/wrAbout/img_04_1.png"
alt="욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-6"
/>
<Image
src="/img/wrAbout/img_04_2.png"
alt="중수재이용"
width={200}
height={100}
className="mb-5 w-[20%]"
/>
<p className="text-2xl font-bold leading-relaxed">
- 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등<br />
- 의무 재이용량: 물 사용량의 10% 이상<br />
- 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등
</p>
</section>
</article>
{/* 카드5 */}
<article ref={(el) => { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title">
<h2 id="card5-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
하수 재이용
</h2>
<section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card5-desc" className="text-[#00A5B1] text-4xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1>
<p className="text-2xl font-bold mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p>
<Image
src="/img/wrAbout/img_05_1.png"
alt="주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다."
width={800}
height={400}
className="w-[90%] mx-auto mb-6"
/>
<Image
src="/img/wrAbout/img_05_2.png"
alt="하수처리 재이용"
width={200}
height={100}
className="mb-5 w-[20%]"
/>
<p className="text-2xl font-bold leading-relaxed">
- 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)<br />
- 의무 재이용량: 일 하수처리량의 10% 이상<br />
- 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등
</p>
</section>
</article>
{/* 카드6 */}
<article ref={(el) => { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title">
<h2 id="card6-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용 목표
</h2>
<section aria-labelledby="card6-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
<h1 id="card6-desc" className="text-[#269DDA] text-4xl font-bold mb-6">2030년 물 재이용 목표를 위해<br />모두의 관심과 노력이 필요합니다.</h1>
<p className="text-2xl font-bold mb-6 leading-relaxed">
환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여<br />한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
</p>
<Image
src="/img/wrAbout/img_06_1.png"
alt="지속가능한 물 재이용 정착으로 건전한 물순환 확산"
width={800}
height={400}
className="w-[80%] mx-auto mb-6"
/>
<div className="flex gap-3 flex-wrap justify-between bg-gray-100 p-6 rounded-lg mb-8">
<div className="flex-1">
<Image
src="/img/wrAbout/icon_01.png"
alt=""
width={70}
height={70}
className="mx-auto mb-4 w-[auto] h-[70px]"
/>
<h3 className="text-center font-semibold text-nowrap mb-2">도시 물 순환이용<br />체계로의 전환</h3>
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
<li className="text-nowrap">- 지속가능한 물 순환이용체계 구축</li>
<li>- 물 재이용시설 관리체계 강화</li>
</ul>
</div>
<div className="flex-1 text-center">
<Image
src="/img/wrAbout/icon_02.png"
alt=""
width={70}
height={70}
className="mx-auto mb-4 w-[auto] h-[70px]"
/>
<h3 className="text-center font-semibold text-nowrap mb-2">유역기반 하수처리수 재이용<br />수요확대 및 공급체계 구축</h3>
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
<li>- 유역기반 통합물관리 연계 강화</li>
<li>- 하수처리수 재이용 공업용수 공급 활성화</li>
<li>- 하수처리수 재이용을 활용한 물순환 촉진</li>
</ul>
</div>
<div className="flex-1 text-center">
<Image
src="/img/wrAbout/icon_03.png"
alt=""
width={70}
height={70}
className="mx-auto mb-4 w-[auto] h-[70px]"
/>
<h3 className="text-center font-semibold text-nowrap mb-2">물 재이용 인식제고<br />및 산업발전 기반 마련</h3>
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
<li>- 물 재이용 교육 및 대국민 홍보 강화</li>
<li>- 핵심기술 육성 및 산업발전 지원 강화</li>
</ul>
</div>
</div>
<Image
src="/img/wrAbout/img_06_2.png"
alt="물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다."
width={800}
height={400}
className="w-[90%] mx-auto"
/>
</section>
</article>
</main>
);
}
-113
View File
@@ -1,113 +0,0 @@
import Image from "next/image";
export default function WrOverviewPage() {
return (
<div className="bg-[#F1F6F9]">
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
{/* 카드 1 - 물재이용 사업 추진 절차 */}
<div className="card-wrap mb-24" id="card1">
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용 사업 추진 절차
</h2>
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
<table className="table-fixed w-full text-center text-sm">
<caption className="sr-only">물 재이용 추진 절차 상세 표</caption>
<colgroup>
<col style={{ width: "28%" }} />
<col style={{ width: "2%" }} />
<col style={{ width: "35%" }} />
<col style={{ width: "5%" }} />
<col style={{ width: "30%" }} />
</colgroup>
<tbody>
{/* 1단계 */}
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">타당성 조사</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 2단계 */}
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">매칭시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획(안) 수립</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 3단계 */}
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">하수도정보시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 신청</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 4단계 */}
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 확정 통보</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 5단계 */}
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획 수립(실시설계)</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 6단계 */}
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">재원협의<br />(지방환경관서장)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 7단계 */}
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">설치승인<br />(시도지사)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 8단계 */}
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">공사시행</td><td></td><td></td></tr>
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
{/* 9단계 */}
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">준공(국고정산)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
</tbody>
</table>
</div>
</div>
{/* 카드 2 - 물재이용 매칭 시스템 */}
<div className="card-wrap mb-24" id="card2">
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
물재이용 매칭 시스템이란?
</h2>
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
{/* 개요 */}
<section>
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">개요</h2>
<p className="text-2xl font-bold text-gray-800 leading-relaxed">
기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환
</p>
</section>
{/* 구성 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">구성</h2>
<ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
<li>공급자: 하수처리장, 빗물이용시설, 중수도 등</li>
<li>수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등</li>
<li>시스템: 수질, 수량, 거리 기반 매칭, 중개</li>
<li>운영주체: 한국환경공단</li>
</ul>
</section>
{/* 절차 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
<p className="text-2xl font-bold text-gray-800 leading-relaxed">
① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행
</p>
</section>
{/* 기대효과 */}
<section className="mt-8">
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">기대효과</h2>
<ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
<li>물 자원 수요-공급 조정을 통한 효율적 분배</li>
<li>물 재이용시설 데이터 수집으로 체계적 관리</li>
<li>지자체, 사업시행자 등 재이용 정보 획득 용이</li>
</ul>
<div className="text-center my-6">
<Image
src="/img/wrOverview/img_ov_01_1.png"
alt="하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함."
width={650}
height={500}
className="h-[500px] mx-auto mb-4 rounded"
/>
<p className="text-2xl font-bold text-gray-800 mt-10">(하수 재이용 시스템 개요도)</p>
</div>
</section>
</div>
</div>
</div>
</div>
);
}
+108
View File
@@ -0,0 +1,108 @@
import { getFetch } from "@/components/ui/api";
import { DEV_URL_FO } from "@/components/ui/config";
import type { FacilityInfo, SubsInfo } from "./type";
const checkWrRegList = async () => {
const wrRegList = await getFetch("wr", `/reuseFc/pubFc`);
return wrRegList;
};
const getRuseFclt = async (
page: string,
sggOrCity: string,
searchTxt?: string
) => {
const checkWrRegListRes = await checkWrRegList();
let url = `/prcsFclt/ruseInfo/${page ? page : 1}/5/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`;
console.log(" checkWrRegListRes ", checkWrRegListRes);
const ruseList = checkWrRegListRes.data.filter((item: string) => item !== "");
if (searchTxt) {
url = url + `/${searchTxt}`;
}
const response = await fetch(DEV_URL_FO + url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(ruseList),
});
const hasuList = await response.json().then((res: any) => {
const data = res.data
.map((item: any) => {
return {
...item,
fctCpc: Number(item.fctCpc) || 0,
searchNm: item.fctNm,
searchPk: item.bscFctNm,
};
})
.filter((hasu) => ruseList.find((ruse) => hasu.bscFctCd === ruse));
return { data: data, count: res.count };
});
return hasuList;
};
export type DmMapActions = {
findRuseFclt(sggOrCity: string, searchTxt: string): void;
setSubsInfo(subsInfo: SubsInfo[]): void;
};
export type DmMapType = {
map: {
ruseFcltList: FacilityInfo[];
subsInfo: SubsInfo[];
};
};
export const dmMapState: DmMapType = {
map: {
ruseFcltList: [],
// subsInfo: [
// {
// ruseUtztnFcltNm: "수원제1,2 재이용1", // 재이용시설명
// ruseUtztnFcltCd: "41110PB001R1",
// },
// {
// ruseUtztnFcltNm: "낙양 재이용1", // 재이용시설명
// ruseUtztnFcltCd: "41150SW001R1",
// },
// {
// ruseUtztnFcltNm: "서남 재이용1", // 재이용시설명
// ruseUtztnFcltCd: "11500PB001R1",
// },
// ],
subsInfo:[]
},
};
export type DmMapStore = DmMapType & DmMapActions;
export const DmMapReducer = (set: any): DmMapActions => {
return {
findRuseFclt: async (sggOrCity: string, searchTxt: string) => {
const ruseFcltList = await getRuseFclt("1", sggOrCity, searchTxt);
set((state: DmMapType) => ({
...state,
map: {
...state.map,
ruseFcltList: ruseFcltList.data,
},
}));
},
setSubsInfo: async (subsInfo: SubsInfo[]) => {
set((state: DmMapType) => ({
...state,
map: {
...state.map,
subsInfo: subsInfo,
},
}));
},
};
};
+39
View File
@@ -0,0 +1,39 @@
export type FacilityInfo= {
bscFctCd: string; // 기본시설 코드
bscFctNm: string; // 기본시설명
rvbsnCd: string; // 유역 코드
rvbsn: string; // 유역명
lgbsn: string; // 대권역명
mdmbsn: string; // 중권역명
rgnCd: string; // 관할청 코드
rgnNm: string; // 관할청 명칭
adstDstrCd: string; // 행정구역 코드
sido: string; // 시도
gugun: string; // 구군
addr: string; // 주소
fctCpc: string; // 시설용량
efflWqltStndrdCd: string; // 방류수 수질기준 코드
efflWqltStndrdNm: string; // 방류수 수질기준명
trtFrfNm: string; // 처리공법명
trtDstr: string; // 처리구역
rnDt: string; // 사용개시일 (예: YYYYMMDD)
cmplDt: string; // 준공일자 (예: YYYYMMDD)
trstOprEtpNm: string; // 위탁운영기관명
bscFctClsNm: string; // 기본시설 구분명
sitArea: string; // 부지면적
trtDstrArea: string; // 처리구역 면적
swrTrtPopl: string; // 하수처리 인구
mnstr: string; // 총괄 유역
efflWtrrgn: string; // 방류수역
rvNm: string; // 하천명
bscFctClsCd: string; // 기본시설 구분 코드
rvbsnNm: string; // 유역명
tel: string; // 전화번호
rswtrYn: string; // 재이용수 여부 ("Y" 또는 "N")
ruse: boolean; // 재이용 여부 (boolean)
}
export type SubsInfo={
ruseUtztnFcltNm: string; // 재이용시설명
ruseUtztnFcltCd: string; // 재이용시설 코드
}
+9 -2
View File
@@ -38,8 +38,17 @@ export type DemandCompType = {
reqDt : string | '';
dmdInfoSn : number | '';
status : string | '';
splyCndtList:SupplyCandidate[]|[],
}
// 공급 후보자 타입 정의
export type SupplyCandidate = {
ruseUtztnFcltNm: string;
ruseUtztnFcltCd: string;
};
export type DemandMatchings = {
splyFcltFk : string | '';
splyFcltFlg : string | '';
@@ -387,11 +396,9 @@ export const SdmState: SdmType = {
similarCaseList: similarCaseList,
upHeadMoneyDetailList : moneyDetailModalHeader,
moneyDetailList: moneyDetailList,
// 수요정보 신청현황
sdmInfoStatusList: [],
upHeadSdmInfoStatus: upHeadSdmInfoStatus,
// 로그인관련
successNextPage : false,
successLogin: false,
+11 -3
View File
@@ -1,19 +1,27 @@
import { createStore } from 'zustand/vanilla';
import { CommonReducer, CommonState, type CommonStore, type CommonType } from './common';
import { SdmReducer, SdmState, type SdmStore, type SdmType } from './sdmMng';
import { dmMapState, type DmMapType, type DmMapStore, DmMapReducer } from './map';
import { tokenManager } from '@/src/entities/auth/lib/token-manager';
export type DmStore = CommonStore & SdmStore;
export type DmState = CommonType & SdmType;
export type DmStore = CommonStore & SdmStore &DmMapStore ;
export type DmState = CommonType & SdmType &DmMapType;
export const StoreZus = (initState: DmState= {
...CommonState,
...SdmState,
...dmMapState,
}) => {
return createStore<DmStore>()((set) => {
tokenManager.setToken("site-id-key", 'DM');
tokenManager.setToken("site-theme-key", 'green');
return {
...initState,
...CommonReducer(set),
...SdmReducer(set)
...SdmReducer(set),
...DmMapReducer(set)
}
})
}
@@ -0,0 +1,108 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
type SearchType = {
searchNm: string;
searchPk: string;
};
type thisProps = {
label: string | undefined;
initValue: string | undefined;
setValue: (searchNm?: string) => void;
setRtnData: (rtnData: any) => void;
searchList?: any[];
};
export default function SearchInputText({
label = undefined,
initValue = undefined,
setValue = () => {},
setRtnData = () => {},
searchList = undefined,
}: thisProps) {
const [isOpen, setIsOpen] = useState(false);
const [textValue, setTextValue] = useState(initValue || "");
const wrapperRef = useRef<HTMLDivElement>(null);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setTextValue(value);
setValue(value);
if (value && value!=='') {
setIsOpen(true);
} else {
setIsOpen(false);
}
},
[setValue]
);
const handleSuggestionClick = useCallback(
(searchNm: string) => {
// Do not update textValue to keep input unchanged on suggestion click
setValue(searchNm);
setIsOpen(false);
setRtnData(searchList?.find((item) => item.searchNm === searchNm) || {});
},
[searchList, setRtnData, setValue]
);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
wrapperRef.current &&
!wrapperRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div ref={wrapperRef} className="relative">
<input
type="text"
value={textValue ||''}
onChange={handleInputChange}
/* onFocus={() => setIsOpen(true)} */
className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`${label}을(를) 입력하세요`}
/>
{isOpen && (
<div className="absolute top-full left-0 z-50 mb-2 max-h-72 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
<div className="max-h-72">
{searchList && searchList.length > 0 ? (
searchList.map((item: SearchType, index: number) => (
<div
key={index}
className="w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl hover:bg-gray-100"
onClick={() => handleSuggestionClick(item.searchNm)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleSuggestionClick(item.searchNm);
}
}}
>
{item.searchPk?.substring(0, 10)}&nbsp;
<span className="float-right">{item.searchNm}</span>
</div>
))
) : (
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
검색 결과가 없습니다.
</div>
)}
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,145 @@
"use client";
import { useDmStore } from "@/app/store";
import {
SearchDiv,
SearchFormLeft,
SearchFormRight,
SelectBox,
type SelectDataType,
} from "@common/business_components/ui";
import { Button, Label } from "@common/components/ui";
import { useCallback, useEffect, useState } from "react";
import SearchInputText from "../SearchInputText/SearchInputText";
export function SearchBar({
setHasuPk,
onApply
}: {
setHasuPk: (hasuPk: string) => void;
onApply: () => void;
}) {
const { map, Common, initializeCommonData, findRuseFclt } = useDmStore(
(state) => state
);
const [sidoValue, setSidoValue] = useState<string>("");
const [sigunValue, setSigunValue] = useState<string>("");
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([
{ text: "전체", val: "0" },
]);
useEffect(() => {
initializeCommonData();
}, [initializeCommonData]);
const initializeSigunData = useCallback(
(sidoCd: string) => {
if (sidoCd === "0") {
setSigunValue("0");
setFilteredSigunData([{ text: "전체", val: "0" }]);
} else {
const newFilteredData = [
{ text: "전체", val: "0" },
...Common.selectSigunData.filter(
(item: SelectDataType) => item.val.substring(0, 2) === sidoCd
),
];
setSigunValue("0");
setFilteredSigunData(newFilteredData);
}
},
[Common.selectSigunData]
);
const onSelectValue = useCallback(
(val: string, type: string) => {
switch (type) {
case "ctpvCd":
initializeSigunData(val);
setSidoValue(val);
break;
case "sggCd":
setSigunValue(val);
}
},
[initializeSigunData]
);
useEffect(() => {
console.log("map.ruseFcltList ", map.ruseFcltList);
}, [map.ruseFcltList]);
return (
<>
<div className="grid grid-cols-4 gap-4 ">
<div className="col-span-4">
<SearchDiv>
<SearchFormLeft>
<div className="col-span-10 flex flex-row">
<SelectBox
className="col-span-2 w-1/6 h-full text-2xl"
label="시도"
selectClass={""}
selectArray={Common.selectSidoData || []}
onSelectValue={(val) =>
onSelectValue(val.toString(), "ctpvCd")
}
value={sidoValue || Common.selectSidoData[0]?.val || ""}
/>
<SelectBox
className="col-span-2 w-1/6 h-full text-2xl ml-10"
label="시군구"
selectClass={""}
selectArray={filteredSigunData || []}
onSelectValue={(val) =>
onSelectValue(val.toString(), "sggCd")
}
value={sigunValue}
/>
<div className="col-span-6 flex flex-row items-center min-w-[27rem] ml-10">
<Label className="text-xl justify-self-center mt-2 mx-3">
하수재이용시설
</Label>
<SearchInputText
label={"시설명"}
initValue={undefined}
setValue={(searchNm?: string) => {
let sggOrCity = "";
if (searchNm) {
if (sigunValue === "0" || sigunValue === "") {
sggOrCity = sidoValue;
} else {
sggOrCity = sigunValue;
}
findRuseFclt(sggOrCity, searchNm);
}
}}
setRtnData={(rtnData: any) => {
setHasuPk(rtnData.bscFctCd || "");
}}
searchList={map.ruseFcltList || []}
/>
</div>
<div className="flex flex-row space-x-2">
<Button
color="red"
className="mx-2 mt-2"
size={"sm"}
onClick={() => {}}
>
후보지선택
</Button>
<Button
color="sky"
className="mt-2"
size={"sm"}
onClick={() => {onApply();}}
>
적용
</Button>
</div>
</div>
</SearchFormLeft>
</SearchDiv>
</div>
</div>
</>
);
}
@@ -1,7 +1,15 @@
import ModalSimilarPage from "@/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page";
import type { FormData } from "@/app/pages/sdmMng/sdmInfoInsert/page";
import ModalSimilarPage from "@/app/(base-layout)/pages/sdmMng/sdmInfoInsert/modalSimilar/page";
import type { FormData } from "@/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page";
import { useDmStore } from "@/app/store";
import { CANDIDATE, DatePicker, FormField, FormLabel, FormUi, SelectBox, UiModal } from "@common/business_components";
import {
CANDIDATE,
DatePicker,
FormField,
FormLabel,
FormUi,
SelectBox,
UiModal,
} from "@common/business_components";
import { Input } from "@common/components";
import { Button } from "@common/components/ui";
import { useSearchParams } from "next/navigation";
@@ -10,6 +18,8 @@ import { AccessibleFormField } from "./AccessibleFormField";
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
import SearchInputText from "../common/searhDemandInput/component";
import type { DemandCompType } from "@/app/store/sdmMng";
import NewWindowPortal from "@/app/(popup-layout)/map/popup/windowPotal";
import Map from "@/app/(popup-layout)/map/popup/sdmInfoMap";
export interface SdmInfoRegFormProps {
form: UseFormReturn<FormData>;
@@ -17,31 +27,46 @@ export interface SdmInfoRegFormProps {
facilityName?: string;
}
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
type FieldComponent = (
field: ControllerRenderProps<FormData, keyof FormData>
) => JSX.Element;
export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfoRegFormProps) {
export function SdmInfoRegForm({
form,
onCheckboxChange,
facilityName,
}: SdmInfoRegFormProps) {
const { initializeCommonData, Sdm } = useDmStore((state) => state);
const { useTypesData } = Sdm;
const searchParams = useSearchParams();
const [sdmNmValue, setSdmNmValue] = useState<string>(searchParams.get('dmdCoNm') || '');
const [sdmNmValue, setSdmNmValue] = useState<string>(
searchParams.get("dmdCoNm") || ""
);
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
const [isSimilarCase, setIsSimilarCase] = useState(false);
const [key, setKey] = useState(0);
// 후보지 팝업 오픈 상태
const [isCandidatePopup, setIsCandidatePopup] = useState(false);
//모달 조회에 필요한 데이터
const [sply, setSply] = useState<number>(0);
const [usageCd, setUsageCd] = useState<string>('청소');
const [usageCd, setUsageCd] = useState<string>("청소");
const [money, setMoney] = useState<number>(0);
useEffect(() => {
initializeCommonData();
}, [initializeCommonData]);
useEffect(() =>{
console.log('candidateTitle ', form.getValues("candidateTitle"))
},[form.getValues("candidateTitle") ])
useEffect(() => {
if (useTypesData && useTypesData.length > 0) {
setTimeout(() => {
form.setValue('usePurpose', useTypesData[0]?.val?.toString() || '');
form.setValue("usePurpose", useTypesData[0]?.val?.toString() || "");
}, 500);
}
}, [useTypesData, form]);
@@ -49,32 +74,51 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
useEffect(() => {
if (facilityName) {
setSdmNmValue(facilityName);
form.setValue('sdmNm', facilityName);
setKey(prev => prev + 1);
form.setValue("sdmNm", facilityName);
setKey((prev) => prev + 1);
}
}, [facilityName, form]);
useEffect(() => {
if (searchParams.get("dmdCoNm")) {
form.setValue("sdmNm", searchParams.get("dmdCoNm") || "");
setSdmNmValue(searchParams.get("dmdCoNm") || "");
setKey((prev) => prev + 1);
}
}, [searchParams, form]);
const handleCandidateClose = useCallback(
() => setIsCandidatePopup(false),
[]
);
const searchCandidate = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
}
//setIsCandidatePopup(true);
const popupOptions = `width=1920,height=1080,resizable='y',scrollbars='y'`;
window.open("/map/popup", "candidatePopup", popupOptions);
};
const searchSimilarCase = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setIsSimilarCase(true);
}
};
const handleCheckboxChange = (checked: boolean) => {
setIsBznmTotNoDisabled(checked);
onCheckboxChange?.(checked);
};
// 시설 선택시 이벤트
const selectComp = useCallback((rtnData: DemandCompType[]) => {
const rtnCompData = rtnData?.[0];
form.setValue('sdmNm', rtnCompData?.dmdCoNm || '');
}, [form]);
const selectComp = useCallback(
(rtnData: DemandCompType[]) => {
const rtnCompData = rtnData?.[0];
form.setValue("sdmNm", rtnCompData?.dmdCoNm || "");
},
[form]
);
const formFields: Array<{
name: keyof FormData;
@@ -93,15 +137,15 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
<SearchInputText
label="수요처명"
setValue={(val) => {
setSdmNmValue(val || '');
field.onChange(val || '');
setSdmNmValue(val || "");
field.onChange(val || "");
}}
setRtnData={selectComp}
initValue={facilityName || sdmNmValue}
{...field}
/>
</div>
)
),
},
{
name: "demandYmd",
@@ -110,7 +154,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
required: true,
component: (field) => (
<div className="w-full">
<DatePicker
<DatePicker
mode="single"
selected={field.value as Date}
onSelect={(date) => {
@@ -122,12 +166,12 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
value: field.value as Date,
onChange: (date: Date | undefined) => {
field.onChange(date as Date);
}
},
}}
selectClass={undefined}
/>
</div>
)
),
},
{
name: "demandQnty",
@@ -135,8 +179,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 0,
required: true,
component: (field) => (
<Input
placeholder="요구수량(㎥/d)을 입력하세요"
<Input
placeholder="요구수량(㎥/d)을 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -148,7 +192,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "usePurpose",
@@ -158,16 +202,16 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
component: (field) => (
<SelectBox
selectClass={undefined}
label=""
selectArray={useTypesData}
label=""
selectArray={useTypesData}
onSelectValue={(val) => {
field.onChange(val.toString());
setUsageCd(val.toString());
}}
className=""
className=""
value={field.value as string}
/>
)
),
},
{
name: "money",
@@ -201,14 +245,14 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
</FormUi>
}
/>
)
),
},
{
name: "reqWaterQlty",
label: "요구수질",
formIndex: 0,
required: true,
component: undefined
component: undefined,
},
{
name: "grssClfg",
@@ -216,8 +260,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="총대장균군수를 입력하세요"
<Input
placeholder="총대장균군수를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -226,7 +270,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "tn",
@@ -234,8 +278,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="총질소를 입력하세요"
<Input
placeholder="총질소를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -244,7 +288,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "combResdch",
@@ -252,8 +296,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="결합잔류염소를 입력하세요"
<Input
placeholder="결합잔류염소를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -262,7 +306,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "tph",
@@ -270,8 +314,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="총인을 입력하세요"
<Input
placeholder="총인을 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -280,16 +324,16 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "trbt",
label: "탁도(NTU)",
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="탁도를 입력하세요"
component: (field) => (
<Input
placeholder="탁도를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -298,7 +342,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "ph",
@@ -306,8 +350,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="수소이온농도를 입력하세요"
<Input
placeholder="수소이온농도를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -316,7 +360,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "bod",
@@ -324,8 +368,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="생물학적 산소요구량을 입력하세요"
<Input
placeholder="생물학적 산소요구량을 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -334,7 +378,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "clIonQnt",
@@ -342,8 +386,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="염화물을 입력하세요"
<Input
placeholder="염화물을 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -352,16 +396,16 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
},
),
},
{
name: "smll",
label: "냄새",
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="냄새를 입력하세요"
<Input
placeholder="냄새를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -370,7 +414,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "cdtnv",
@@ -378,8 +422,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="전기전도도를 입력하세요"
<Input
placeholder="전기전도도를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -388,7 +432,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "crmty",
@@ -396,8 +440,8 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="색도를 입력하세요"
<Input
placeholder="색도를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -406,16 +450,15 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "rmrk",
label: "비고",
formIndex: 1,
required: true,
component: (field) => (
<Input
placeholder="비고를 입력하세요"
<Input
placeholder="비고를 입력하세요"
type="text"
className="!text-xl text-gray-600"
value={field.value as string}
@@ -424,73 +467,61 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
name={field.name}
ref={field.ref}
/>
)
),
},
{
name: "candidateTitle",
label: CANDIDATE || "공급처 후보지 선택(필수아님)",
formIndex: 2,
component: (field) => (
component: () => (
<div>
<div className="-mt-14 float-right flex gap-10">
<Button className={'bg-gray-200'} onClick={searchCandidate}>
<Button className={"bg-gray-200"} onClick={searchCandidate}>
후보지 조회
</Button>
<Button className={'bg-gray-200'} onClick={searchSimilarCase}>
<Button className={"bg-gray-200"} onClick={searchSimilarCase}>
유사사례
</Button>
</div>
<div className="flex justify-center gap-10 mt-5">
<Input
placeholder="후보지1"
type="text"
className="!text-xl w-[130px] text-gray-600"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
<Input
placeholder="후보지2"
type="text"
className="!text-xl w-[130px] text-gray-600"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
<Input
placeholder="후보지3"
type="text"
className="!text-xl w-[130px] text-gray-600"
value={field.value as string}
onChange={field.onChange}
onBlur={field.onBlur}
name={field.name}
ref={field.ref}
/>
{(form.getValues("candidateTitle") || []).map((candidate: any, index: number) => (
<Input
key={index}
placeholder={`후보지${index + 1}`}
type="text"
className="!text-xl w-[130px] text-gray-600"
value={candidate.ruseUtztnFcltNm}
readOnly
/>
))}
</div>
</div>
)
),
},
];
const normalFields = formFields.filter(f => f.formIndex === 0 && f.component);
const groupFields = formFields.filter(f => f.formIndex === 1 && f.component);
const candidateFields = formFields.filter(f => f.formIndex === 2 && f.component);
const normalFields = formFields.filter(
(f) => f.formIndex === 0 && f.component
);
const groupFields = formFields.filter(
(f) => f.formIndex === 1 && f.component
);
const candidateFields = formFields.filter(
(f) => f.formIndex === 2 && f.component
);
return (
<>
{normalFields.map(({ name, label, component, required }) => (
{normalFields.map(({ name, label, component, required }) =>
component ? (
<FormField
key={name}
control={form.control}
name={name}
render={({ field }) => (
name === 'money' ? component(field) : (
render={({ field }) =>
name === "money" ? (
component(field)
) : (
// <FormUi label={label}>
// {component(field)}
// </FormUi>
@@ -502,10 +533,10 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
inputComponent={component(field)}
/>
)
)}
}
/>
) : null
))}
)}
{groupFields.length > 0 && (
<div className="mt-10">
@@ -535,7 +566,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
</div>
)}
{candidateFields.map(({ name, label, component, required }, index) => (
{candidateFields.map(({ name, label, component, required }, index) =>
component ? (
<FormField
key={name + index}
@@ -555,11 +586,13 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
)}
/>
) : null
))}
)}
{isSimilarCase && (
<UiModal
body={<ModalSimilarPage sply={sply} usageCd={usageCd} money={money} />}
body={
<ModalSimilarPage sply={sply} usageCd={usageCd} money={money} />
}
bodyClass="sm:max-w-[100vw] w-[1400px] h-[900px]"
open={isSimilarCase}
onOpenChange={(open) => !open && setIsSimilarCase(false)}
@@ -567,6 +600,7 @@ export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfo
noFooterYn="Y"
/>
)}
</>
);
}
}
@@ -4,14 +4,33 @@ import { BZNM_TOT_NO, FormField, FormUi } from "@common/business_components";
import { Button, Input } from "@common/components";
import { zodResolver } from "@hookform/resolvers/zod";
import { sha512 } from 'js-sha512';
import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
import { FormProvider, useForm, type ControllerRenderProps } from "react-hook-form";
import * as z from "zod";
export function SdmLoginForm() {
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
const createFormSchema = () => z.object({
const { actionLogin, initializeCommonData } = useDmStore((state) => state);
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true);
const formatBusinessNumber = (value: string) => {
// 숫자만 추출
const digitsOnly = value.replace(/\D/g, "");
// 10자리까지만 처리
const limitedDigits = digitsOnly.slice(0, 10);
// 3-2-5 형식으로 포맷팅
if (limitedDigits.length <= 3) {
return limitedDigits;
} else if (limitedDigits.length <= 5) {
return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3)}`;
} else {
return `${limitedDigits.slice(0, 3)}-${limitedDigits.slice(3, 5)}-${limitedDigits.slice(5)}`;
}
};
// 스키마를 useMemo로 최적화
const sdmLoginFormSchema = useMemo(() => z.object({
dmdInfoId: z.string()
.min(1, "사업자등록번호를 입력해주세요")
.regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다"),
@@ -24,11 +43,12 @@ export function SdmLoginForm() {
.max(16, "비밀번호는 16자 이내로 입력해주세요"),
rgtr: z.string().optional(),
dmdAplyPswd: z.string().optional(),
});
}), [isBznmTotNoDisabled]);
const { actionLogin, initializeCommonData } = useDmStore((state) => state);
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
const formSchema = createFormSchema();
type FormData = z.infer<typeof sdmLoginFormSchema>;
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
const formSchema = sdmLoginFormSchema;
useEffect(() => {
initializeCommonData();
@@ -76,7 +96,12 @@ export function SdmLoginForm() {
placeholder="사업자등록번호를 입력하세요"
type="text"
className="!mt-5 !mb-5 !text-xl text-gray-600"
{...field}
{...field}
value={formatBusinessNumber(field.value as string)}
onChange={(e) => {
const dmdId = e.target.value.replace(/\D/g, "").slice(0, 10);
field.onChange(dmdId);
}}
/>
)
},
+6 -2
View File
@@ -27,13 +27,17 @@ export function middleware(req: NextRequest) {
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
.join("&");
console.log(' sesesesesesevereveraserwereqwrewbrtkehjwrtg server')
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
decodedParams ? `?${decodedParams}` : ""
}`;
return NextResponse.rewrite(new URL(newUrl));
}
if (pathname === "/") {
console.log(" maim 화면이동 ");
return NextResponse.redirect(new URL("/pages/wrStatus", req.url));
}
if (pathname.startsWith("/attach-service")) {
const requestHeaders = new Headers(req.headers);
@@ -124,5 +128,5 @@ export function middleware(req: NextRequest) {
}
export const config = {
matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*"],
matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*", "/"],
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Some files were not shown because too many files have changed in this diff Show More