diff --git a/apps/dm/.env.local b/apps/dm/.env.local index 592656b61..7e0ec15fc 100644 --- a/apps/dm/.env.local +++ b/apps/dm/.env.local @@ -20,6 +20,7 @@ NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&ver NEXT_PUBLIC_GATEWAY_URL=http://con.mind-one.co.kr:11083 # 개발 서버 API # NEXT_PUBLIC_GATEWAY_URL=http://localhost:8883 # 개발 서버 API CONTENTS_INTRO = 31 +CONTENTS_EXAMPLE = 32 NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= diff --git a/apps/dm/.env.mindone b/apps/dm/.env.mindone index 73e8a388a..1a11ec6a9 100644 --- a/apps/dm/.env.mindone +++ b/apps/dm/.env.mindone @@ -21,6 +21,7 @@ NEXT_PUBLIC_GATEWAY_URL=http://con.mind-one.co.kr:11083 # 개발 서버 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 # 박이정 CONTENTS_INTRO = 31 +CONTENTS_EXAMPLE = 32 NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= diff --git a/apps/dm/.env.production b/apps/dm/.env.production index 9ce7b404a..b2617e68e 100644 --- a/apps/dm/.env.production +++ b/apps/dm/.env.production @@ -19,6 +19,7 @@ NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x} NEXT_PUBLIC_GATEWAY_URL = http://apigateway.iipbs-was.svc.cluster.local:8000 CONTENTS_INTRO = 31 +CONTENTS_EXAMPLE = 32 NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA= diff --git a/apps/dm/app/(with-layout)/example/_components/GlobalCaseSection.tsx b/apps/dm/app/(with-layout)/example/_components/GlobalCaseSection.tsx index 73492a21d..26998ce78 100644 --- a/apps/dm/app/(with-layout)/example/_components/GlobalCaseSection.tsx +++ b/apps/dm/app/(with-layout)/example/_components/GlobalCaseSection.tsx @@ -1,780 +1,548 @@ "use client"; -import Image from "next/image"; -import CaseImageSlider from "@/components/CaseImageSlider"; - -type Slide = { - src: string; - alt: string; -}; - -type ContentBlock = { - title: string; - items: TextItem[]; -}; - -type InfoCard = { - title: string; - desc: string; -}; - -type TableData = { - headers: string[]; - rows: string[][]; -}; - -type OverviewCompareRow = { - country: string; - flag?: string; - overseas: string; - current: string; - future: string; -}; - -type GlobalCase = - | { - type: "overview"; - id: string; - title: string; - } - | { - type: "default"; - id: string; - title: string; - slides: Slide[]; - blocks?: ContentBlock[]; - cards?: InfoCard[]; - table?: TableData; - }; - -function SectionTitle({ children }: { children: React.ReactNode }) { - return ( -
- -

{children}

-
- ); -} - -function BlockTitle({ children }: { children: React.ReactNode }) { - return ( -
- - - - -

{children}

-
- ); -} - -function Hr({ className = "" }: { className?: string }) { - return
; -} - -type TextItem = - | string - | { - text: React.ReactNode; - depth?: number; // 0: 기본 / 1: 하위 / 2: 더 하위 - }; - -function ContentList({ items }: { items: TextItem[] }) { - return ( - - ); -} - -function ContentBlocks({ - blocks, - cards, - table, -}: { - blocks?: ContentBlock[]; - cards?: InfoCard[]; - table?: TableData; -}) { - if (!blocks?.length) return null; - - return ( -
- {blocks.map((block, index) => ( -
- {block.title} - - {!!block.items.length && ( - - )} - - {block.title.includes("재생 수자원 개발법") && ( - - )} - - {block.title.includes("대만 재이용 시설별 재이용수 공급단지") && ( - - )} -
- ))} -
- ); -} - -function InfoCards({ cards }: { cards?: InfoCard[] }) { - if (!cards?.length) return null; - - return ( -
- {cards.map((card) => ( -
- - {card.title} - -

- {card.desc} -

-
- ))} -
- ); -} - -function DataTable({ table }: { table?: TableData }) { - if (!table) return null; - - return ( -
- - - - {table.headers.map((header, index) => ( - - ))} - - - - - {table.rows.map((row, rowIndex) => ( - - {row.map((cell, cellIndex) => ( - - ))} - - ))} - -
- {header} -
- {cell} -
-
- ); -} - -/* ========================= - 사례개요 전용 컴포넌트 -========================= */ - -const compareRows: OverviewCompareRow[] = [ - { - country: "한국", - flag: "/images/landing/business/globalCase/flag_kr.png", - overseas: "12.1%", - current: "15.6%", - future: "17%", - }, - { - country: "대만", - flag: "/images/landing/business/globalCase/flag_tw.png", - overseas: "2.4%", - current: "4.3%", - future: "16.5%", - }, -]; - -const reuseRateRows = [ - ["이스라엘", "중동&북아프리카", "92.3", "튀르키예", "동유럽&중앙아시아", "26.6"], - ["쿠웨이트", "중동&북아프리카", "89.3", "독일", "서유럽", "25.0"], - ["호주", "동아시아&태평양", "49.0", "스페인", "서유럽", "24.4"], - ["싱가포르", "동아시아&태평양", "44.4", "한국", "동아시아&태평양", "12.1"], - ["그리스", "서유럽", "39.1", "미국", "북아메리카", "13.1"], - ["사우디아라비아", "중동&북아프리카", "32.8", "프랑스", "서유럽", "9.5"], - ["이탈리아", "서유럽", "31.7", "대만", "동아시아&태평양", "2.4"], - ["룩셈부르크", "서유럽", "29.1", "일본", "동아시아&태평양", "0.8"], - ["포르투갈", "서유럽", "29.0", "", "", ""], -]; - -function WaterBullet() { - return ( - bullet - ); -} - -function OverviewNoticeBox() { - return ( -
-
- -
- - 우리나라 하수처리수 재이용률은 15% 수준 - - -
    -
  • - 2024년 하수도 통계 기준 15.6%이며, 제2차 물 재이용 기본 계획을 고려한{" "} - 2030년 목표는 17% -
  • -
  • - 대만은 가뭄과 공업용수 증가에 대응하여 현재 4.3% 수준이며, 16.5%까지 확대 계획 수립 -
  • -
-
-
-
- ); -} - -function OverviewCompareTable() { - return ( -
- - - - - - - - - - - - {compareRows.map((row) => ( - - - - - - - ))} - -
- 국가 - - 해외정보 - - 현재 - - 미래(목표, 예측치) -
-
- {row.flag && ( - {`${row.country} - )} - {row.country} -
-
- {row.overseas} - - {row.current} - - {row.future} -
-
- ); -} - -function OverviewDescriptionBox() { - return ( -
- -
- ); -} - -function OverviewReuseRateTable() { - return ( -
- - - - {["국가명", "지역", "재이용률", "국가명", "지역", "재이용률"].map((header, index) => ( - - ))} - - - - - {reuseRateRows.map((row, rowIndex) => ( - - {row.map((cell, cellIndex) => ( - - ))} - - ))} - -
- {header} -
- {cell} -
-
- ); -} - -function OverviewSection() { - return ( -
-
- 주요 국가 물 재이용 계획 - - -
- -
- 주요 국가의 하수처리수 재이용률 (%) - - -
-
- ); -} - -/* ========================= - 데이터 -========================= */ - -const globalCases: GlobalCase[] = [ - { - type: "overview", - id: "example-global-overview", - title: "사례 개요", - }, - { - type: "default", - id: "example-global-taiwan", - title: "[대만] 국가 주도의 공업용수 공급", - slides: [ - { - src: "/images/landing/business/globalCase/taiwan_01.png", - alt: "대만 국가 주도의 공업용수 공급 1", - }, - { - src: "/images/landing/business/globalCase/taiwan_02.png", - alt: "대만 국가 주도의 공업용수 공급 2", - }, - { - src: "/images/landing/business/globalCase/taiwan_03.png", - alt: "대만 국가 주도의 공업용수 공급 3", - }, - ], - blocks: [ - { - title: "재생 수자원 개발법 제정 (2015년제정, 2022개정)", - items: [ - { text: "목적: 수자원 지속 가능성 향상, 재생 수자원 활용을 통해 물부족 문제 해결" }, - { text: "내용: 재생 수자원 정의·분류, 이용 의무, 이용 제한, 수질 기준 등" } - ], - }, - { - title: "대만정부 하수재이용 사업 시행", - items: [ - { text: "대만은 물 재이용에 적극적이며 국가주도로 하수재이용 사업 시행(안핑)" }, - { text: "1,518억의 사업비 중 92% 국비, 지방비 8% 투입", depth: 1 }, - { text: "재이용수는 24km 관로로 타이난 TSMC로 공급", depth: 1 }, - { text: "안핑을 포함한 16건 공사 완료시 628,100 CMD 공급 가능" }, - { text: "공급단가는 수질과 운영조건에 따라 다르게 책정" }, - ], - }, - { - title: "대만 재이용 시설별 재이용수 공급단지", - items: [], - }, - { - title: "특이사항", - items: [ - "대만은 강수량이 풍부하지만 지역별 편차로 수자원 확보에 어려움을 겪고 있으며, 특히 남부 지역은 우기와 건기의 강수 차이가 큼", - "최근 수년간 심각한 가뭄이 발생하면서 반도체 산업을 국가 핵심 산업으로 지정, 공업용수 수요가 크게 증가", - "주요 반도체 공장이 남부에 집중되어 있어 물 부족 문제가 지속적으로 발생, 2021년 가뭄 당시 산업단지 용수 공급에도 차질 발생", - "정부는 2023년부터 산업용수 추가 요금을 부과하고, 농업용수 규제를 강화하는 등 용수 관리 정책 시행", - "기후변화 대응을 위해 수자원 보호 및 재이용 정책을 지속적으로 확대", - "산업용수 다소비 산업과 하수처리수를 재이용수 주요 공급원으로 활용 계획", - ], - }, - ], - cards: [ - { - title: "제4조.", - desc: "물 소비 계획에 따른 재이용수 이용 관련 내용 (일정 비율 재이용수 사용 규정)", - }, - { - title: "제5조.", - desc: "재이용수의 운영자, 사용자 관련 내용 (지자체에서 하수 무상공급 제공 가능)", - }, - { - title: "제7조.", - desc: "재이용수 이용 범위, 이용량 재이용수 수질 기준 내용", - }, - { - title: "제8조.", - desc: "재이용수 사업 주체, 책임 등에 관한 내용 (광역인 경우, 정부가 지자체 지정)", - }, - ], - table: { - headers: ["사업", "가격(NTD/㎥)", "가격(원/㎥)"], - rows: [ - ["Fongshan(가오슝)", "23.39", "1,038원"], - ["Linhai(가오슝)", "19.28", "858원"], - ["Anping(타이난)", "38.28", "1,699원"], - ["Yongkang(타이난)", "23.67", "1,050원"], - ["Rende(타이난)", "28.26", "1,254원"], - ["Shuinan(타이중)", "15.00", "668원"], - ["Quaitou(가오슝)", "35.44", "1,573원"], - ], - }, - }, - { - type: "default", - id: "example-global-singapore", - title: "[싱가포르] 수요가 높은 산업의 물 재이용 의무화", - slides: [ - { - src: "/images/landing/business/globalCase/singapore_01.png", - alt: "싱가포르 NEWater 사례 1", - }, - { - src: "/images/landing/business/globalCase/singapore_02.png", - alt: "싱가포르 NEWater 사례 2", - }, - { - src: "/images/landing/business/globalCase/singapore_03.png", - alt: "싱가포르 NEWater 사례 3", - }, - ], - blocks: [ - { - title: "싱가포르 NEWater 비중 확대", - items: [ - "2003년 공식 NEWater 공장을 가동했으며 신뢰확보를 위해 NEWater Visitor Center를 함께 건설", - "NEWater 전체파이프 공급 → 반도체 공장에서 가장 많이 사용", - "2060년까지 NEWater 비중 55%까지 높이는 것이 싱가포르의 목표", - ], - }, - { - title: "싱가포르 재이용수 의무화 및 지원정책", - items: [ - { text: "공업용수 수요가 높은 산업에 대해 물 재이용을 의무화" }, - { text: "반도체업계 전체 비 재이용수(non-domestic용수) 17% 차지 → 공업용수 수요 원인", depth: 1 }, - { text: "2024년 1월 1일부터 연간 최소 60,000㎥를 소비하는 사업자는 재이용수 사용 의무화", depth: 1 }, - { text: "반도체 공장의 경우 최소 50% 이상의 재이용수를 요구함(현재는 43% 정도)", depth: 1 }, - { text: "재정 지원을 통해 재이용 활성화를 지원" }, - { text: "재이용 설비를 기준으로 2025년까지 싱가포르 수자원공사의 2가지 금융상품 제공", depth: 1 }, - { text: "1. Water Efficiency Fund", depth: 2 }, - { text: "2. Industrial Water Solutions Demonstration Fund", depth: 2 }, - { text: "2025년 기준 지원 한도 상향 → 물 재이용 활성 도모", depth: 1 }, - { text: "투입비용 50% → 75% 지원 / WSDP: $400만 → $500만", depth: 3 }, - { text: "물 효율이 높은 장비를 도입하는 데에도 최대 $300만 지원 가능", depth: 3 }, - ], - }, - { - title: "특이사항", - items: [ - { text: "싱가포르는 천연 수자원이 많지 않아, 말레이시아 조호르와 1962년 맺은 협약으로 물을 수입해왔으며, 재이용수를 늘리기 위한 노력을 계속하고 있음" }, - { text: "싱가포르는 국토 내에 호수와 하천이 적고 집수공간도 적어 빗물로 확보할 수 있는 수자원량은 전체 물 수요의 20%에 불과함", depth: 1 }, - - { text: "싱가포르는 2002년부터 하수 재이용수인 NEWater를 개발, 비음용수와 간접음용수로 활용하고 있음" }, - { text: "재이용수를 NEWater라는 이름으로 명명하고 NEWater에 대한 신뢰 확보를 위해 NEWater 방문자 센터도 건설함", depth: 1 }, - { text: "비음용수로 많이 사용하며 건기에는 NEWater를 저수지에 추가하여 원수와 섞음. 원수는 상수도에서 처리 한 후 가정에 수돗물로 공급함", depth: 1 }, - { text: "NEWater는 전용 파이프를 통해 주로 웨이퍼 제조 공장, 산업단지, 상업용 건물에서 산업 및 냉각수로 사용. 제일 많이 사용하는 곳은 웨이퍼 제조 공장", depth: 1 }, - - { text: "2024년 4월 기준 1㎥ 당 공업용수 가격은 $1.66이며, Non-domestic 기준 NEWater 가격은 $2.41" }, - { text: "공업용수 가격 $1.66은 수도요금(water tariff)과 수자원세(waterborne tax)로 구분됨. $1.66에는 $1만큼의 수자원세가 포함됨" }, - { text: "2023년 싱가포르는 산업용수 수요가 높은 산업에 대해 물 재이용을 의무화했으며, 정부는 기금을 통해 물 재이용을 지원함" }, - ], - }, - ], - }, - { - type: "default", - id: "example-global-australia", - title: "[호주] 생활 속 물 재이용 / 광역 물 재이용수 공급", - slides: [ - { - src: "/images/landing/business/globalCase/australia_01.png", - alt: "호주 생활 속 물 재이용 1", - }, - { - src: "/images/landing/business/globalCase/australia_02.png", - alt: "호주 광역 물 재이용수 공급 2", - }, - { - src: "/images/landing/business/globalCase/australia_03.png", - alt: "호주 광역 물 재이용수 공급 3", - }, - ], - blocks: [ - { - title: "생활용수로 재이용수의 적극 활용", - items: [ - "재이용수를 공원, 골프장 등의 관계용수로 공급(초록색 지역)", - "Dual pipe로 재이용수를 특정 주거단지로 공급(보라색 지역)", - "현재 7,000여 가구가 재이용수를 사용", - "2030년까지 43,000가구로 확대 예정", - ], - }, - { - title: "호주 재이용수 광역 활용(퀸즈랜드주 브리즈번)", - items: [ - { text: "SEQ Water Grid는 브리즈번 남동부지역의 식수 공급을 위해 설정한 광역 수로망" }, - { text: "이 광역수로망을 활용하여 물이 부족한 지역으로 물을 공급" }, - - { - text: ( - <> - 12개 댐, 32개 수처리 시설, 3개 재이용시설, 담수화 플랜트 1개, 저수지 28개와{" "} - - 600km 이상의 물 공급 관로를 건설 - - - ), - depth: 1, - }, - { - text: ( - <> - 지역적으로 강수량이 고르지 않아,{" "} - - 댐의 저수율이 불균등 할 때 - {" "} - 지역의 물 공급을 보충하는 중요한 역할 수행 - - ), - depth: 1, - }, - - - ], - }, - { - title: "특이사항", - items: [ - "호주는 2000~2009년 강수량 감소로 밀레니엄 가뭄이 발생하여 심각한 물 부족 현상이 발생함", - "호주는 해안 도시에 인구가 밀집하여 거주하고 있으며 인구 증가율이 타 선진국 대비 높아, 향후 도시에 공급할 수자원 확보가 필요함", - "호주정부는 1990년대부터 하·폐수가 방류될 경우의 환경 피해에 대해 인식하기 시작하고 배출규제와 위원회를 구성하기 시작", - "연방정부는 주에 인센티브를 주는 방식으로 물 정책을 장려하고 토지소유권과 물 권리를 구분하여 거래를 허용하고 물 재이용 활성화를 위한 Water Smart Australia 프로그램을 시행", - "호주의 용수공급 체계는 영국과 미국의 특성들이 존재하지만, 주의 특성과 주정부의 정책 방향에 따라 주마다 많은 차이가 있음", - ], - }, - ], - }, - { - type: "default", - id: "example-global-eu", - title: "[EU] 법률을 제정, EU국가에서 물 재이용 유도", - slides: [ - { - src: "/images/landing/business/globalCase/eu_01.png", - alt: "EU 물 재이용 기술 사례", - }, - { - src: "/images/landing/business/globalCase/eu_02.png", - alt: "EU 물 재이용 유도 사례", - }, - ], - blocks: [ - { - title: "물 재이용에 대한 법률을 제정(2020 제정, 2023 시행)", - items: [ - "유럽 지역의 물 부족 문제를 인식하고 ’20년 물 재이용 법률을 제정 (2020/741)", - "2023년부터 모든 EU국가에서 시행", - ], - }, - { - title: "유럽연합 회원국 물 재이용 지원정책", - items: [ - { text: "유럽연합 차원에서 다양한 물 재이용 프로그램 자금 지원" }, - - { text: "2021년부터 3년간 Life RECYCLIO 프로그램 지원 → 세부내용 물 재이용 프로그램", depth: 1 }, - - { text: "1. 환경 관련 사업에 자금을 지원하는 Life 프로그램의 일환으로 80만 유로를 지원", depth: 2 }, - { text: "2. 식품·농업 분야 물 재이용 확대를 위해 기술 이전 및 평가 실시", depth: 2 }, - - { text: "물 재이용 정책 수행을 위해 BOOST-INN 180만 유로를 지원", depth: 1 }, - - { text: "프랑스, 이탈리아, 스페인 등 유럽 선진국 솔루션 공유", depth: 3 }, - - { text: "EU산하 JRC(공동연구센터)에서 물 재이용 기술제공 지원" }, - - { text: ( - <> - ‘물 재이용 회원 관리 실무 커뮤니티(Water Reuse Management Community of Practice)’ 시범사업 수행 -
-   유럽 전역에 성공적인 모델 활용 및 혁신적인 물 재이용 솔루션을 확장할 수 있는 기술을 제공 - - ), - depth: 1 - } - ], - }, - { - title: "특이사항", - items: [ - "2019년 기준 EU 인구의 38%가 물 부족의 영향을 받을 것으로 예상됨. 이 흐름대로면 2030년까지 유럽 유역 전반에 물 부족 문제가 발생하게 될 것으로 예상. 인구 증가, 도시화의 확대, 염분화 및 기후변화들이 수자원 문제들의 주요 요인이며, 식습관 변화와 산업용수 수요 증가 등으로 악화되고 있는 상황", - "유럽의 물 부족 상황은 지역에 따라 다르게 나타나 스페인을 비롯한 남부유럽에서 특히 심각함. 전반적으로 북유럽에 비해 남유럽의 물 스트레스가 심하며, 특히 스페인과 포르투갈, 프랑스 동부지역의 물 스트레스가 심각함", - "유럽 연합에서도 물 부족 문제를 인식하고 2000년 EU Water Framework Directive를 개발, 이후 2020년 재이용에 관한 법률을 제정", - ], - }, - ], - }, -]; - -function DefaultGlobalSection({ - item, -}: { - item: Extract; -}) { - return ( - <> - - - - - ); -} +import { useEffect, useRef } from "react"; export default function GlobalCaseSection() { - const title = "해외 재이용 사례"; + const rootRef = useRef(null); - return ( -
-
-

- {title} - + useEffect(() => { + const root = rootRef.current; + if (!root) return; + + const cleanups: Array<() => void> = []; + const sliders = Array.from(root.querySelectorAll(".image-slider")); + + sliders.forEach((sliderElement) => { + const slider = sliderElement as HTMLElement; + const viewport = slider.querySelector( + ".image-slider-viewport", + ) as HTMLElement | null; + const track = slider.querySelector(".image-slider-track") as HTMLElement | null; + const dotsWrap = slider.querySelector( + ".image-slider-dots", + ) as HTMLElement | null; + const pages = Array.from( + slider.querySelectorAll(".image-slider-page"), + ) as HTMLElement[]; + + if (!viewport || !track || !dotsWrap || pages.length === 0) return; + + let page = 0; + let dragging = false; + let isHover = false; + let startX = 0; + let deltaX = 0; + let timer: number | null = null; + + const autoPlayMs = Number(slider.dataset.autoplay ?? "4500"); + const canSlide = pages.length > 1; + + if (!canSlide) { + dotsWrap.style.display = "none"; + } else { + dotsWrap.style.display = ""; + } + + const render = () => { + track.style.transform = `translateX(-${page * 100}%)`; + + Array.from(dotsWrap.querySelectorAll("button")).forEach((dot, index) => { + dot.classList.toggle("is-active", index === page); + }); + }; + + const goTo = (nextPage: number) => { + if (!canSlide) return; + + if (nextPage < 0) page = pages.length - 1; + else if (nextPage >= pages.length) page = 0; + else page = nextPage; + + render(); + }; + + const stopTimer = () => { + if (timer !== null) { + window.clearInterval(timer); + timer = null; + } + }; + + const startTimer = () => { + stopTimer(); + if (!canSlide || dragging || isHover) return; + + timer = window.setInterval(() => { + page = page + 1 >= pages.length ? 0 : page + 1; + render(); + }, autoPlayMs); + }; + + const dotButtons = pages.map((_, index) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "image-slider-dot"; + + const onClick = () => { + goTo(index); + startTimer(); + }; + + button.addEventListener("click", onClick); + dotsWrap.appendChild(button); + + cleanups.push(() => { + button.removeEventListener("click", onClick); + }); + + return button; + }); + + const onMouseEnter = () => { + isHover = true; + stopTimer(); + }; + + const onMouseLeave = () => { + isHover = false; + startTimer(); + }; + + const onPointerDown = (event: PointerEvent) => { + if (!canSlide) return; + + dragging = true; + startX = event.clientX; + deltaX = 0; + viewport.setPointerCapture(event.pointerId); + }; + + const onPointerMove = (event: PointerEvent) => { + if (!dragging) return; + deltaX = event.clientX - startX; + }; + + const onPointerUp = () => { + if (!dragging) return; + + dragging = false; + + if (deltaX > 50) goTo(page - 1); + if (deltaX < -50) goTo(page + 1); + + deltaX = 0; + startTimer(); + }; + + viewport.addEventListener("mouseenter", onMouseEnter); + viewport.addEventListener("mouseleave", onMouseLeave); + viewport.addEventListener("pointerdown", onPointerDown); + viewport.addEventListener("pointermove", onPointerMove); + viewport.addEventListener("pointerup", onPointerUp); + viewport.addEventListener("pointercancel", onPointerUp); + + cleanups.push(() => { + stopTimer(); + viewport.removeEventListener("mouseenter", onMouseEnter); + viewport.removeEventListener("mouseleave", onMouseLeave); + viewport.removeEventListener("pointerdown", onPointerDown); + viewport.removeEventListener("pointermove", onPointerMove); + viewport.removeEventListener("pointerup", onPointerUp); + viewport.removeEventListener("pointercancel", onPointerUp); + dotButtons.forEach((button) => button.remove()); + }); + + render(); + startTimer(); + }); + + return () => { + cleanups.forEach((cleanup) => cleanup()); + }; + }, []); + + const contentsHtml = ` +
+
+

+ 해외 재이용 사례 +  

-
- {globalCases.map((item, index) => ( -
- {item.title} +
+
+
+   +

사례 개요

+
- {item.type === "overview" ? ( - - ) : ( - - )} +
+
+
+ +   +   + +

주요 국가 물 재이용 계획

+
- {index !== globalCases.length - 1 &&
} -
- ))} +
+
+ bullet +
+ 우리나라 하수처리수 재이용률은 15% 수준 +
    +
  • 2024년 하수도 통계 기준 15.6%이며, 제2차 물 재이용 기본 계획을 고려한 2030년 목표는 17%
  • +
  • 대만은 가뭄과 공업용수 증가에 대응하여 현재 4.3% 수준이며, 16.5%까지 확대 계획 수립
  • +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
국가해외정보현재미래(목표, 예측치)
+
+ 한국 국기 + 한국 +
+
12.1%15.6%17%
+
+ 대만 국기 + 대만 +
+
2.4%4.3%16.5%
+
+
+ +
+
+ +   +   + +

주요 국가의 하수처리수 재이용률 (%)

+
+ +
+
    +
  • 전 세계적으로 물 재이용은 호주, 유럽, 아프리카, 미국 등 가뭄이 잦거나 심각한 가뭄이 발생한 국가에서 많이 이루어짐
  • +
  • 이스라엘, 쿠웨이트 등 중동 및 북아프리카 지역은 가용 수자원이 제한되어 있어 재이용량 비중도 매우 높은 편
  • +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
국가명지역재이용률국가명지역재이용률
이스라엘중동&북아프리카92.3튀르키예동유럽&중앙아시아26.6
쿠웨이트중동&북아프리카89.3독일서유럽25.0
호주동아시아&태평양49.0스페인서유럽24.4
싱가포르동아시아&태평양44.4한국동아시아&태평양12.1
그리스서유럽39.1미국북아메리카13.1
사우디아라비아중동&북아프리카32.8프랑스서유럽9.5
이탈리아서유럽31.7대만동아시아&태평양2.4
룩셈부르크서유럽29.1일본동아시아&태평양0.8
포르투갈서유럽29.0
+
+
+
+ + +
+ +
+
+   +

[대만] 국가 주도의 공업용수 공급

+
+ +
+
+
+
+
+
대만 국가 주도의 공업용수 공급 1
+
대만 국가 주도의 공업용수 공급 2
+
+
+
대만 국가 주도의 공업용수 공급 3
+
+
+
+
+
+
+ +
+
+
  

재생 수자원 개발법 제정 (2015년제정, 2022개정)

+
    +
  • 목적: 수자원 지속 가능성 향상, 재생 수자원 활용을 통해 물부족 문제 해결
  • +
  • 내용: 재생 수자원 정의·분류, 이용 의무, 이용 제한, 수질 기준 등
  • +
+
+
제4조.

물 소비 계획에 따른 재이용수 이용 관련 내용 (일정 비율 재이용수 사용 규정)

+
제5조.

재이용수의 운영자, 사용자 관련 내용 (지자체에서 하수 무상공급 제공 가능)

+
제7조.

재이용수 이용 범위, 이용량 재이용수 수질 기준 내용

+
제8조.

재이용수 사업 주체, 책임 등에 관한 내용 (광역인 경우, 정부가 지자체 지정)

+
+
+ +
+
  

대만정부 하수재이용 사업 시행

+
    +
  • 대만은 물 재이용에 적극적이며 국가주도로 하수재이용 사업 시행(안핑)
  • +
  • 1,518억의 사업비 중 92% 국비, 지방비 8% 투입
  • +
  • 재이용수는 24km 관로로 타이난 TSMC로 공급
  • +
  • 안핑을 포함한 16건 공사 완료시 628,100 CMD 공급 가능
  • +
  • 공급단가는 수질과 운영조건에 따라 다르게 책정
  • +
+
+ +
+
  

대만 재이용 시설별 재이용수 공급단지

+
+ + + + + + + + + + + + + +
사업가격(NTD/㎥)가격(원/㎥)
Fongshan(가오슝)23.391,038원
Linhai(가오슝)19.28858원
Anping(타이난)38.281,699원
Yongkang(타이난)23.671,050원
Rende(타이난)28.261,254원
Shuinan(타이중)15.00668원
Quaitou(가오슝)35.441,573원
+
+
+ +
+
  

특이사항

+
    +
  • 대만은 강수량이 풍부하지만 지역별 편차로 수자원 확보에 어려움을 겪고 있으며, 특히 남부 지역은 우기와 건기의 강수 차이가 큼
  • +
  • 최근 수년간 심각한 가뭄이 발생하면서 반도체 산업을 국가 핵심 산업으로 지정, 공업용수 수요가 크게 증가
  • +
  • 주요 반도체 공장이 남부에 집중되어 있어 물 부족 문제가 지속적으로 발생, 2021년 가뭄 당시 산업단지 용수 공급에도 차질 발생
  • +
  • 정부는 2023년부터 산업용수 추가 요금을 부과하고, 농업용수 규제를 강화하는 등 용수 관리 정책 시행
  • +
  • 기후변화 대응을 위해 수자원 보호 및 재이용 정책을 지속적으로 확대
  • +
  • 산업용수 다소비 산업과 하수처리수를 재이용수 주요 공급원으로 활용 계획
  • +
+
+
+
+ +
+ +
+
 

[싱가포르] 수요가 높은 산업의 물 재이용 의무화

+
+
+
+
+
+
싱가포르 NEWater 사례 1
+
싱가포르 NEWater 사례 2
+
+
+
싱가포르 NEWater 사례 3
+
+
+
+
+
+
+
+
+
  

싱가포르 NEWater 비중 확대

+
    +
  • 2003년 공식 NEWater 공장을 가동했으며 신뢰확보를 위해 NEWater Visitor Center를 함께 건설
  • +
  • NEWater 전체파이프 공급 → 반도체 공장에서 가장 많이 사용
  • +
  • 2060년까지 NEWater 비중 55%까지 높이는 것이 싱가포르의 목표
  • +
+
+
+
  

싱가포르 재이용수 의무화 및 지원정책

+
    +
  • 공업용수 수요가 높은 산업에 대해 물 재이용을 의무화
  • +
  • 반도체업계 전체 비 재이용수(non-domestic용수) 17% 차지 → 공업용수 수요 원인
  • +
  • 2024년 1월 1일부터 연간 최소 60,000㎥를 소비하는 사업자는 재이용수 사용 의무화
  • +
  • 반도체 공장의 경우 최소 50% 이상의 재이용수를 요구함(현재는 43% 정도)
  • +
  • 재정 지원을 통해 재이용 활성화를 지원
  • +
  • 재이용 설비를 기준으로 2025년까지 싱가포르 수자원공사의 2가지 금융상품 제공
  • +
  • 1. Water Efficiency Fund
  • +
  • 2. Industrial Water Solutions Demonstration Fund
  • +
  • 2025년 기준 지원 한도 상향 → 물 재이용 활성 도모
  • +
  • * 투입비용 50% → 75% 지원 / WSDP: $400만 → $500만
  • +
  • * 물 효율이 높은 장비를 도입하는 데에도 최대 $300만 지원 가능
  • +
+
+
+
  

특이사항

+
    +
  • 싱가포르는 천연 수자원이 많지 않아, 말레이시아 조호르와 1962년 맺은 협약으로 물을 수입해왔으며, 재이용수를 늘리기 위한 노력을 계속하고 있음
  • +
  • 싱가포르는 국토 내에 호수와 하천이 적고 집수공간도 적어 빗물로 확보할 수 있는 수자원량은 전체 물 수요의 20%에 불과함
  • +
  • 싱가포르는 2002년부터 하수 재이용수인 NEWater를 개발, 비음용수와 간접음용수로 활용하고 있음
  • +
  • 재이용수를 NEWater라는 이름으로 명명하고 NEWater에 대한 신뢰 확보를 위해 NEWater 방문자 센터도 건설함
  • +
  • 비음용수로 많이 사용하며 건기에는 NEWater를 저수지에 추가하여 원수와 섞음. 원수는 상수도에서 처리 한 후 가정에 수돗물로 공급함
  • +
  • NEWater는 전용 파이프를 통해 주로 웨이퍼 제조 공장, 산업단지, 상업용 건물에서 산업 및 냉각수로 사용. 제일 많이 사용하는 곳은 웨이퍼 제조 공장
  • +
  • 2024년 4월 기준 1㎥ 당 공업용수 가격은 $1.66이며, Non-domestic 기준 NEWater 가격은 $2.41
  • +
  • 공업용수 가격 $1.66은 수도요금(water tariff)과 수자원세(waterborne tax)로 구분됨. $1.66에는 $1만큼의 수자원세가 포함됨
  • +
  • 2023년 싱가포르는 산업용수 수요가 높은 산업에 대해 물 재이용을 의무화했으며, 정부는 기금을 통해 물 재이용을 지원함
  • +
+
+
+
+ +
+ +
+
 

[호주] 생활 속 물 재이용 / 광역 물 재이용수 공급

+
+
+
+
+
+
호주 생활 속 물 재이용 1
+
호주 광역 물 재이용수 공급 2
+
+
+
호주 광역 물 재이용수 공급 3
+
+
+
+
+
+
+
+
+
  

생활용수로 재이용수의 적극 활용

+
    +
  • 재이용수를 공원, 골프장 등의 관계용수로 공급(초록색 지역)
  • +
  • Dual pipe로 재이용수를 특정 주거단지로 공급(보라색 지역)
  • +
  • 현재 7,000여 가구가 재이용수를 사용
  • +
  • 2030년까지 43,000가구로 확대 예정
  • +
+
+
+
  

호주 재이용수 광역 활용(퀸즈랜드주 브리즈번)

+
    +
  • SEQ Water Grid는 브리즈번 남동부지역의 식수 공급을 위해 설정한 광역 수로망
  • +
  • 이 광역수로망을 활용하여 물이 부족한 지역으로 물을 공급
  • +
  • 12개 댐, 32개 수처리 시설, 3개 재이용시설, 담수화 플랜트 1개, 저수지 28개와 600km 이상의 물 공급 관로를 건설
  • +
  • 지역적으로 강수량이 고르지 않아, 댐의 저수율이 불균등 할 때 지역의 물 공급을 보충하는 중요한 역할 수행
  • +
+
+
+
  

특이사항

+
    +
  • 호주는 2000~2009년 강수량 감소로 밀레니엄 가뭄이 발생하여 심각한 물 부족 현상이 발생함
  • +
  • 호주는 해안 도시에 인구가 밀집하여 거주하고 있으며 인구 증가율이 타 선진국 대비 높아, 향후 도시에 공급할 수자원 확보가 필요함
  • +
  • 호주정부는 1990년대부터 하·폐수가 방류될 경우의 환경 피해에 대해 인식하기 시작하고 배출규제와 위원회를 구성하기 시작
  • +
  • 연방정부는 주에 인센티브를 주는 방식으로 물 정책을 장려하고 토지소유권과 물 권리를 구분하여 거래를 허용하고 물 재이용 활성화를 위한 Water Smart Australia 프로그램을 시행
  • +
  • 호주의 용수공급 체계는 영국과 미국의 특성들이 존재하지만, 주의 특성과 주정부의 정책 방향에 따라 주마다 많은 차이가 있음
  • +
+
+
+
+ +
+ +
+
 

[EU] 법률을 제정, EU국가에서 물 재이용 유도

+
+
+
+
+
+
EU 물 재이용 기술 사례
+
EU 물 재이용 유도 사례
+
+
+
+
+
+
+
+
+
  

물 재이용에 대한 법률을 제정(2020 제정, 2023 시행)

+
    +
  • 유럽 지역의 물 부족 문제를 인식하고 ’20년 물 재이용 법률을 제정 (2020/741)
  • +
  • 2023년부터 모든 EU국가에서 시행
  • +
+
+
+
  

유럽연합 회원국 물 재이용 지원정책

+
    +
  • 유럽연합 차원에서 다양한 물 재이용 프로그램 자금 지원
  • +
  • 2021년부터 3년간 Life RECYCLIO 프로그램 지원 → 세부내용 물 재이용 프로그램
  • +
  • 1. 환경 관련 사업에 자금을 지원하는 Life 프로그램의 일환으로 80만 유로를 지원
  • +
  • 2. 식품·농업 분야 물 재이용 확대를 위해 기술 이전 및 평가 실시
  • +
  • 물 재이용 정책 수행을 위해 BOOST-INN 180만 유로를 지원
  • +
  • * 프랑스, 이탈리아, 스페인 등 유럽 선진국 솔루션 공유
  • +
  • EU산하 JRC(공동연구센터)에서 물 재이용 기술제공 지원
  • +
  • ‘물 재이용 회원 관리 실무 커뮤니티(Water Reuse Management Community of Practice)’ 시범사업 수행
     유럽 전역에 성공적인 모델 활용 및 혁신적인 물 재이용 솔루션을 확장할 수 있는 기술을 제공
  • +
+
+
+
  

특이사항

+
    +
  • 2019년 기준 EU 인구의 38%가 물 부족의 영향을 받을 것으로 예상됨. 이 흐름대로면 2030년까지 유럽 유역 전반에 물 부족 문제가 발생하게 될 것으로 예상. 인구 증가, 도시화의 확대, 염분화 및 기후변화들이 수자원 문제들의 주요 요인이며, 식습관 변화와 산업용수 수요 증가 등으로 악화되고 있는 상황
  • +
  • 유럽의 물 부족 상황은 지역에 따라 다르게 나타나 스페인을 비롯한 남부유럽에서 특히 심각함. 전반적으로 북유럽에 비해 남유럽의 물 스트레스가 심하며, 특히 스페인과 포르투갈, 프랑스 동부지역의 물 스트레스가 심각함
  • +
  • 유럽 연합에서도 물 부족 문제를 인식하고 2000년 EU Water Framework Directive를 개발, 이후 2020년 재이용에 관한 법률을 제정
  • +
+
+
+

+ `; + + return ( +
); -} \ No newline at end of file +} diff --git a/apps/dm/app/(with-layout)/example/_components/GreywaterExampleSection.tsx b/apps/dm/app/(with-layout)/example/_components/GreywaterExampleSection.tsx index 1705a6c84..b3958df87 100644 --- a/apps/dm/app/(with-layout)/example/_components/GreywaterExampleSection.tsx +++ b/apps/dm/app/(with-layout)/example/_components/GreywaterExampleSection.tsx @@ -1,191 +1,415 @@ "use client"; -function SectionTitle({ children }: { children: React.ReactNode }) { - return ( -
- -

{children}

-
- ); -} - -function BlockTitle({ children }: { children: React.ReactNode }) { - return ( -
- -

{children}

-
- ); -} - -// 구분선 -function Hr({ className = "" }: { className?: string }) { - return
; -} - -type SupplyRow = { - facility: string; - usage: string; - amount: string; -}; - -function SupplyTable({ rows }: { rows: SupplyRow[] }) { - return ( -
- - - - - - - - - - {rows.map((row, index) => ( - - - - - - ))} - -
- 시설명 - - 이용용도 - - 공급량(㎥) -
- {row.facility} - - {row.usage} - - {row.amount} -
-
- ); -} - -function InfoList({ - projectName, - purpose, - totalCost, - treatment, -}: { - projectName: string; - purpose: string; - totalCost: string; - treatment: string; -}) { - return ( -
-
- 사업명 -
    -
  • {projectName}
  • -
-
- -
- 사업목적 -
    -
  • {purpose}
  • -
-
- -
- 사업 주요 내용 -
    -
  • 총사업비 : {totalCost}
  • -
  • 처리공법 : {treatment}
  • -
-
- -
- 사업량 -
-
- ); -} +import { useEffect, useRef } from "react"; export default function GreywaterExampleSection() { - const title = "중수도 사례"; - return ( -
- {/* 상단 타이틀 */} -
-

- {title} - + const rootRef = useRef(null); + + useEffect(() => { + const root = rootRef.current; + if (!root) return; + + const cleanups: Array<() => void> = []; + const sliders = Array.from(root.querySelectorAll(".image-slider")); + + sliders.forEach((sliderElement) => { + const slider = sliderElement as HTMLElement; + const viewport = slider.querySelector( + ".image-slider-viewport", + ) as HTMLElement | null; + const track = slider.querySelector(".image-slider-track") as HTMLElement | null; + const dotsWrap = slider.querySelector( + ".image-slider-dots", + ) as HTMLElement | null; + const pages = Array.from( + slider.querySelectorAll(".image-slider-page"), + ) as HTMLElement[]; + + if (!viewport || !track || !dotsWrap || pages.length === 0) return; + + let page = 0; + let dragging = false; + let isHover = false; + let startX = 0; + let deltaX = 0; + let timer: number | null = null; + + const autoPlayMs = Number(slider.dataset.autoplay ?? "4500"); + const canSlide = pages.length > 1; + + if (!canSlide) { + dotsWrap.style.display = "none"; + } else { + dotsWrap.style.display = ""; + } + + const render = () => { + track.style.transform = `translateX(-${page * 100}%)`; + + Array.from(dotsWrap.querySelectorAll("button")).forEach((dot, index) => { + dot.classList.toggle("is-active", index === page); + }); + }; + + const goTo = (nextPage: number) => { + if (!canSlide) return; + + if (nextPage < 0) page = pages.length - 1; + else if (nextPage >= pages.length) page = 0; + else page = nextPage; + + render(); + }; + + const stopTimer = () => { + if (timer !== null) { + window.clearInterval(timer); + timer = null; + } + }; + + const startTimer = () => { + stopTimer(); + if (!canSlide || dragging || isHover) return; + + timer = window.setInterval(() => { + page = page + 1 >= pages.length ? 0 : page + 1; + render(); + }, autoPlayMs); + }; + + const dotButtons = pages.map((_, index) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "image-slider-dot"; + + const onClick = () => { + goTo(index); + startTimer(); + }; + + button.addEventListener("click", onClick); + dotsWrap.appendChild(button); + + cleanups.push(() => { + button.removeEventListener("click", onClick); + }); + + return button; + }); + + const onMouseEnter = () => { + isHover = true; + stopTimer(); + }; + + const onMouseLeave = () => { + isHover = false; + startTimer(); + }; + + const onPointerDown = (event: PointerEvent) => { + if (!canSlide) return; + + dragging = true; + startX = event.clientX; + deltaX = 0; + viewport.setPointerCapture(event.pointerId); + }; + + const onPointerMove = (event: PointerEvent) => { + if (!dragging) return; + deltaX = event.clientX - startX; + }; + + const onPointerUp = () => { + if (!dragging) return; + + dragging = false; + + if (deltaX > 50) goTo(page - 1); + if (deltaX < -50) goTo(page + 1); + + deltaX = 0; + startTimer(); + }; + + viewport.addEventListener("mouseenter", onMouseEnter); + viewport.addEventListener("mouseleave", onMouseLeave); + viewport.addEventListener("pointerdown", onPointerDown); + viewport.addEventListener("pointermove", onPointerMove); + viewport.addEventListener("pointerup", onPointerUp); + viewport.addEventListener("pointercancel", onPointerUp); + + cleanups.push(() => { + stopTimer(); + viewport.removeEventListener("mouseenter", onMouseEnter); + viewport.removeEventListener("mouseleave", onMouseLeave); + viewport.removeEventListener("pointerdown", onPointerDown); + viewport.removeEventListener("pointermove", onPointerMove); + viewport.removeEventListener("pointerup", onPointerUp); + viewport.removeEventListener("pointercancel", onPointerUp); + dotButtons.forEach((button) => button.remove()); + }); + + render(); + startTimer(); + }); + + return () => { + cleanups.forEach((cleanup) => cleanup()); + }; + }, []); + + const contentsHtml = ` +
+
+

+ 중수도 사례 +  

- -
-
- 경기도 안산시 - +
+
+
+   +

경기도 안산시

+
- +
+
+
+ +   +   + +

사업명

+
+
    +
  • 안산시 중수도 설치사업
  • +
+
+ +
+
+ +   +   + +

사업목적

+
+
    +
  • 수자원을 효율적으로 활용하여 지속가능성 도모 및 물 절약 실현
  • +
+
+ +
+
+ +   +   + +

사업 주요 내용

+
+
    +
  • 총사업비 : 81억원
  • +
  • 처리공법 : MBLM공법
  • +
+
+ +
+
+ +   +   + +

사업량

+
+
+ + + + + + + + + + + + + + + +
시설명이용용도공급량(㎥)
경기테크노파크청소·화장실용수40㎥/일
올림픽수영장청소·화장실/세척·살수용수35㎥/일
와스타디움청소·화장실/조경용수90㎥/일
호수실내수영장청소·화장실/조경/세척·살수용수50㎥/일
한양대학교 에리카캠퍼스청소·화장실용수370㎥/일
+
+
+
-
+
-
- 경기도 용인시 +
+
+   +

경기도 용인시

+
- + +
+
+
+ +   +   + +

사업명

+
+
    +
  • 용인시 중수도 설치사업
  • +
+
- +
+
+ +   +   + +

사업목적

+
+
    +
  • 물 부족 등의 비상상황에 대한 대비, 하수라도 사용할 집결 등 수자원의 효율적 활용
  • +
+
+ +
+
+ +   +   + +

사업 주요 내용

+
+
    +
  • 총사업비 : 40억원
  • +
  • 처리공법 : MBLM공법
  • +
+
+ +
+
+ +   +   + +

사업량

+
+
+ + + + + + + + + + + + + +
시설명이용용도공급량(㎥)
여성회관청소·화장실용수35㎥/일
수지아르피아청소·화장실용수35㎥/일
지상작전사령부청소·화장실/조경/세척·살수용수53㎥/일
+
+
+
-
+
-
- 경기도 평택시 +
+
+   +

경기도 평택시

+
- + +
+
+
+ +   +   + +

사업명

+
+
    +
  • 평택시 중수도 설치사업
  • +
+
- +
+
+ +   +   + +

사업목적

+
+
    +
  • 수자원을 효율적으로 활용하여 지속가능성 도모 및 물 절약 실현
  • +
+
+ +
+
+ +   +   + +

사업 주요 내용

+
+
    +
  • 총사업비 : 21억원
  • +
  • 처리공법 : MBLM공법
  • +
+
+ +
+
+ +   +   + +

사업량

+
+
+ + + + + + + + + + + +
시설명이용용도공급량(㎥)
평택대학교청소·화장실용수50㎥/일
+
+
+
+ `; + + return ( +
); -} \ No newline at end of file +} diff --git a/apps/dm/app/(with-layout)/example/_components/RainExampleSection.tsx b/apps/dm/app/(with-layout)/example/_components/RainExampleSection.tsx index 5f975356a..483263c82 100644 --- a/apps/dm/app/(with-layout)/example/_components/RainExampleSection.tsx +++ b/apps/dm/app/(with-layout)/example/_components/RainExampleSection.tsx @@ -1,225 +1,382 @@ "use client"; -import CaseImageSlider from "@/components/CaseImageSlider"; - -function SectionTitle({ children }: { children: React.ReactNode }) { - return ( -
- -

{children}

-
- ); -} - -function BlockTitle({ children }: { children: React.ReactNode }) { - return ( -
- - - - -

{children}

-
- ); -} - -function Hr({ className = "" }: { className?: string }) { - return
; -} - -function InfoList({ - projectName, - purpose, - details, -}: { - projectName: string; - purpose: string; - details: string[]; -}) { - return ( -
-
- 사업명 -
    -
  • {projectName}
  • -
-
- -
- 사업목적 -
    -
  • {purpose}
  • -
-
- -
- 사업 주요 내용 -
    - {details.map((item, index) => ( -
  • {item}
  • - ))} -
-
-
- ); -} - -type RainTableRow = { - facility: string; - storage: string; - supply: string; - pipe: string; -}; - -function RainTable({ rows }: { rows: RainTableRow[] }) { - return ( -
- - - - - - - - - - - - {rows.map((row, index) => ( - - - - - {index === 0 && ( - - )} - - - - ))} - -
- 시설명 - - 저류조용량(㎥) - - 빗물공급기 - - 빗물분사장치(m) -
- {row.facility} - - {row.storage} - - {row.supply} - - {row.pipe} -
-
- ); -} +import { useEffect, useRef } from "react"; export default function RainExampleSection() { - const title = "빗물이용시설 사례"; + const rootRef = useRef(null); - return ( -
-
-

- {title} - + useEffect(() => { + const root = rootRef.current; + if (!root) return; + + const cleanups: Array<() => void> = []; + const sliders = Array.from(root.querySelectorAll(".image-slider")); + + sliders.forEach((sliderElement) => { + const slider = sliderElement as HTMLElement; + const viewport = slider.querySelector(".image-slider-viewport") as HTMLElement | null; + const track = slider.querySelector(".image-slider-track") as HTMLElement | null; + const dotsWrap = slider.querySelector(".image-slider-dots") as HTMLElement | null; + const pages = Array.from( + slider.querySelectorAll(".image-slider-page"), + ) as HTMLElement[]; + + if (!viewport || !track || !dotsWrap || pages.length === 0) return; + + let page = 0; + let dragging = false; + let isHover = false; + let startX = 0; + let deltaX = 0; + let timer: number | null = null; + + const autoPlayMs = Number(slider.dataset.autoplay ?? "4500"); + const canSlide = pages.length > 1; + + if (!canSlide) { + dotsWrap.style.display = "none"; + } else { + dotsWrap.style.display = ""; + } + + const render = () => { + track.style.transform = `translateX(-${page * 100}%)`; + + Array.from(dotsWrap.querySelectorAll("button")).forEach((dot, index) => { + dot.classList.toggle("is-active", index === page); + }); + }; + + const goTo = (nextPage: number) => { + if (!canSlide) return; + + if (nextPage < 0) page = pages.length - 1; + else if (nextPage >= pages.length) page = 0; + else page = nextPage; + + render(); + }; + + const stopTimer = () => { + if (timer !== null) { + window.clearInterval(timer); + timer = null; + } + }; + + const startTimer = () => { + stopTimer(); + if (!canSlide || dragging || isHover) return; + + timer = window.setInterval(() => { + page = page + 1 >= pages.length ? 0 : page + 1; + render(); + }, autoPlayMs); + }; + + const dotButtons = pages.map((_, index) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "image-slider-dot"; + + const onClick = () => { + goTo(index); + startTimer(); + }; + + button.addEventListener("click", onClick); + dotsWrap.appendChild(button); + + cleanups.push(() => { + button.removeEventListener("click", onClick); + }); + + return button; + }); + + const onMouseEnter = () => { + isHover = true; + stopTimer(); + }; + + const onMouseLeave = () => { + isHover = false; + startTimer(); + }; + + const onPointerDown = (event: PointerEvent) => { + if (!canSlide) return; + + dragging = true; + startX = event.clientX; + deltaX = 0; + viewport.setPointerCapture(event.pointerId); + }; + + const onPointerMove = (event: PointerEvent) => { + if (!dragging) return; + deltaX = event.clientX - startX; + }; + + const onPointerUp = () => { + if (!dragging) return; + + dragging = false; + + if (deltaX > 50) goTo(page - 1); + if (deltaX < -50) goTo(page + 1); + + deltaX = 0; + startTimer(); + }; + + viewport.addEventListener("mouseenter", onMouseEnter); + viewport.addEventListener("mouseleave", onMouseLeave); + viewport.addEventListener("pointerdown", onPointerDown); + viewport.addEventListener("pointermove", onPointerMove); + viewport.addEventListener("pointerup", onPointerUp); + viewport.addEventListener("pointercancel", onPointerUp); + + cleanups.push(() => { + stopTimer(); + viewport.removeEventListener("mouseenter", onMouseEnter); + viewport.removeEventListener("mouseleave", onMouseLeave); + viewport.removeEventListener("pointerdown", onPointerDown); + viewport.removeEventListener("pointermove", onPointerMove); + viewport.removeEventListener("pointerup", onPointerUp); + viewport.removeEventListener("pointercancel", onPointerUp); + dotButtons.forEach((button) => button.remove()); + }); + + render(); + startTimer(); + }); + + return () => { + cleanups.forEach((cleanup) => cleanup()); + }; + }, []); + + const contentsHtml = ` +
+
+

+ 빗물이용시설 사례 +  

-
-
- 경기도 수원시 +
+
+
+   +

경기도 수원시

+
- +
+
+
+
+
+
+ 수원시 빗물이용시설 사례 1 +
+
+ 수원시 빗물이용시설 사례 2 +
+
+
+
+ 수원시 빗물이용시설 사례 3 +
+
+ 수원시 빗물이용시설 사례 4 +
+
+
+
+ 수원시 빗물이용시설 사례 5 +
+
+
+
+
+
+
- +
+
+
+ +   +   + +

사업명

+
+
    +
  • 수원시 빗물이용시설 설치사업
  • +
+
-
- 사업량 +
+
+ +   +   + +

사업목적

+
+
    +
  • 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물공급기를 설치하여 축서기 도시 열섬효과 및 도로 비산먼지 저감
  • +
+
- +
+
+ +   +   + +

사업 주요 내용

+
+
    +
  • 총사업비 : 20억원
  • +
  • 위치 : 수원시 영통구, 팔달구 및 장안구 일원
  • +
+
+ +
+
+ +   +   + +

사업량

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
시설명저류조용량(㎥)빗물공급기빗물분사장치(m)
광교중학교4,5001대160
수원월드컵경기장12,000451
수원종합운동장10,000231
수원컨벤션센터3,600460
+
+
-
+
-
- 제주특별자치도 +
+
+   +

제주특별자치도

+
- +
+
+
+
+
+
+ 제주특별자치도 빗물이용시설 사례 1 +
+
+ 제주특별자치도 빗물이용시설 사례 2 +
+
+
+
+ 제주특별자치도 빗물이용시설 사례 3 +
+
+
+
+
+
+
- +
+
+
+ +   +   + +

사업명

+
+
    +
  • 제주특별자치도 중규모 빗물이용시설 설치사업
  • +
+
+ +
+
+ +   +   + +

사업목적

+
+
    +
  • 강우시 비닐하우스에서 발생하는 대체수자원인 빗물을 순환 활용하고자 하는 사업으로, 지하수 사용 저감과 집중호우시 도로와 농경지 침수피해 예방
  • +
+
+ +
+
+ +   +   + +

사업 주요 내용

+
+
    +
  • 총사업비 : 278억원
  • +
  • 위치 : 제주특별자치도 서귀포시 남원읍 위미리 일원
  • +
  • 사업량 : 저류조 7,000㎥, 배수갑 500㎡, 관로 21.7km
  • +
  • 사업기간 : 2024. 5. ~ 2028. 12.
  • +
+
+
+ `; + + return ( +
); -} \ No newline at end of file +} diff --git a/apps/dm/app/(with-layout)/example/_components/SewageExampleSection.tsx b/apps/dm/app/(with-layout)/example/_components/SewageExampleSection.tsx index cd4c1d420..266a30060 100644 --- a/apps/dm/app/(with-layout)/example/_components/SewageExampleSection.tsx +++ b/apps/dm/app/(with-layout)/example/_components/SewageExampleSection.tsx @@ -1,367 +1,582 @@ "use client"; -import CaseImageSlider from "@/components/CaseImageSlider"; - -type SewageCase = { - id: string; - title: string; - slides: { - src: string; - alt: string; - }[]; - projectName: string; - purpose: string[]; - method?: string[]; - details: string[]; - note?: string[]; -}; - -function SectionTitle({ children }: { children: React.ReactNode }) { - return ( -
- -

{children}

-
- ); -} - -function BlockTitle({ children }: { children: React.ReactNode }) { - return ( -
- - - - -

{children}

-
- ); -} - -function Hr({ className = "" }: { className?: string }) { - return
; -} - -function InfoBlock({ - title, - items, -}: { - title: string; - items?: string[]; -}) { - if (!items || items.length === 0) return null; - - return ( -
- {title} -
    - {items.map((item, index) => ( -
  • {item}
  • - ))} -
-
- ); -} - -function InfoList({ item }: { item: SewageCase }) { - return ( -
- - - - - -
- ); -} - -const sewageCases: SewageCase[] = [ - { - id: "example-sewage-guri", - title: "경기도 구리시", - slides: [ - { - src: "/images/landing/business/sewage/guri_01.png", - alt: "구리시 도로자동청소시스템", - }, - { - src: "/images/landing/business/sewage/guri_02.png", - alt: "구리시 도로자동청소시스템 위치도", - }, - { - src: "/images/landing/business/sewage/guri_03.png", - alt: "구리시 도로자동청소시스템 위치도2", - }, - { - src: "/images/landing/business/sewage/guri_04.png", - alt: "구리시 도로자동청소시스템 원리", - }, - ], - projectName: "구리시 도로자동청소시스템 설치사업", - purpose: [ - "고농도 미세먼지 발생 및 폭염의 급증에 따라 공공하수처리시설 발생 재이용수 일부를 도로자동청소시스템에 활용하여 미세먼지 저감 및 폭염 취약성 완화", - ], - details: [ - "총사업비 : 61억원", - "위치 : 구리시 경춘로 및 갈매중앙로 일대 등", - "관로 : 5.8km", - "공급량 : 178㎥/일", - ], - }, - { - id: "example-sewage-bucheon", - title: "경기도 부천시", - slides: [ - { - src: "/images/landing/business/sewage/bucheon_01.png", - alt: "부천시 하수 재이용수 생산시설", - }, - { - src: "/images/landing/business/sewage/bucheon_02.png", - alt: "부천시 하수처리수 재이용 사례", - }, - ], - projectName: "부천시 하수 재이용수 생산시설 설치 운영", - purpose: [ - "굴포하수처리시설 하수처리수 재이용수 생산시설 설치 운영을 통해 여월천 등 부천 관내 하천의 건천화 방지, 하천 생태계 보존 및 주민들에게 친수 공간 제공", - ], - details: [ - "총사업비 : 212억원", - "위치 : 부천시 여월천, 삼정천, 심곡천 및 시민의 강", - "관로 : 23.6km", - "공급량 : 50,000㎥/일", - ], - }, - { - id: "example-sewage-suwon", - title: "경기도 수원시", - slides: [ - { - src: "/images/landing/business/sewage/suwon_01.png", - alt: "수원시 황구지천1", - }, - { - src: "/images/landing/business/sewage/suwon_02.png", - alt: "수원시 황구지천2", - }, - { - src: "/images/landing/business/sewage/suwon_03.png", - alt: "수원시 황구지천 지도", - }, - ], - projectName: "수원시 황구지천 하수처리수 재이용사업", - purpose: [ - "황구지천 공공하수처리시설 하수처리수 재이용사업으로 황구 호매실천 및 금곡천 건천화 방지로 주민들에게 친수 공간 제공 및 하천 생태계 보전", - ], - details: [ - "총사업비 : 110억원", - "위치 : 수원시 권선구 호매실천, 금곡천", - "관로 : 3.9km", - "공급량 : 25,000㎥/일(하수처리시설 용량 : 45,000㎥/일)", - ], - }, - { - id: "example-sewage-paju", - title: "경기도 파주시", - slides: [ - { - src: "/images/landing/business/sewage/paju_01.png", - alt: "파주시 하수처리수 시설 전경1", - }, - { - src: "/images/landing/business/sewage/paju_02.png", - alt: "파주시 하수처리수 시설 전경2", - }, - ], - projectName: "파주시 하수처리수 재이용사업", - purpose: [ - "운정 및 금촌하수처리시설의 하수처리수를 막공법으로 재처리하여 파주LCD산업단지 내 3개 업체에 공업용수를 공급(LG디스플레이, 희성전자, 엔비텍)", - "하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례", - ], - method: ["민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)"], - details: [ - "총사업비 : 651억원", - "위치 : 파주시 금촌동 714번지 일원", - "관로 : 13.6km", - "공급량 : 40,000㎥/일(시설용량 : 41,200㎥/일)", - ], - }, - { - id: "example-sewage-pohang", - title: "경상북도 포항시", - slides: [ - { - src: "/images/landing/business/sewage/pohang_01.png", - alt: "포항시 하수처리수 전경1", - }, - { - src: "/images/landing/business/sewage/pohang_02.png", - alt: "포항시 하수처리수 전경2", - }, - { - src: "/images/landing/business/sewage/pohang_03.png", - alt: "포항시 하수처리수 재이용 공급계통", - }, - ], - projectName: "포항시 하수처리수 재이용사업", - purpose: [ - "포항시 내 공단 개발 확대로 지속적이고 안정적인 공업용수 공급 추진(포스코, 동국산업 등)", - ], - method: ["민간투자사업(운영사 : ㈜포웰, 한국수자원공사)"], - details: [ - "총사업비 : 1,400억원", - "위치 : 포항시 남구 상도동 125번지 일원", - "관로 : 11.7km", - "공급량 : 100,000㎥/일(시설용량 : 100,000㎥/일)", - "처리공법 : 전처리시설(U/F) + 주처리시설(R/O)", - ], - }, - { - id: "example-sewage-ulsan", - title: "울산광역시", - slides: [ - { - src: "/images/landing/business/sewage/ulsan_01.png", - alt: "울산광역시 폐수처리수 전경1", - }, - { - src: "/images/landing/business/sewage/ulsan_02.png", - alt: "울산광역시 폐수처리수 전경2", - }, - { - src: "/images/landing/business/sewage/ulsan_03.png", - alt: "울산광역시 폐수처리수 시설1", - }, - { - src: "/images/landing/business/sewage/ulsan_04.png", - alt: "울산광역시 폐수처리수 시설2", - }, - ], - projectName: "울산광역시 폐수처리수 재이용사업", - purpose: [ - "’18년 갈수기시 낙동강 수질 악화로 울산석유화학단지 용수 공급 부족을 해소하기 위해 미포산단 내 한화케미칼 등 8개사 공업용수 공급", - ], - method: ["민간투자사업(시행사 :울산화이텍워터(주))"], - details: [ - "총사업비 : 1,729억원", - "위치 : 울산광역시 울주군 청량읍 상개로 14 일원", - "관로 : 11.7km", - "공급량 : 17,000㎥/일(시설용량 : 86,000㎥/일)", - "처리공법 : 전처리시설(U/F) + 주처리시설(R/O, EDI)", - ], - }, - { - id: "example-sewage-yeosu", - title: "전라남도 여수시", - slides: [ - { - src: "/images/landing/business/sewage/yeosu_01.png", - alt: "여수시 하수처리수 재이용시설1", - }, - { - src: "/images/landing/business/sewage/yeosu_02.png", - alt: "여수시 하수처리수 재이용시설2", - }, - ], - projectName: "여수시 하수처리수 재이용사업", - purpose: [ - "수요자가 원하는 양질의 공업용수를 저렴하게 공급하여 한정된 수자원의 효율적 이용 및 지역적 물 부족 해소, 하수재이용을 통한 오염총량 저감 및 방류수역 수질개선을 위한 여수국가산단 내 공업용수 공급(GS칼텍스, YNCC 등)", - ], - method: ["민간투자사업(시행사 : 여수그린허브(주), 운영사 : 테크로스)"], - details: [ - "총사업비 : 750억원", - "위치 : 여수시 신월로 284 일원", - "관로 : 18.6km", - "공급량 : 50,000㎥/일(시설용량 : 50,000㎥/일)", - "처리공법 : 전처리시설(U/F) + 주처리시설(R/O)", - ], - }, - { - id: "example-sewage-boryeong", - title: "충청남도 보령시", - slides: [ - { - src: "/images/landing/business/sewage/boryeong_01.png", - alt: "보령시 하수처리수 재이용시설1", - }, - { - src: "/images/landing/business/sewage/boryeong_02.png", - alt: "보령시 하수처리수 재이용시설2", - }, - ], - projectName: "보령시 하수처리수 재이용사업", - purpose: [ - "한국중부발전(주)의 보령 및 신보령화력발전소에 10,000㎥/일의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진", - "국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU(기후에너지환경부, 충청남도, 보령시, 한국중부발전(주))를 바탕으로 추진되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모", - ], - method: ["재정사업(2017년 한국환경공단과 사업대행 위·수탁협약 체결)"], - details: [ - "총사업비 : 315억원", - "위치 : 보령시 대천방조제로 322 일원", - "관로 : 8.7km", - "공급량 : 10,000㎥/일(시설용량 : 10,000㎥/일)", - "처리공법 : 전처리시설(U/F) + 주처리시설(R/O)", - ], - note: [ - "국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU(환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모", - ], - }, - { - id: "example-sewage-asan", - title: "충청남도 아산시", - slides: [ - { - src: "/images/landing/business/sewage/asan_01.png", - alt: "아산시 하수처리수 재이용시설", - }, - { - src: "/images/landing/business/sewage/asan_02.png", - alt: "아산시 하수처리수 재이용 공정도", - }, - ], - projectName: "아산시 하수처리수 재이용사업", - purpose: [ - "신도시 개발에 따른 인구 급증으로 발생하는 하수를 재처리하여 탕정산업단지(삼성디스플레이)에 공업용수를 안정적으로 공급함으로써 지역기반 및 활성화를 도모", - ], - method: ["민간투자사업(시행사 : 아산스마트원터, 운영사 : 한국수자원공사)"], - details: [ - "총사업비 : 297억원", - "위치 : 아산시 탕정면 갈산리 807 일원", - "관로 : 4.2km", - "공급량 : 27,000㎥/일(시설용량 : 45,000㎥/일)", - "처리공법 : 전처리시설(U/F) + 주처리시설(R/O)", - ], - }, -]; +import { useEffect, useRef } from "react"; export default function SewageExampleSection() { - const title = "하수처리수 재이용 사례"; + const rootRef = useRef(null); - return ( -
-
-

- {title} - + useEffect(() => { + const root = rootRef.current; + if (!root) return; + + const cleanups: Array<() => void> = []; + const sliders = Array.from(root.querySelectorAll(".image-slider")); + + sliders.forEach((sliderElement) => { + const slider = sliderElement as HTMLElement; + const viewport = slider.querySelector( + ".image-slider-viewport", + ) as HTMLElement | null; + const track = slider.querySelector(".image-slider-track") as HTMLElement | null; + const dotsWrap = slider.querySelector( + ".image-slider-dots", + ) as HTMLElement | null; + const pages = Array.from( + slider.querySelectorAll(".image-slider-page"), + ) as HTMLElement[]; + + if (!viewport || !track || !dotsWrap || pages.length === 0) return; + + let page = 0; + let dragging = false; + let isHover = false; + let startX = 0; + let deltaX = 0; + let timer: number | null = null; + + const autoPlayMs = Number(slider.dataset.autoplay ?? "4500"); + const canSlide = pages.length > 1; + + if (!canSlide) { + dotsWrap.style.display = "none"; + } else { + dotsWrap.style.display = ""; + } + + const render = () => { + track.style.transform = `translateX(-${page * 100}%)`; + + Array.from(dotsWrap.querySelectorAll("button")).forEach((dot, index) => { + dot.classList.toggle("is-active", index === page); + }); + }; + + const goTo = (nextPage: number) => { + if (!canSlide) return; + + if (nextPage < 0) page = pages.length - 1; + else if (nextPage >= pages.length) page = 0; + else page = nextPage; + + render(); + }; + + const stopTimer = () => { + if (timer !== null) { + window.clearInterval(timer); + timer = null; + } + }; + + const startTimer = () => { + stopTimer(); + if (!canSlide || dragging || isHover) return; + + timer = window.setInterval(() => { + page = page + 1 >= pages.length ? 0 : page + 1; + render(); + }, autoPlayMs); + }; + + const dotButtons = pages.map((_, index) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "image-slider-dot"; + + const onClick = () => { + goTo(index); + startTimer(); + }; + + button.addEventListener("click", onClick); + dotsWrap.appendChild(button); + + cleanups.push(() => { + button.removeEventListener("click", onClick); + }); + + return button; + }); + + const onMouseEnter = () => { + isHover = true; + stopTimer(); + }; + + const onMouseLeave = () => { + isHover = false; + startTimer(); + }; + + const onPointerDown = (event: PointerEvent) => { + if (!canSlide) return; + + dragging = true; + startX = event.clientX; + deltaX = 0; + viewport.setPointerCapture(event.pointerId); + }; + + const onPointerMove = (event: PointerEvent) => { + if (!dragging) return; + deltaX = event.clientX - startX; + }; + + const onPointerUp = () => { + if (!dragging) return; + + dragging = false; + + if (deltaX > 50) goTo(page - 1); + if (deltaX < -50) goTo(page + 1); + + deltaX = 0; + startTimer(); + }; + + viewport.addEventListener("mouseenter", onMouseEnter); + viewport.addEventListener("mouseleave", onMouseLeave); + viewport.addEventListener("pointerdown", onPointerDown); + viewport.addEventListener("pointermove", onPointerMove); + viewport.addEventListener("pointerup", onPointerUp); + viewport.addEventListener("pointercancel", onPointerUp); + + cleanups.push(() => { + stopTimer(); + viewport.removeEventListener("mouseenter", onMouseEnter); + viewport.removeEventListener("mouseleave", onMouseLeave); + viewport.removeEventListener("pointerdown", onPointerDown); + viewport.removeEventListener("pointermove", onPointerMove); + viewport.removeEventListener("pointerup", onPointerUp); + viewport.removeEventListener("pointercancel", onPointerUp); + dotButtons.forEach((button) => button.remove()); + }); + + render(); + startTimer(); + }); + + return () => { + cleanups.forEach((cleanup) => cleanup()); + }; + }, []); + + const contentsHtml = ` +
+
+

+ 하수처리수 재이용 사례 +  

-
- {sewageCases.map((item, index) => ( -
- {item.title} +
+
+
+   +

경기도 구리시

+
- +
+
+
+
+
+
구리시 도로자동청소시스템
+
구리시 도로자동청소시스템 위치도
+
+
+
구리시 도로자동청소시스템 위치도2
+
구리시 도로자동청소시스템 원리
+
+
+
+
+
+
- +
+
+
  

사업명

+
  • 구리시 도로자동청소시스템 설치사업
+
+
+
  

사업목적

+
  • 고농도 미세먼지 발생 및 폭염의 급증에 따라 공공하수처리시설 발생 재이용수 일부를 도로자동청소시스템에 활용하여 미세먼지 저감 및 폭염 취약성 완화
+
+
+
  

사업 주요 내용

+
    +
  • 총사업비 : 61억원
  • +
  • 위치 : 구리시 경춘로 및 갈매중앙로 일대 등
  • +
  • 관로 : 5.8km
  • +
  • 공급량 : 178㎥/일
  • +
+
+
+
- {index !== sewageCases.length - 1 &&
} -
- ))} +
+ +
+
 

경기도 부천시

+
+
+
+
+
+
부천시 하수 재이용수 생산시설
+
부천시 하수처리수 재이용 사례
+
+
+
+
+
+
+
+
+
  

사업명

+
  • 부천시 하수 재이용수 생산시설 설치 운영
+
+
+
  

사업목적

+
  • 굴포하수처리시설 하수처리수 재이용수 생산시설 설치 운영을 통해 여월천 등 부천 관내 하천의 건천화 방지, 하천 생태계 보존 및 주민들에게 친수 공간 제공
+
+
+
  

사업 주요 내용

+
    +
  • 총사업비 : 212억원
  • +
  • 위치 : 부천시 여월천, 삼정천, 심곡천 및 시민의 강
  • +
  • 관로 : 23.6km
  • +
  • 공급량 : 50,000㎥/일
  • +
+
+
+
+ +
+ +
+
 

경기도 수원시

+
+
+
+
+
+
수원시 황구지천1
+
수원시 황구지천2
+
+
+
수원시 황구지천 지도
+
+
+
+
+
+
+
+
+
  

사업명

+
  • 수원시 황구지천 하수처리수 재이용사업
+
+
+
  

사업목적

+
  • 황구지천 공공하수처리시설 하수처리수 재이용사업으로 황구 호매실천 및 금곡천 건천화 방지로 주민들에게 친수 공간 제공 및 하천 생태계 보전
+
+
+
  

사업 주요 내용

+
    +
  • 총사업비 : 110억원
  • +
  • 위치 : 수원시 권선구 호매실천, 금곡천
  • +
  • 관로 : 3.9km
  • +
  • 공급량 : 25,000㎥/일(하수처리시설 용량 : 45,000㎥/일)
  • +
+
+
+
+ +
+ +
+
 

경기도 파주시

+
+
+
+
+
+
파주시 하수처리수 시설 전경1
+
파주시 하수처리수 시설 전경2
+
+
+
+
+
+
+
+
+
  

사업명

+
  • 파주시 하수처리수 재이용사업
+
+
+
  

사업목적

+
    +
  • 운정 및 금촌하수처리시설의 하수처리수를 막공법으로 재처리하여 파주LCD산업단지 내 3개 업체에 공업용수를 공급(LG디스플레이, 희성전자, 엔비텍)
  • +
  • 하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례
  • +
+
+
+
  

사업방식

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

사업 주요 내용

+
    +
  • 총사업비 : 651억원
  • +
  • 위치 : 파주시 금촌동 714번지 일원
  • +
  • 관로 : 13.6km
  • +
  • 공급량 : 40,000㎥/일(시설용량 : 41,200㎥/일)
  • +
+
+
+
+ +
+ +
+
 

경상북도 포항시

+
+
+
+
+
+
포항시 하수처리수 전경1
+
포항시 하수처리수 전경2
+
+
+
포항시 하수처리수 재이용 공급계통
+
+
+
+
+
+
+
+
+
  

사업명

+
  • 포항시 하수처리수 재이용사업
+
+
+
  

사업목적

+
  • 포항시 내 공단 개발 확대로 지속적이고 안정적인 공업용수 공급 추진(포스코, 동국산업 등)
+
+
+
  

사업방식

+
  • 민간투자사업(운영사 : ㈜포웰, 한국수자원공사)
+
+
+
  

사업 주요 내용

+
    +
  • 총사업비 : 1,400억원
  • +
  • 위치 : 포항시 남구 상도동 125번지 일원
  • +
  • 관로 : 11.7km
  • +
  • 공급량 : 100,000㎥/일(시설용량 : 100,000㎥/일)
  • +
  • 처리공법 : 전처리시설(U/F) + 주처리시설(R/O)
  • +
+
+
+
+ +
+ +
+
 

울산광역시

+
+
+
+
+
+
울산광역시 폐수처리수 전경1
+
울산광역시 폐수처리수 전경2
+
+
+
울산광역시 폐수처리수 시설1
+
울산광역시 폐수처리수 시설2
+
+
+
+
+
+
+
+
+
  

사업명

+
  • 울산광역시 폐수처리수 재이용사업
+
+
+
  

사업목적

+
  • ’18년 갈수기시 낙동강 수질 악화로 울산석유화학단지 용수 공급 부족을 해소하기 위해 미포산단 내 한화케미칼 등 8개사 공업용수 공급
+
+
+
  

사업방식

+
  • 민간투자사업(시행사 :울산화이텍워터(주))
+
+
+
  

사업 주요 내용

+
    +
  • 총사업비 : 1,729억원
  • +
  • 위치 : 울산광역시 울주군 청량읍 상개로 14 일원
  • +
  • 관로 : 11.7km
  • +
  • 공급량 : 17,000㎥/일(시설용량 : 86,000㎥/일)
  • +
  • 처리공법 : 전처리시설(U/F) + 주처리시설(R/O, EDI)
  • +
+
+
+
+ +
+ +
+
 

전라남도 여수시

+
+
+
+
+
+
여수시 하수처리수 재이용시설1
+
여수시 하수처리수 재이용시설2
+
+
+
+
+
+
+
+
+
  

사업명

+
  • 여수시 하수처리수 재이용사업
+
+
+
  

사업목적

+
  • 수요자가 원하는 양질의 공업용수를 저렴하게 공급하여 한정된 수자원의 효율적 이용 및 지역적 물 부족 해소, 하수재이용을 통한 오염총량 저감 및 방류수역 수질개선을 위한 여수국가산단 내 공업용수 공급(GS칼텍스, YNCC 등)
+
+
+
  

사업방식

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

사업 주요 내용

+
    +
  • 총사업비 : 750억원
  • +
  • 위치 : 여수시 신월로 284 일원
  • +
  • 관로 : 18.6km
  • +
  • 공급량 : 50,000㎥/일(시설용량 : 50,000㎥/일)
  • +
  • 처리공법 : 전처리시설(U/F) + 주처리시설(R/O)
  • +
+
+
+
+ +
+ +
+
 

충청남도 보령시

+
+
+
+
+
+
보령시 하수처리수 재이용시설1
+
보령시 하수처리수 재이용시설2
+
+
+
+
+
+
+
+
+
  

사업명

+
  • 보령시 하수처리수 재이용사업
+
+
+
  

사업목적

+
    +
  • 한국중부발전(주)의 보령 및 신보령화력발전소에 10,000㎥/일의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진
  • +
  • 국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU(기후에너지환경부, 충청남도, 보령시, 한국중부발전(주))를 바탕으로 추진되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모
  • +
+
+
+
  

사업방식

+
  • 재정사업(2017년 한국환경공단과 사업대행 위·수탁협약 체결)
+
+
+
  

사업 주요 내용

+
    +
  • 총사업비 : 315억원
  • +
  • 위치 : 보령시 대천방조제로 322 일원
  • +
  • 관로 : 8.7km
  • +
  • 공급량 : 10,000㎥/일(시설용량 : 10,000㎥/일)
  • +
  • 처리공법 : 전처리시설(U/F) + 주처리시설(R/O)
  • +
+
+
+
  

특이사항

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

충청남도 아산시

+
+
+
+
+
+
아산시 하수처리수 재이용시설
+
아산시 하수처리수 재이용 공정도
+
+
+
+
+
+
+
+
+
  

사업명

+
  • 아산시 하수처리수 재이용사업
+
+
+
  

사업목적

+
  • 신도시 개발에 따른 인구 급증으로 발생하는 하수를 재처리하여 탕정산업단지(삼성디스플레이)에 공업용수를 안정적으로 공급함으로써 지역기반 및 활성화를 도모
+
+
+
  

사업방식

+
  • 민간투자사업(시행사 : 아산스마트원터, 운영사 : 한국수자원공사)
+
+
+
  

사업 주요 내용

+
    +
  • 총사업비 : 297억원
  • +
  • 위치 : 아산시 탕정면 갈산리 807 일원
  • +
  • 관로 : 4.2km
  • +
  • 공급량 : 27,000㎥/일(시설용량 : 45,000㎥/일)
  • +
  • 처리공법 : 전처리시설(U/F) + 주처리시설(R/O)
  • +
+
+
+
+ `; + + return ( +
); -} \ No newline at end of file +} diff --git a/apps/dm/app/(with-layout)/example/exampleSection.tsx b/apps/dm/app/(with-layout)/example/exampleSection.tsx new file mode 100644 index 000000000..42ae60f08 --- /dev/null +++ b/apps/dm/app/(with-layout)/example/exampleSection.tsx @@ -0,0 +1,8 @@ +import { ContentsPage } from "@/components/ui/contents"; + +export default function ExampleSection() { + const contsSn = Number(process.env.CONTENTS_EXAMPLE ?? 32); + const menuCd = "24"; + + return ; +} diff --git a/apps/dm/app/(with-layout)/example/page.tsx b/apps/dm/app/(with-layout)/example/page.tsx index 94e908d81..0bcd8f1de 100644 --- a/apps/dm/app/(with-layout)/example/page.tsx +++ b/apps/dm/app/(with-layout)/example/page.tsx @@ -1,13 +1,15 @@ /** * 물 재이용 사례 */ +export const dynamic = "force-dynamic"; import SubPageHeader from "@/components/SubPageHeader"; import BusinessSubNav, { type BusinessSubNavMenu } from "@/components/BusinessSubNav"; -import RainExampleSection from "./_components/RainExampleSection"; -import GreywaterExampleSection from "./_components/GreywaterExampleSection"; -import SewageExampleSection from "./_components/SewageExampleSection"; -import GlobalCaseSection from "./_components/GlobalCaseSection"; +import ExampleSection from "./exampleSection"; +// import RainExampleSection from "./_components/RainExampleSection"; +// import GreywaterExampleSection from "./_components/GreywaterExampleSection"; +// import SewageExampleSection from "./_components/SewageExampleSection"; +// import GlobalCaseSection from "./_components/GlobalCaseSection"; const menus: BusinessSubNavMenu[] = [ { @@ -78,11 +80,12 @@ export default function ExamplePage() {
- + + {/* - + */}
); -} \ No newline at end of file +} diff --git a/apps/dm/app/(with-layout)/intro/_components/LawIntroSection.tsx b/apps/dm/app/(with-layout)/intro/_components/LawIntroSection.tsx index 58efdd908..0ca6ccf69 100644 --- a/apps/dm/app/(with-layout)/intro/_components/LawIntroSection.tsx +++ b/apps/dm/app/(with-layout)/intro/_components/LawIntroSection.tsx @@ -6,14 +6,14 @@ export default function LawIntroSection() {

물재이용법 - +  

- +  

법률 제·개정 및 기본계획 수립

@@ -25,7 +25,7 @@ export default function LawIntroSection() {
- +  

제2차 계획 대비 실적 현황

@@ -37,7 +37,7 @@ export default function LawIntroSection() {
- +  

우리나라의 수자원 현황 및 물이용 여건

@@ -49,7 +49,7 @@ export default function LawIntroSection() {
- +  

물 재이용 정책 변화 추이

@@ -61,7 +61,7 @@ export default function LawIntroSection() {
- +  

타법령과의 관계 검토

@@ -73,7 +73,7 @@ export default function LawIntroSection() {
- +  

주요 국가계획의 정책방향과 연계한 물 재이용 정책 필요

diff --git a/apps/dm/app/contents-page.css b/apps/dm/app/contents-page.css index ce14baf22..0bc23308b 100644 --- a/apps/dm/app/contents-page.css +++ b/apps/dm/app/contents-page.css @@ -85,3 +85,481 @@ margin: 2.75rem 0; background: rgba(0, 0, 0, 0.1); } + +.contents-page .rain-example-sections { + padding-bottom: 120px; +} + +.contents-page .image-slider { + margin-top: 2rem; +} + +.contents-page .image-slider-inner { + max-width: 1240px; + margin: 0 auto; +} + +.contents-page .image-slider-viewport { + overflow: hidden; + user-select: none; + touch-action: pan-y; + cursor: grab; +} + +.contents-page .image-slider-viewport:active { + cursor: grabbing; +} + +.contents-page .image-slider-track { + display: flex; + transition: transform 0.5s ease-out; +} + +.contents-page .image-slider-page { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.25rem; + min-width: 100%; +} + +.contents-page .image-slide { + position: relative; + height: 340px; + overflow: hidden; + border-radius: 20px; + background: #f5f5f5; +} + +.contents-page .image-slide img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.contents-page .image-slider-dots { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + margin-top: 1.25rem; +} + +.contents-page .image-slider-dot { + width: 8px; + height: 8px; + border: 0; + border-radius: 9999px; + background: #d8dde3; + transition: all 0.2s ease; + cursor: pointer; +} + +.contents-page .image-slider-dot.is-active { + background: #18bceb; +} + +@media (max-width: 768px) { + .contents-page .image-slider-page { + grid-template-columns: 1fr; + } + + .contents-page .image-slide { + height: 280px; + } +} + +.contents-page .content-group { + margin-top: 2.5rem; + padding: 0 2.5rem; +} + +.contents-page .content-block + .content-block { + margin-top: 2.25rem; +} + +.contents-page .block-title { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.contents-page .block-title-icon { + position: relative; + display: inline-block; + width: 27px; + height: 16px; + flex-shrink: 0; +} + +.contents-page .block-title-icon .bubble { + position: absolute; + top: 0; + width: 16px; + height: 16px; + border-radius: 9999px; +} + +.contents-page .block-title-icon .bubble-sky { + left: 0; + background: rgba(1, 172, 229, 0.4); +} + +.contents-page .block-title-icon .bubble-green { + left: 7px; + background: rgba(109, 187, 79, 0.4); +} + +.contents-page .block-title h4 { + font-size: 20px; + font-weight: 600; + color: #1a1a1a; +} + +.contents-page .bullet-list { + margin-top: 1rem; + padding-left: 2rem; + color: #4f4f4f; + font-size: 16px; + line-height: 1.8; + list-style: disc; +} + +.contents-page .table-wrap { + margin-top: 2rem; + overflow-x: auto; +} + +.contents-page .contents-table { + width: 100%; + min-width: 860px; + border-collapse: collapse; + border: 1px solid #d8dde3; + text-align: center; +} + +.contents-page .contents-table th, +.contents-page .contents-table td { + border: 1px solid #d8dde3; + padding: 0.75rem 1rem; +} + +.contents-page .contents-table th { + font-size: 18px; + font-weight: 500; + color: #fff; +} + +.contents-page .contents-table td { + background: #fff; + font-size: 16px; + color: #4f4f4f; +} + +.contents-page .contents-table .th-blue { + background: #14a9d9; +} + +.contents-page .contents-table .th-teal { + background: #39b2bd; +} + +.contents-page .contents-table .th-mint { + background: #58b99a; +} + +.contents-page .contents-table .th-green { + background: #70bf4b; +} + +.contents-page .contents-table .th-teal-greywater { + background: #40b5a5; +} + +.contents-page .contents-table th.th-gray, +.contents-page .global-table th.th-gray { + background: #bfc9d2; + color: #222; +} + +.contents-page .contents-table th.th-blue, +.contents-page .global-table th.th-blue { + background: #14a9d9; + color: #fff; +} + +.contents-page .global-table th.th-teal-2 { + background: #55bda5; + color: #fff; +} + +.contents-page .global-table th.th-green-2 { + background: #6ab84e; + color: #fff; +} + +.contents-page .bg-cyan { + background: #eaf7f8 !important; +} + +.contents-page .bg-green { + background: #eaf8e6 !important; +} + +.contents-page .global-case-sections { + padding-bottom: 120px; +} + +.contents-page .greywater-example-sections { + padding-bottom: 120px; +} + +.contents-page .sewage-example-sections { + padding-bottom: 120px; +} + +.contents-page .global-content-group { + gap: 0; +} + +.contents-page .content-rich-list { + margin-top: 1rem; + color: #4f4f4f; + font-size: 16px; + line-height: 1.8; +} + +.contents-page .content-rich-list li + li { + margin-top: 0.5rem; +} + +.contents-page .content-rich-list .depth-0 { + margin-left: 1.5rem; + padding-left: 0.25rem; + list-style: disc; + font-weight: 600; + color: #555; +} + +.contents-page .content-rich-list .depth-1 { + margin-left: 2.125rem; + list-style: none; + color: #777; +} + +.contents-page .content-rich-list .depth-1::before { + content: "- "; +} + +.contents-page .content-rich-list .depth-2 { + margin-left: 3.25rem; + list-style: none; + color: #666; +} + +.contents-page .content-rich-list .depth-3 { + margin-left: 3.5rem; + list-style: none; + color: #777; +} + +.contents-page .global-notice-box, +.contents-page .global-description-box { + max-width: 1000px; + margin: 1.25rem auto 0; + border-radius: 20px; + background: #eaf7f8; +} + +.contents-page .global-notice-box { + padding: 1.75rem 2rem; +} + +.contents-page .global-description-box { + padding: 1.5rem 1.75rem; +} + +.contents-page .global-notice-row { + display: flex; + align-items: flex-start; + gap: 0.5rem; +} + +.contents-page .global-water-bullet { + width: 27px; + height: 33px; + margin-top: 0.25rem; + flex-shrink: 0; +} + +.contents-page .global-notice-title { + display: block; + font-size: 26px; + font-weight: 800; + color: #007fa4; +} + +.contents-page .global-text-dark { + color: #1a1a1a; +} + +.contents-page .global-plain-list { + margin-top: 1.25rem; + padding-left: 1.25rem; + list-style: disc; + color: #555; + font-size: 16px; + line-height: 1.7; +} + +.contents-page .global-plain-list li + li { + margin-top: 0.5rem; +} + +.contents-page .global-table-wrap { + overflow-x: auto; +} + +.contents-page .global-overview-table-wrap { + max-width: 1000px; + margin: 1.25rem auto 0; +} + +.contents-page .global-table { + width: 100%; + text-align: center; + border-collapse: collapse; +} + +.contents-page .global-table th { + padding: 0.75rem 1rem; + font-size: 17px; + font-weight: 700; + color: #fff; +} + +.contents-page .overview-compare-table { + min-width: 760px; +} + +.contents-page .overview-compare-table th { + padding-top: 1rem; + padding-bottom: 1rem; + font-size: 18px; + font-weight: 500; + border-top-left-radius: 10px; + border-top-right-radius: 10px; +} + +.contents-page .overview-compare-table td { + border: 1px solid #aeb9c2; + padding: 1rem; + font-size: 28px; + font-weight: 700; + color: #4f4f4f; + background: #fff; +} + +.contents-page .overview-compare-table .country-cell { + font-size: 20px; + color: #333; +} + +.contents-page .country-wrap { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; +} + +.contents-page .country-flag { + width: 42px; + height: 28px; + object-fit: cover; +} + +.contents-page .reuse-rate-table { + min-width: 960px; +} + +.contents-page .reuse-rate-table th { + border-top-left-radius: 10px; + border-top-right-radius: 10px; +} + +.contents-page .reuse-rate-table td { + border: 1px solid #aeb9c2; + padding: 0.75rem 1rem; + font-size: 15px; + color: #4f4f4f; + background: #fff; +} + +.contents-page .reuse-rate-table .cell-strong { + font-weight: 600; +} + +.contents-page .info-card-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1.25rem; + margin-top: 2rem; +} + +.contents-page .info-card { + border: 2px solid #01ace5; + border-radius: 8px; + background: #eaf5f5; + padding: 1.25rem; +} + +.contents-page .info-card strong { + display: block; + font-size: 20px; + font-weight: 700; + color: #333; +} + +.contents-page .info-card p { + margin-top: 0.75rem; + font-size: 15px; + line-height: 1.6; + color: #555; +} + +.contents-page .global-table-padding { + margin-top: 2rem; + padding: 0 120px; +} + +.contents-page .global-price-table td { + background: #fff; +} + +.contents-page .global-accent { + color: #01ace5; +} + +@media (max-width: 1200px) { + .contents-page .info-card-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .contents-page .global-table-padding { + padding: 0; + } +} + +@media (max-width: 768px) { + .contents-page .global-notice-row { + flex-direction: column; + } + + .contents-page .global-notice-title { + font-size: 22px; + } + + .contents-page .info-card-grid { + grid-template-columns: 1fr; + } +} diff --git a/apps/dm/components/ui/contents/contentsPageClient.tsx b/apps/dm/components/ui/contents/contentsPageClient.tsx index 6628178ef..4fe355dea 100644 --- a/apps/dm/components/ui/contents/contentsPageClient.tsx +++ b/apps/dm/components/ui/contents/contentsPageClient.tsx @@ -1,5 +1,7 @@ "use client"; +import { useEffect, useRef } from "react"; + import type { ContentsDetail } from "@/src/entities/content/model/contents-interface"; type ContentsPageClientProps = { @@ -7,9 +9,170 @@ type ContentsPageClientProps = { }; function ContentsPageClient({ initialContent }: ContentsPageClientProps) { + const rootRef = useRef(null); + + useEffect(() => { + const root = rootRef.current; + if (!root) return; + + const cleanups: Array<() => void> = []; + const sliders = Array.from(root.querySelectorAll(".image-slider")); + + sliders.forEach((sliderElement) => { + const slider = sliderElement as HTMLElement; + const viewport = slider.querySelector( + ".image-slider-viewport", + ) as HTMLElement | null; + const track = slider.querySelector( + ".image-slider-track", + ) as HTMLElement | null; + const dotsWrap = slider.querySelector( + ".image-slider-dots", + ) as HTMLElement | null; + const pages = Array.from( + slider.querySelectorAll(".image-slider-page"), + ) as HTMLElement[]; + + if (!viewport || !track || !dotsWrap || pages.length === 0) return; + + let page = 0; + let dragging = false; + let isHover = false; + let startX = 0; + let deltaX = 0; + let timer: number | null = null; + + const autoPlayMs = Number(slider.dataset.autoplay ?? "4500"); + const canSlide = pages.length > 1; + + if (!canSlide) { + dotsWrap.style.display = "none"; + } else { + dotsWrap.style.display = ""; + } + + const render = () => { + track.style.transform = `translateX(-${page * 100}%)`; + + Array.from(dotsWrap.querySelectorAll("button")).forEach((dot, index) => { + dot.classList.toggle("is-active", index === page); + }); + }; + + const goTo = (nextPage: number) => { + if (!canSlide) return; + + if (nextPage < 0) page = pages.length - 1; + else if (nextPage >= pages.length) page = 0; + else page = nextPage; + + render(); + }; + + const stopTimer = () => { + if (timer !== null) { + window.clearInterval(timer); + timer = null; + } + }; + + const startTimer = () => { + stopTimer(); + if (!canSlide || dragging || isHover) return; + + timer = window.setInterval(() => { + page = page + 1 >= pages.length ? 0 : page + 1; + render(); + }, autoPlayMs); + }; + + const dotButtons = pages.map((_, index) => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "image-slider-dot"; + + const onClick = () => { + goTo(index); + startTimer(); + }; + + button.addEventListener("click", onClick); + dotsWrap.appendChild(button); + + cleanups.push(() => { + button.removeEventListener("click", onClick); + }); + + return button; + }); + + const onMouseEnter = () => { + isHover = true; + stopTimer(); + }; + + const onMouseLeave = () => { + isHover = false; + startTimer(); + }; + + const onPointerDown = (event: PointerEvent) => { + if (!canSlide) return; + + dragging = true; + startX = event.clientX; + deltaX = 0; + viewport.setPointerCapture(event.pointerId); + }; + + const onPointerMove = (event: PointerEvent) => { + if (!dragging) return; + deltaX = event.clientX - startX; + }; + + const onPointerUp = () => { + if (!dragging) return; + + dragging = false; + + if (deltaX > 50) goTo(page - 1); + if (deltaX < -50) goTo(page + 1); + + deltaX = 0; + startTimer(); + }; + + viewport.addEventListener("mouseenter", onMouseEnter); + viewport.addEventListener("mouseleave", onMouseLeave); + viewport.addEventListener("pointerdown", onPointerDown); + viewport.addEventListener("pointermove", onPointerMove); + viewport.addEventListener("pointerup", onPointerUp); + viewport.addEventListener("pointercancel", onPointerUp); + + cleanups.push(() => { + stopTimer(); + viewport.removeEventListener("mouseenter", onMouseEnter); + viewport.removeEventListener("mouseleave", onMouseLeave); + viewport.removeEventListener("pointerdown", onPointerDown); + viewport.removeEventListener("pointermove", onPointerMove); + viewport.removeEventListener("pointerup", onPointerUp); + viewport.removeEventListener("pointercancel", onPointerUp); + dotButtons.forEach((button) => button.remove()); + }); + + render(); + startTimer(); + }); + + return () => { + cleanups.forEach((cleanup) => cleanup()); + }; + }, [initialContent?.contsCn]); + return (