diff --git a/apps/dm/.env.local b/apps/dm/.env.local index 622545fd6..f691fb313 100644 --- a/apps/dm/.env.local +++ b/apps/dm/.env.local @@ -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 # 박이정 diff --git a/apps/dm/.env.production b/apps/dm/.env.production index 8a552821e..7576a7dfb 100644 --- a/apps/dm/.env.production +++ b/apps/dm/.env.production @@ -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 diff --git a/apps/dm/app/(base-layout)/layout.tsx b/apps/dm/app/(base-layout)/layout.tsx new file mode 100644 index 000000000..5caa088b0 --- /dev/null +++ b/apps/dm/app/(base-layout)/layout.tsx @@ -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 ( + + {children} + + ); +} diff --git a/apps/dm/app/pages/sdmMng/sdmApplyList/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmApplyList/page.tsx similarity index 97% rename from apps/dm/app/pages/sdmMng/sdmApplyList/page.tsx rename to apps/dm/app/(base-layout)/pages/sdmMng/sdmApplyList/page.tsx index 2dda06008..212eca6e7 100644 --- a/apps/dm/app/pages/sdmMng/sdmApplyList/page.tsx +++ b/apps/dm/app/(base-layout)/pages/sdmMng/sdmApplyList/page.tsx @@ -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={} 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" /> diff --git a/apps/dm/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx similarity index 100% rename from apps/dm/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx rename to apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/modalSimilar/page.tsx diff --git a/apps/dm/app/pages/sdmMng/sdmInfoInsert/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx similarity index 83% rename from apps/dm/app/pages/sdmMng/sdmInfoInsert/page.tsx rename to apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx index a0f905c35..40ff3948f 100644 --- a/apps/dm/app/pages/sdmMng/sdmInfoInsert/page.tsx +++ b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInfoInsert/page.tsx @@ -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>; +export type FormData = z.infer; 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(''); - const [defaultValues, setDefaultValues] = useState({ + const [defaultValues, setDefaultValues] = useState({ 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 = 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 ( -
+

@@ -207,14 +228,14 @@ function SdmInfoInsertContent() {
-
-
- {!isLogin? - } + {!isLogin ? + } 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}

); diff --git a/apps/dm/app/pages/sdmMng/sdmInsert/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInsert/page.tsx similarity index 77% rename from apps/dm/app/pages/sdmMng/sdmInsert/page.tsx rename to apps/dm/app/(base-layout)/pages/sdmMng/sdmInsert/page.tsx index 040304347..926a8f902 100644 --- a/apps/dm/app/pages/sdmMng/sdmInsert/page.tsx +++ b/apps/dm/app/(base-layout)/pages/sdmMng/sdmInsert/page.tsx @@ -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 ( -
- - +
+
+ + +
); } diff --git a/apps/dm/app/pages/sdmMng/sdmLogin/page.tsx b/apps/dm/app/(base-layout)/pages/sdmMng/sdmLogin/page.tsx similarity index 100% rename from apps/dm/app/pages/sdmMng/sdmLogin/page.tsx rename to apps/dm/app/(base-layout)/pages/sdmMng/sdmLogin/page.tsx diff --git a/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx b/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx new file mode 100644 index 000000000..8d5bdd0ba --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/wrAbout/page.tsx @@ -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 ( +
+
+ {/* 사이드바 */} + + +
{ cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title"> +

+ + 물재이용이란 +

+
+

물 재이용이 무엇인가요?

+

+ 물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를
+ 물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,
+ 하천 유지 등의 용도로 이용하는 것을 말합니다. +

+ 이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다. +

+ 환경부는 2011년에 '물의 재이용 촉진 및 지원에 관한 법률'을 제정하여
+ 한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다. +

+
+
+ + {/* 카드2 */} +
{ cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title"> +

+ + 물재이용 필요성 +

+
+

물 재이용, 왜 필요한가요?

+

+ 빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.
+ 물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서
가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다. +

+ 이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다. +

+ - 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.
+ - 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다. +

+ 세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다. +

+ - 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.
+ - 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다. +

+ 물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다. +
+
+ + {/* 카드3 */} +
{ cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title"> +

+ + 빗물 재이용 +

+
+

친환경 수자원 빗물,
편리하게 사용할 수 있어요!

+

가뭄을 극복하고 홍수를 예방하기 위해
비가 내릴 때 빗물을 모아두었다가 사용합니다.

+ 주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다. + 빗물이용 +

+ - 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등
+ - 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)
+ - 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등 +

+
+
+ + {/* 카드4 */} +
{ cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title"> +

+ + 중수 재이용 +

+
+

요모조모 쓸모 많은 중수,
알뜰하게 재이용할 수 있어요!

+

한번 사용한 물을 중수도 시설을 통해 재생하여
개별적 또는 지역적으로 재이용할 수 있습니다.

+ 욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다. + 중수재이용 +

+ - 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등
+ - 의무 재이용량: 물 사용량의 10% 이상
+ - 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등 +

+
+
+ + {/* 카드5 */} +
{ cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title"> +

+ + 하수 재이용 +

+
+

아직 할 일이 많은 하수처리수,
풍족하게 재이용할 수 있어요!

+

사용하고 버려져 공공하수처리장으로 모여든 물은
용도에 맞는 처리 후 재이용 할 수 있습니다.

+ 주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다. + 하수처리 재이용 +

+ - 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)
+ - 의무 재이용량: 일 하수처리량의 10% 이상
+ - 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등 +

+
+
+ + {/* 카드6 */} +
{ cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title"> +

+ + 물재이용 목표 +

+
+

2030년 물 재이용 목표를 위해
모두의 관심과 노력이 필요합니다.

+

+ 환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여
한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다. +

+ 지속가능한 물 재이용 정착으로 건전한 물순환 확산 +
+
+ +

도시 물 순환이용
체계로의 전환

+
    +
  • - 지속가능한 물 순환이용체계 구축
  • +
  • - 물 재이용시설 관리체계 강화
  • +
+
+
+ +

유역기반 하수처리수 재이용
수요확대 및 공급체계 구축

+
    +
  • - 유역기반 통합물관리 연계 강화
  • +
  • - 하수처리수 재이용 공업용수 공급 활성화
  • +
  • - 하수처리수 재이용을 활용한 물순환 촉진
  • +
+
+
+ +

물 재이용 인식제고
및 산업발전 기반 마련

+
    +
  • - 물 재이용 교육 및 대국민 홍보 강화
  • +
  • - 핵심기술 육성 및 산업발전 지원 강화
  • +
+
+
+ 물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다. +
+
+
+
+ ); +} diff --git a/apps/dm/app/pages/wrExample/page.tsx b/apps/dm/app/(base-layout)/pages/wrExample/page.tsx similarity index 64% rename from apps/dm/app/pages/wrExample/page.tsx rename to apps/dm/app/(base-layout)/pages/wrExample/page.tsx index 066e0abe2..4849c02b0 100644 --- a/apps/dm/app/pages/wrExample/page.tsx +++ b/apps/dm/app/(base-layout)/pages/wrExample/page.tsx @@ -2,49 +2,49 @@ export default function WrExamplePage() { return ( -
+
-

+

물 재이용사례

-
+
{/* 카드 1 */}
-

+

사업사례 1

{/* 사업명 */}
-

사업명

-

+

사업명

+

경기권역 반도체 사업장 물 재이용 사업
: 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등)

{/* 사업 배경 및 목적 */}
-

사업 배경 및 목적

-

+

사업 배경 및 목적

+

삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정

{/* 사업 주요 내용 */}
-

사업 주요 내용

-

+

사업 주요 내용

+

1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]
2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)]

{/* 특이사항 */}
-

특이사항

-

+

특이사항

+

본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행

@@ -63,15 +63,15 @@ export default function WrExamplePage() { {/* 카드2 */}
-

+

사업사례 2

{/* 사업명 */}
-

사업명

-

+

사업명

+

경기도 파주시 하수재이용
: 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급

*LG디스플레이(P10공장), 희성전자, 엔비텍 등 @@ -79,15 +79,15 @@ export default function WrExamplePage() {

{/* 사업 방식 */}
-

사업 방식

-

+

사업 방식

+

민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)

{/* 사업 주요 내용 */}
-

사업 주요 내용

-

+

사업 주요 내용

+

처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)
공급량 : 4만m³/일
관로 : 13.6km
@@ -96,8 +96,8 @@ export default function WrExamplePage() {

{/* 특이사항 */}
-

특이사항

-

+

특이사항

+

하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례

@@ -124,29 +124,29 @@ export default function WrExamplePage() { - 하수처리수 공급 + 하수처리수 공급 - 위치 - 금촌하수 - 운정하수 + 위치 + 금촌하수 + 운정하수 - 시설용량 - 44,000㎥/일 - 60,000㎥/일 + 시설용량 + 44,000㎥/일 + 60,000㎥/일 - 유입량 - 27,000㎥/일
자연유하 - 30,000㎥/일
이송펌프장 + 유입량 + 27,000㎥/일
자연유하 + 30,000㎥/일
이송펌프장 {/* 첫 화살표 (자연유하 / 관로이송) */}
- 자연유하: 161m + 자연유하: 161m 오른쪽 화살표 - 관로이송: 3.2km + 관로이송: 3.2km
@@ -177,26 +177,26 @@ export default function WrExamplePage() { - 물재이용 처리시설 + 물재이용 처리시설 - 유입유량 - 57,000㎥/일 + 유입유량 + 57,000㎥/일 - 재이용시설 - 40,000㎥/일 + 재이용시설 + 40,000㎥/일 - 농축폐수처리 - 17,000㎥/일 + 농축폐수처리 + 17,000㎥/일 {/* 두 번째 화살표 (공급관로) */}
- 공급관로 L=10.4km + 공급관로 L=10.4km 아래 화살표 - 수요처 공급 + 수요처 공급 - 수요처 - LG디스플레이 외 2개사 + 수요처 + LG디스플레이 외 2개사 - 수요량 - 40,000㎥/일 + 수요량 + 40,000㎥/일 @@ -238,30 +238,30 @@ export default function WrExamplePage() { {/* 카드3 */}
-

+

사업사례 3

{/* 사업명 */}
-

사업명

-

+

사업명

+

충남 보령 하수재이용
: 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진

{/* 사업 방식 */}
-

사업 방식

-

+

사업 방식

+

2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결

{/* 사업 주요 내용 */}
-

사업 주요 내용

-

+

사업 주요 내용

+

처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)
공급량 : 1만m3/일
관로 : 8.7km
@@ -270,8 +270,8 @@ export default function WrExamplePage() {

{/* 특이사항 */}
-

특이사항

-

+

특이사항

+

국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU (환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕 으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모

@@ -290,22 +290,22 @@ export default function WrExamplePage() { {/* 카드4 */}
-

+

사업사례 4

{/* 사업명 */}
-

사업명

-

+

사업명

+

경기도 수원시 빗물이용과 중수도

{/* 사업 내용 */}
-

사업 내용

-

+

사업 내용

+

(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감
  ※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(21.7월, 수원시청).
  다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란

@@ -320,7 +320,7 @@ export default function WrExamplePage() { height={250} className="w-[100%] mx-auto" /> - <월드컵경기장 인근도로 노면 살수> + <월드컵경기장 인근도로 노면 살수>

- <주유기 모양의 빗물 공급기> + <주유기 모양의 빗물 공급기>
- <빗물주유기에서 살수용수 공급> + <빗물주유기에서 살수용수 공급>
- <빗물 공급기 및 빗물분사장치 위치> + <빗물 공급기 및 빗물분사장치 위치>
diff --git a/apps/dm/app/(base-layout)/pages/wrOverview/page.tsx b/apps/dm/app/(base-layout)/pages/wrOverview/page.tsx new file mode 100644 index 000000000..3e5825899 --- /dev/null +++ b/apps/dm/app/(base-layout)/pages/wrOverview/page.tsx @@ -0,0 +1,113 @@ +import Image from "next/image"; + +export default function WrOverviewPage() { + return ( +
+
+ {/* 카드 1 - 물재이용 사업 추진 절차 */} +
+

+ + 물재이용 사업 추진 절차 +

+
+ + + + + + + + + + + {/* 1단계 */} + + + {/* 2단계 */} + + + {/* 3단계 */} + + + {/* 4단계 */} + + + {/* 5단계 */} + + + {/* 6단계 */} + + + {/* 7단계 */} + + + {/* 8단계 */} + + + {/* 9단계 */} + + +
물 재이용 추진 절차 상세 표
타당성 조사
↓
매칭시스템 신청사업계획(안) 수립
↓
하수도정보시스템 신청국고지원 신청→기술검토(한국환경공단)
↓
국고지원 확정 통보
↓
사업계획 수립(실시설계)
↓
(행정관) 기술검토 신청재원협의(지방환경관서장)→기술검토(한국환경공단)
↓
(행정관) 기술검토 신청설치승인(시도지사)→기술검토(한국환경공단)
↓
공사시행
↓
(행정관) 기술검토 신청준공(국고정산)→기술검토(한국환경공단)
+
+
+ + {/* 카드 2 - 물재이용 매칭 시스템 */} +
+

+ + 물재이용 매칭 시스템이란? +

+
+ {/* 개요 */} +
+

개요

+

+ 기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환 +

+
+ + {/* 구성 */} +
+

구성

+
    +
  • 공급자: 하수처리장, 빗물이용시설, 중수도 등
  • +
  • 수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등
  • +
  • 시스템: 수질, 수량, 거리 기반 매칭, 중개
  • +
  • 운영주체: 한국환경공단
  • +
+
+ + {/* 절차 */} +
+

사업 주요 내용

+

+ ① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행 +

+
+ + {/* 기대효과 */} +
+

기대효과

+
    +
  • 물 자원 수요-공급 조정을 통한 효율적 분배
  • +
  • 물 재이용시설 데이터 수집으로 체계적 관리
  • +
  • 지자체, 사업시행자 등 재이용 정보 획득 용이
  • +
+
+ 하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함. +

(하수 재이용 시스템 개요도)

+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/dm/app/pages/wrStatus/page.tsx b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx similarity index 86% rename from apps/dm/app/pages/wrStatus/page.tsx rename to apps/dm/app/(base-layout)/pages/wrStatus/page.tsx index dd7ec5552..7ae840beb 100644 --- a/apps/dm/app/pages/wrStatus/page.tsx +++ b/apps/dm/app/(base-layout)/pages/wrStatus/page.tsx @@ -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 ( -
+
{/* 우측 콘텐츠 영역 */}
{/* 상단 제목 */} @@ -410,8 +410,8 @@ const WrStatusPage: React.FC = () => {
{/* 타이틀 영역 */}
-

재이용시설개소수(2023)

-

행정구역별 재이용시설 개소수 조회

+

재이용시설현황(2023)

+

행정구역별 재이용시설현황(개소)

{/* 본문 콘텐츠 */} @@ -455,19 +455,19 @@ const WrStatusPage: React.FC = () => {
- 하수재이용시설 (총 717건) + 하수재이용시설 (총 717개소)
- 빗물이용시설 (총 3,730건) + 빗물이용시설 (총 3,730개소)
- 중수도시설 (총 1,074건) + 중수도시설 (총 1,074개소)
- 온배수시설 (총 201건) + 온배수시설 (총 201개소)
@@ -482,7 +482,7 @@ const WrStatusPage: React.FC = () => {

재이용시설현황(2023)

-

행정구역별 재이용시설 현황 비교

+

행정구역별 재이용시설현황(개소)

@@ -493,10 +493,10 @@ const WrStatusPage: React.FC = () => { 번호 시도 - 하수 - 빗물 - 중수도 - 온배수 + 하수(이용량,천m³/년) + 빗물(이용량,천m³/년) + 중수도(이용량,천m³/년) + 온배수(이용량,천m³/년) @@ -522,7 +522,7 @@ const WrStatusPage: React.FC = () => {

재이용통계(2023)

-

항목별 통계 보기

+

행정구역별 재이용시설 현황 개소수 그래프

diff --git a/apps/dm/app/(popup-layout)/layout.tsx b/apps/dm/app/(popup-layout)/layout.tsx new file mode 100644 index 000000000..742519a66 --- /dev/null +++ b/apps/dm/app/(popup-layout)/layout.tsx @@ -0,0 +1,11 @@ +"use client"; +import "../globals.css"; +import { StoreProvider } from "../store"; + +export default function PopupLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return {children}; +} diff --git a/apps/dm/app/(popup-layout)/map/popup/page.tsx b/apps/dm/app/(popup-layout)/map/popup/page.tsx new file mode 100644 index 000000000..908f2f09c --- /dev/null +++ b/apps/dm/app/(popup-layout)/map/popup/page.tsx @@ -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(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [checkedLayers, setCheckedLayers] = useState([]); + const [checkedLayersData, setCheckedLayersData] = useState([]); + const [selectDataPk, setSelectDataPk] = useState(""); + const [subs, setSubs] = useState([]); + + //적용 버튼 클릭 이벤트 + 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 ( + <> + { + if (hasuPk !== "") { + setSelectDataPk(hasuPk); + } + }} + onApply={onApply} + /> +
+ +
+ + ); +}; + +export default Map; diff --git a/apps/dm/app/(popup-layout)/map/popup/sdmInfoMap.tsx b/apps/dm/app/(popup-layout)/map/popup/sdmInfoMap.tsx new file mode 100644 index 000000000..84dc49119 --- /dev/null +++ b/apps/dm/app/(popup-layout)/map/popup/sdmInfoMap.tsx @@ -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(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const [checkedLayers, setCheckedLayers] = useState([]); + const [checkedLayersData, setCheckedLayersData] = useState([]); + const [selectDataPk, setSelectDataPk] = useState(""); + + 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 ( + <> + { + if (hasuPk !== "") { + setSelectDataPk(hasuPk); + } + }} + /> +
+ +
+ + ); +}; + +export default Map; diff --git a/apps/dm/app/(popup-layout)/map/popup/windowPotal.tsx b/apps/dm/app/(popup-layout)/map/popup/windowPotal.tsx new file mode 100644 index 000000000..38b8fc7db --- /dev/null +++ b/apps/dm/app/(popup-layout)/map/popup/windowPotal.tsx @@ -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) { //