Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5f4f6ddd0 | ||
|
|
f332d64aa6 | ||
|
|
0d48a0b579 | ||
|
|
fc3ef2c59e | ||
|
|
21e0ea086e | ||
|
|
f50a9aaf3a | ||
|
|
60e73212e8 | ||
|
|
6660c85150 | ||
|
|
2cfd57fdc3 | ||
|
|
5e5411a34f | ||
|
|
01852c1528 | ||
|
|
4488bd23c7 | ||
|
|
c2696c6eeb | ||
|
|
4ef86a3e6a | ||
|
|
badea83e9e | ||
|
|
fc2f119584 | ||
|
|
23a92ec38c | ||
|
|
268c45ed05 | ||
|
|
fe2f3faee4 | ||
|
|
3e5a14fbc8 | ||
|
|
e891702da2 | ||
|
|
527b4293c4 | ||
|
|
4e2fe7aaa1 | ||
|
|
0a109111cb | ||
|
|
3b6e8ceedb | ||
|
|
99229a8453 | ||
|
|
383e5f8382 | ||
|
|
a11570b563 | ||
|
|
af1feb3920 | ||
|
|
a6df8d261a | ||
|
|
f96942beee | ||
|
|
7994698349 | ||
|
|
ab495c6572 | ||
|
|
804688b3c1 | ||
|
|
47c986d517 | ||
|
|
8435096e5a | ||
|
|
425eb39a86 | ||
|
|
e30a7d184b | ||
|
|
1ec1c42e9f | ||
|
|
b4e3d922b1 | ||
|
|
a028b6ae8b | ||
|
|
9565931042 | ||
|
|
1042391702 | ||
|
|
68f468fa2c | ||
|
|
fb9f2a9d4a | ||
|
|
7c2ff7c037 | ||
|
|
ad0803f405 | ||
|
|
03675aaa60 | ||
|
|
c0c19c0425 | ||
|
|
0bbb27e3d3 | ||
|
|
214d283366 | ||
|
|
7d95ddf6a5 | ||
|
|
a19c5c269a | ||
|
|
d641866cc4 | ||
|
|
6102e0af6b | ||
|
|
225bae08d3 | ||
|
|
d8b5abb12f | ||
|
|
e4de937f6e | ||
|
|
d9a3da96bb | ||
|
|
87d1f33c05 | ||
|
|
05331db008 | ||
|
|
f0cb735d69 | ||
|
|
d68c6c151c | ||
|
|
dfb910f270 | ||
|
|
ddf3a5cfa0 | ||
|
|
5976b35c90 | ||
|
|
6b204fce15 | ||
|
|
f731d0f9f0 | ||
|
|
e231d0c4be | ||
|
|
e4328a2ed4 | ||
|
|
069e489d03 | ||
|
|
990dffc11c | ||
|
|
b4140d6457 | ||
|
|
91af5fd27c | ||
|
|
588e4984b3 | ||
|
|
baabc72f58 | ||
|
|
0bb16f9795 | ||
|
|
be7efb2ad0 | ||
|
|
1dab522bd2 | ||
|
|
7a24c10f3d | ||
|
|
7525fa01b6 | ||
|
|
fbd9575df7 | ||
|
|
14608cca4d | ||
|
|
0dab113a3f | ||
|
|
95fc7a22dc | ||
|
|
9c69ba2122 | ||
|
|
978a049277 | ||
|
|
538065e29e | ||
|
|
99a8f874e8 | ||
|
|
4c955beff2 | ||
|
|
ae41ea100c | ||
|
|
07e34beca1 | ||
|
|
87d8ddef98 | ||
|
|
8b29d02880 | ||
|
|
7816b4e450 | ||
|
|
ae97a48ba2 | ||
|
|
240c2f847c | ||
|
|
e39a102c14 | ||
|
|
65d2cbd732 | ||
|
|
db70ba56c6 | ||
|
|
3fa1c526fe | ||
|
|
992893510e | ||
|
|
5fe4bc94da | ||
|
|
c22e07bc16 | ||
|
|
5f302a5711 | ||
|
|
cc749c0c24 | ||
|
|
18ec5c937f | ||
|
|
bd6dfd31fd | ||
|
|
e4f4f70bc9 | ||
|
|
5aaa55d0c9 | ||
|
|
8b1e8ae2eb | ||
|
|
5a3ef19e5d | ||
|
|
01cb48e5b6 | ||
|
|
ea7536bea3 | ||
|
|
2356c10d34 | ||
|
|
8b2dd27a79 | ||
|
|
114293bb00 | ||
|
|
e0171a5a28 | ||
|
|
d7db174c1d | ||
|
|
079542cf74 | ||
|
|
43f210a141 | ||
|
|
5015b8e0d9 | ||
|
|
4d0c0ad48c | ||
|
|
7db61928b1 | ||
|
|
3dae039765 | ||
|
|
35ff13fb38 | ||
|
|
231c0c2940 | ||
|
|
a92f567e5c | ||
|
|
ff9d373fcd | ||
|
|
c111c2c084 | ||
|
|
2d5a08a271 | ||
|
|
9a8abee77c | ||
|
|
4c30c2fa47 | ||
|
|
0cca1c8dbe | ||
|
|
def7b2b01c | ||
|
|
4891725f68 |
@@ -1,8 +0,0 @@
|
||||
# 디폴트 무시된 파일
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 에디터 기반 HTTP 클라이언트 요청
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -0,0 +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_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
|
||||
|
||||
|
||||
@@ -0,0 +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_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
NEXT_PUBLIC_SITE_ID = DM
|
||||
NEXT_PUBLIC_PORT = 3000
|
||||
NEXT_PUBLIC_PROXY_HOST = http://wr.hasuon.or.kr
|
||||
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
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl
|
||||
NEXT_PUBLIC_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://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { BaseLayout } from "@common/components";
|
||||
import type { MenuItem } from "./store/common";
|
||||
import { useDmStore } from "./store";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Base({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const { getMenuAuth } = useDmStore((state) => state);
|
||||
|
||||
const router = useRouter();
|
||||
const handleClick = (id:MenuItem) =>{
|
||||
getMenuAuth(id.menuCd);
|
||||
router.push(id.urlPath);
|
||||
}
|
||||
|
||||
return <BaseLayout handleMenuClick={handleClick}>{children}</BaseLayout>;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
|
||||
type CommonProps = {
|
||||
value: string | number | '';
|
||||
format: string;
|
||||
}
|
||||
|
||||
export default function TransValueFormat({value='', format=''}: CommonProps) {
|
||||
return (
|
||||
format === 'yyyyMMdd' ?
|
||||
<>{value.toString().substring(0,4)+'-'+value.toString().substring(4,6)+'-'+value.toString().substring(6,8)}</>
|
||||
: format === 'number' ?
|
||||
<>{String(value ?? '').includes('.')
|
||||
? String(value).split('.').map((part, index) =>
|
||||
index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part
|
||||
).join('.')
|
||||
: String(value ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</>
|
||||
: format === 'float1' ?
|
||||
<>{String(value ?? '').includes('.')
|
||||
? String(value).split('.').map((part, index) =>
|
||||
index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part
|
||||
).join('.')
|
||||
: String(value ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")}</>
|
||||
: <>{value}</>
|
||||
)
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,70 @@
|
||||
@import "../../../packages/common/components/src/base.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client"
|
||||
import Base from "./base";
|
||||
import "./globals.css";
|
||||
import { StoreProvider } from "./store";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<StoreProvider>
|
||||
<Base>{children}</Base>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client"
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@common/components/ui';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<div className='flex'>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/wrAbout">물재이용이란</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/wrExample">물재이용 사례</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/wrStatus">물재이용 현황</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/wrOverview">물재이용 사업추진절차</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/sdmMng/sdmInsert">수요처 등록</Link>
|
||||
</Button>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/sdmMng/sdmInfoInsert">수요처 정보 등록</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<Button className='ml-5 place-self-auto'>
|
||||
<Link href="/pages/sdmMng/sdmApplyList">수요처 신청 현황(개발중)</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
"use client"
|
||||
import { Table, TableBody, TableCell, TableRow } from '@common/components/ui/table';
|
||||
|
||||
import { useDmStore } from '@/app/store';
|
||||
import { ContentsDiv, Title, UiModal, UiTable } from '@common/business_components/ui';
|
||||
import ListDiv from '@common/business_components/ui/list_div';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import SdmLogin from '../sdmLogin/page';
|
||||
import type { DemandCompType } from '@/app/store/sdmMng';
|
||||
import TransValueFormat from '@/app/common/transValueFormat';
|
||||
|
||||
const TABLE_STYLES = {
|
||||
header: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line",
|
||||
cell: "text-right whitespace-pre-line",
|
||||
fullWidth: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line",
|
||||
table: "fixed w-full"
|
||||
} as const;
|
||||
|
||||
type SdmApplyProps = {
|
||||
sdmNo : string;
|
||||
}
|
||||
|
||||
export default function SdmApplyList({}: SdmApplyProps) {
|
||||
const {getSdmInfoStatusList, Sdm, Common} = useDmStore((state) => state);
|
||||
const {successLogin, sdmInfoStatusList, upHeadSdmInfoStatus} = Sdm;
|
||||
const {selectSidoData, selectSigunData} = Common;
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [isLoginStorageInfo, setIsLoginStorageInfo] = useState(false);
|
||||
const [sdmApplyDetail, setSdmApplyDetail] = useState<DemandCompType>({} as DemandCompType);
|
||||
const [transformedList, setTransformedList] = useState<DemandCompType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('sessionStorage : ',sessionStorage.getItem('user'));
|
||||
const user = sessionStorage.getItem('user');
|
||||
if (user) {
|
||||
setIsLoginStorageInfo(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (successLogin || isLoginStorageInfo) {
|
||||
setIsLogin(true);
|
||||
const user = sessionStorage.getItem('user');
|
||||
if (user) {
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
getSdmInfoStatusList(userData.dmdInfoId);
|
||||
} catch (error) {
|
||||
console.error('Error parsing user data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [successLogin, getSdmInfoStatusList, isLoginStorageInfo]);
|
||||
|
||||
// 데이터 변환을 위한 별도의 useEffect
|
||||
useEffect(() => {
|
||||
if (sdmInfoStatusList.length > 0) {
|
||||
const newTransformedList = sdmInfoStatusList.map((item: DemandCompType) => ({
|
||||
...item,
|
||||
ctpvCd: selectSidoData.find(sido => sido.val === item.ctpvCd?.trim())?.text || '',
|
||||
sggCd: selectSigunData.find(sigun => sigun.val === item.sggCd?.trim())?.text || ''
|
||||
}));
|
||||
setTransformedList(newTransformedList);
|
||||
}
|
||||
}, [sdmInfoStatusList, selectSidoData, selectSigunData]);
|
||||
|
||||
const onClickSdmStatus = (index: number | undefined) => {
|
||||
if (index !== undefined) {
|
||||
const selectedStatus = transformedList[index];
|
||||
if (selectedStatus) {
|
||||
setSdmApplyDetail(selectedStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title title="수요신청현황" />
|
||||
<ContentsDiv>
|
||||
<ListDiv>
|
||||
<div className="overflow-y-scroll h-[250px]">
|
||||
<div className="h-[110%] w-[99%]">
|
||||
<UiTable
|
||||
headName='수요정보 신청현황'
|
||||
tableData={transformedList}
|
||||
headlist={upHeadSdmInfoStatus}
|
||||
pageSize={0}
|
||||
total={0}
|
||||
cellClick={(index) => onClickSdmStatus(index)}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ListDiv>
|
||||
<ListDiv>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">수요신청 상세현황</h3>
|
||||
</div>
|
||||
<div className={`card !m-[1.2rem] !p-0r`}>
|
||||
<Table variant="secondary" style={{ tableLayout: 'fixed', width: '100%' }}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>요구시기</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.dmndTmng?.toString()}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>요구수량(m³/일)</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.dmndQty} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>사용용도</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.useUsgCd}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>희망단가(원/m³)</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.wntUntprc} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className={TABLE_STYLES.fullWidth}>요구수량</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>총대장균군수(개/100ml)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.grssClfg} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>결합잔류염소(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.combResdch} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>탁도(NTU)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.trbt} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>생물학적 산소요구량(BOD)(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.blgcOxygDmndQnt} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>냄새</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.smll} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>색도(도)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.crmty} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>총질소(T-N)(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tn} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>총인(T-P)(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.tph} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>수소이온농도(pH)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.ph} format="number"/>}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>염화물(mgCL/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.clIonQnt} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>전기전도도(㎲/cm)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.cdtnv} format="number"/>}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>비고</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.rmrk}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className={TABLE_STYLES.fullWidth}>공급처 후보지</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지1</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지2</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지3</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</ListDiv>
|
||||
<ListDiv>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">수요신청 진행상태</h3>
|
||||
</div>
|
||||
<div className={`card !m-[1.2rem] !p-0r`}>
|
||||
<Table variant="secondary" style={{ tableLayout: 'fixed', width: '100%' }}>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>진행상태</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}>{sdmApplyDetail?.status?.toString()}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>공급처매칭결과</TableCell>
|
||||
<TableCell colSpan={2} className={TABLE_STYLES.cell}></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</ListDiv>
|
||||
</ContentsDiv>
|
||||
{!isLogin?
|
||||
<UiModal
|
||||
body={<SdmLogin/>}
|
||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||
open={!isLogin}
|
||||
onOpenChange={(open) => !open && setIsLogin(false)}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
:null}
|
||||
<Toaster position="top-center" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
"use client"
|
||||
|
||||
import { useDmStore } from '@/app/store';
|
||||
import type { MoneyDetailType, SimilarCaseType } from '@/app/store/sdmMng';
|
||||
import { SelectBox, Title, UiTable } from '@common/business_components/ui';
|
||||
import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan';
|
||||
import { Table, TableBody, TableCell, TableHeader, TableRow } from '@common/components/ui/table';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
type modalType = {
|
||||
sply: number | 0;
|
||||
usageCd: string | '';
|
||||
money: number | 0;
|
||||
};
|
||||
|
||||
const searchUseTypeList = [
|
||||
{ text: '공급량', val: 'S' },
|
||||
{ text: '사용용도', val: 'U' },
|
||||
];
|
||||
|
||||
export default function ModalSimilarPage({ sply = 0, usageCd = '', money = 0 }: modalType) {
|
||||
const { Sdm, getSimilarCaseList } = useDmStore((state) => state);
|
||||
const [searchUseType, setSearchUseType] = useState<{text: string, value: string}>({text: '공급량', value: 'S'});
|
||||
const [moneyDetailList, setMoneyDetailList] = useState<MoneyDetailType[]>([]);
|
||||
const {
|
||||
upHeadSimilarCaseList,
|
||||
similarCaseList,
|
||||
upHeadMoneyDetailList,
|
||||
} = Sdm;
|
||||
|
||||
const rowSpans = useMemo(() => {
|
||||
const spans: { [key: number]: number } = {};
|
||||
let currentValue = '';
|
||||
let currentSpan = 0;
|
||||
let startIndex = 0;
|
||||
|
||||
similarCaseList.forEach((item: SimilarCaseType, index: number) => {
|
||||
if (item.ruseBizNm === currentValue
|
||||
|| item.bizBgngDate === currentValue
|
||||
|| item.bizEndDate === currentValue
|
||||
|| item.reqBizReuseFcltCst === currentValue) {
|
||||
currentSpan++;
|
||||
spans[startIndex] = currentSpan;
|
||||
} else {
|
||||
currentValue = item.ruseBizNm;
|
||||
currentSpan = 1;
|
||||
startIndex = index;
|
||||
spans[index] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
return spans;
|
||||
}, [similarCaseList]);
|
||||
|
||||
const handleCellClick = (index: number) => {
|
||||
setMoneyDetailList([{
|
||||
reqBizGrssCst: similarCaseList[index]?.reqBizGrssCst ?? '',
|
||||
reqBizReuseFcltCst: similarCaseList[index]?.reqBizReuseFcltCst ?? '',
|
||||
reqBizPplCst: similarCaseList[index]?.reqBizPplCst ?? '',
|
||||
reqBizArchCst: similarCaseList[index]?.reqBizArchCst ?? '',
|
||||
reqBizMchnCst: similarCaseList[index]?.reqBizMchnCst ?? '',
|
||||
reqBizMtrngCst: similarCaseList[index]?.reqBizMtrngCst ?? '',
|
||||
reqBizGisCst: similarCaseList[index]?.reqBizGisCst ?? '',
|
||||
reqBizCctvCst: similarCaseList[index]?.reqBizCctvCst ?? '',
|
||||
reqBizCmcnCst: similarCaseList[index]?.reqBizCmcnCst ?? '',
|
||||
reqBizWstCst: similarCaseList[index]?.reqBizWstCst ?? '',
|
||||
reqBizDesignCst: similarCaseList[index]?.reqBizDesignCst ?? '',
|
||||
reqBizSpvsCst: similarCaseList[index]?.reqBizSpvsCst ?? '',
|
||||
reqBizSbsdCst: similarCaseList[index]?.reqBizSbsdCst ?? '',
|
||||
reqBizEtc: similarCaseList[index]?.reqBizEtc ?? '',
|
||||
prcmPlanNlep: similarCaseList[index]?.prcmPlanNlep ?? '',
|
||||
prcmPlanLcep: similarCaseList[index]?.prcmPlanLcep ?? '',
|
||||
prcmPlanBrdnCst: similarCaseList[index]?.prcmPlanBrdnCst ?? '',
|
||||
prcmPlanEcqrRt: similarCaseList[index]?.prcmPlanEcqrRt ?? '',
|
||||
prcmPlanGrssCst: similarCaseList[index]?.prcmPlanGrssCst ?? '',
|
||||
clclnNlep: similarCaseList[index]?.clclnNlep ?? '',
|
||||
clclnLcep: similarCaseList[index]?.clclnLcep ?? '',
|
||||
clclnBrdnCst: similarCaseList[index]?.clclnBrdnCst ?? '',
|
||||
clclnEcqrRt: similarCaseList[index]?.clclnEcqrRt ?? '',
|
||||
clclnGrssCst: similarCaseList[index]?.clclnGrssCst ?? ''
|
||||
}]);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const type = searchUseType.value;
|
||||
getSimilarCaseList(type, type === 'S' ? sply : type === 'U' ? usageCd : money);
|
||||
}, [getSimilarCaseList, money, searchUseType, sply, usageCd]);
|
||||
|
||||
return (
|
||||
<div className="align-top w-[1360px] overflow-x-auto">
|
||||
<Title title="유사사례 조회" />
|
||||
<div className="overflow-x-auto h-[450px] w-full mt-8" tabIndex={0} aria-label="유사사례 테이블 영역">
|
||||
<div className="w-[150%]">
|
||||
<div className="card-header justify-self-start">
|
||||
<h3 className="text-2xl font-medium text-label">유사사례</h3>
|
||||
<SelectBox
|
||||
tabIndex={0}
|
||||
aria-label="검색 기준 선택"
|
||||
selectArray={searchUseTypeList}
|
||||
value={searchUseType.value}
|
||||
onSelectValue={(val) => {
|
||||
setSearchUseType({text: searchUseTypeList.find(item => item.val === val)?.text ?? '', value: val.toString()});
|
||||
}}
|
||||
className="w-[140px] ml-20"
|
||||
label=""
|
||||
selectClass="w-[140px]"
|
||||
/>
|
||||
</div>
|
||||
<div className={`card !m-[1.2rem] !p-0r`}>
|
||||
<Table variant="secondary">
|
||||
<TableHeader className="font-bold">
|
||||
<HeadMakeColSpan upHeadList={upHeadSimilarCaseList} />
|
||||
</TableHeader>
|
||||
<TableBody className='text-center'>
|
||||
{similarCaseList.length > 0 ? similarCaseList?.map((el: SimilarCaseType, index) => (
|
||||
<TableRow key={`similar-case-row-${index}-${el.ruseBizNm}-${el.bizBgngDate}`}>
|
||||
{upHeadSimilarCaseList.map((head) => (
|
||||
head.id && head.id.length > 1 && (
|
||||
<TableCell
|
||||
tabIndex={head.id === 'someClickableId' ? 0 : undefined}
|
||||
role="button"
|
||||
aria-label={`유사사례 ${head.id} 클릭 시 상세 정보 표시`}
|
||||
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id as keyof SimilarCaseType] === 'number' ? 'text-right' : 'text-center'} cursor-pointer`}
|
||||
key={`similar-case-cell-${head.id}-${index}-${el.ruseBizNm}`}
|
||||
rowSpan={(head.id === 'businessName' || head.id ==='businessDuration' || head.id === 'money') ? rowSpans[index] : undefined}
|
||||
style={(head.id === 'businessName' || head.id === 'businessDuration' || head.id === 'money') && index > 0 && similarCaseList[index - 1]?.ruseBizNm === el.ruseBizNm ? { display: 'none' } : undefined}
|
||||
onClick={() => handleCellClick(index)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleCellClick(index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{typeof el[head.id as keyof SimilarCaseType] !== 'number'
|
||||
? String(el[head.id as keyof SimilarCaseType] ?? '')
|
||||
: String(el[head.id as keyof SimilarCaseType]).includes('.')
|
||||
? String(el[head.id as keyof SimilarCaseType]).split('.').map((part, index) =>
|
||||
index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part
|
||||
).join('.')
|
||||
: String(el[head.id as keyof SimilarCaseType] ?? '').replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
|
||||
</TableCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
)) : <></>}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto h-[270px] w-full mt-8">
|
||||
<div className="w-[130%]">
|
||||
<UiTable
|
||||
headName={"사업비 상세정보"}
|
||||
tableData={moneyDetailList}
|
||||
headlist={upHeadMoneyDetailList}
|
||||
pageSize={0}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
"use client";
|
||||
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
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 { 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({
|
||||
sdmNm: z.string()
|
||||
.min(1, "수요처를 선택해주세요"),
|
||||
demandYmd: z.date().optional(),
|
||||
demandQnty: z.string()
|
||||
.min(1, "요구수량을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "요구수량은 숫자만 입력 가능합니다"),
|
||||
usePurpose: z.string().optional(),
|
||||
money: z.string()
|
||||
.regex(/^[0-9]+$/, "희망단가는 숫자만 입력 가능합니다"),
|
||||
/**요구수질 */
|
||||
reqWaterQlty : z.string().optional(),
|
||||
grssClfg: z.string()
|
||||
.min(1, "총대장균군수를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "총대장균군수는 숫자만 입력 가능합니다"),
|
||||
tn : z.string()
|
||||
.min(1, "총질소를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "총질소는 숫자만 입력 가능합니다"),
|
||||
combResdch : z.string()
|
||||
.min(1, "결합잔류염소를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "결합잔류염소는 숫자만 입력 가능합니다"),
|
||||
tph : z.string()
|
||||
.min(1, "총인산물을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "총인산물은 숫자만 입력 가능합니다"),
|
||||
trbt : z.string()
|
||||
.min(1, "탁도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "탁도는 숫자만 입력 가능합니다"),
|
||||
ph : z.string()
|
||||
.min(1, "수소이온농도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "수소이온농도는 숫자만 입력 가능합니다"),
|
||||
bod : z.string()
|
||||
.min(1, "생물학적 산소요구량을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "생물학적 산소요구량은 숫자만 입력 가능합니다"),
|
||||
/**컬럼확인필요 */
|
||||
clIonQnt : z.string()
|
||||
.min(1, "염화물을 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "염화물은 숫자만 입력 가능합니다"),
|
||||
smll : z.string()
|
||||
.min(1, "냄새를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "냄새는 숫자만 입력 가능합니다"),
|
||||
cdtnv : z.string()
|
||||
.min(1, "전기전도도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "전기전도도는 숫자만 입력 가능합니다"),
|
||||
crmty : z.string()
|
||||
.min(1, "색도를 입력해주세요")
|
||||
.regex(/^[0-9]+$/, "색도는 숫자만 입력 가능합니다"),
|
||||
rmrk : z.string().optional(),
|
||||
/* telno: z.string()
|
||||
.min(8, "전화번호는 8자 이상 입력해주세요")
|
||||
.max(12, "전화번호는 12자 이내로 입력해주세요")
|
||||
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), */
|
||||
candidateTitle: z.string().optional(),
|
||||
});
|
||||
|
||||
export type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||
|
||||
function SdmInfoInsertContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const {insertSdmInfoData, Sdm} = useDmStore((state) => state);
|
||||
const {successLogin} = Sdm;
|
||||
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
|
||||
const formSchema = createFormSchema();
|
||||
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [facilityName, setFacilityName] = useState<string>('');
|
||||
const [defaultValues, setDefaultValues] = useState<FormData>({
|
||||
sdmNm: '',
|
||||
demandYmd: new Date(),
|
||||
demandQnty: '',
|
||||
usePurpose: '',
|
||||
money: '',
|
||||
/**요구수질 */
|
||||
reqWaterQlty: '',
|
||||
grssClfg: '',
|
||||
tn: '',
|
||||
combResdch: '',
|
||||
tph: '',
|
||||
trbt: '',
|
||||
ph: '',
|
||||
bod: '',
|
||||
/**컬럼확인필요 */
|
||||
clIonQnt: '',
|
||||
smll: '',
|
||||
cdtnv: '',
|
||||
crmty: '',
|
||||
rmrk: '',
|
||||
/** 후보지 제목 */
|
||||
candidateTitle: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const user = sessionStorage.getItem('user');
|
||||
setDefaultValues(prev => ({
|
||||
...prev,
|
||||
sdmNm: searchParams.get('dmdCoNm') || '',
|
||||
}));
|
||||
sessionStorage.setItem('user', searchParams.get('dmdInfoId') || '');
|
||||
setIsLogin(user || searchParams.get('dmdInfoId') ? true : false);
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const local = localStorage.getItem('user');
|
||||
if(local) {
|
||||
setIsLogin(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
// 로그인 성공 시 모달 닫기 및 폼 업데이트
|
||||
useEffect(() => {
|
||||
if (successLogin) {
|
||||
setIsLogin(true);
|
||||
const user = sessionStorage.getItem('user');
|
||||
if (user) {
|
||||
try {
|
||||
const userData = JSON.parse(user);
|
||||
if (userData.dmdCoNm) {
|
||||
setFacilityName(userData.dmdCoNm);
|
||||
form.setValue('sdmNm', userData.dmdCoNm);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing user data:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [successLogin, form]);
|
||||
|
||||
// 체크박스 상태가 변경될 때마다 money 필드의 에러를 초기화
|
||||
useEffect(() => {
|
||||
if (ismoneyDisabled) {
|
||||
form.clearErrors('money');
|
||||
form.setValue('money', '');
|
||||
}
|
||||
}, [ismoneyDisabled, form]);
|
||||
|
||||
const onSubmit: SubmitHandler<FormData> = async (data) => {
|
||||
try {
|
||||
// Transform form data to match API schema
|
||||
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,
|
||||
ctpvCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.ctpvCd?.toString() || searchParams.get('ctpvCd') || '',
|
||||
sggCd: JSON.parse(sessionStorage.getItem('user') || '{}')?.sggCd?.toString() || searchParams.get('sggCd') || '',
|
||||
daddr: JSON.parse(sessionStorage.getItem('user') || '{}')?.daddr?.toString() || searchParams.get('daddr') || '',
|
||||
telno: JSON.parse(sessionStorage.getItem('user') || '{}')?.telno?.toString() || searchParams.get('telno') || '',
|
||||
rgtr: JSON.parse(sessionStorage.getItem('user') || '{}')?.rgtr?.toString() || searchParams.get('rgtr') || '',
|
||||
useUsgCd: data.usePurpose || '',
|
||||
wntUntprc: Number(data.money) || '',
|
||||
dmndTmng: data.demandYmd || new Date(),
|
||||
dmndQty: Number(data.demandQnty) || '',
|
||||
grssClfg: Number(data.grssClfg) || '',
|
||||
tn: Number(data.tn) || '',
|
||||
combResdch: Number(data.combResdch) || '',
|
||||
tph: Number(data.tph) || '',
|
||||
trbt: Number(data.trbt) || '',
|
||||
ph: Number(data.ph) || '',
|
||||
blgcOxygDmndQnt: Number(data.bod) || '',
|
||||
clIonQnt: Number(data.clIonQnt) || '',
|
||||
smll: Number(data.smll) || '',
|
||||
cdtnv: Number(data.cdtnv) || '',
|
||||
crmty: Number(data.crmty) || '',
|
||||
rmrk: data.rmrk || '',
|
||||
bzmnNo: "",
|
||||
dmdAplyPswd: "",
|
||||
index: 0,
|
||||
reqDt: "",
|
||||
dmdInfoSn: 0,
|
||||
status : '',
|
||||
};
|
||||
|
||||
await insertSdmInfoData(apiData);
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 underline">
|
||||
수요정보 등록
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<SdmInfoRegForm
|
||||
form={form}
|
||||
onCheckboxChange={setIsmoneyDisabled}
|
||||
facilityName={facilityName}
|
||||
/>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-40 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
|
||||
>
|
||||
수요정보 등록
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isLogin?
|
||||
<UiModal
|
||||
body={<SdmLogin/>}
|
||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||
open={!isLogin}
|
||||
onOpenChange={(open) => !open && setIsLogin(false)}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
:null}
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MyForm() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SdmInfoInsertContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
import SdmInsertForm from '@common/business_components/ui/sdmInsertForm';
|
||||
import { useRouter } from "next/navigation";
|
||||
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 { selectSidoData, selectSigunData } = Common;
|
||||
const { demandCompList, successNextPage } = Sdm;
|
||||
const router = useRouter();
|
||||
const formDataRef = useRef<DemandCompType | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
initializeCommonData();
|
||||
}, [initializeCommonData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (successNextPage && formDataRef.current) {
|
||||
const data = formDataRef.current;
|
||||
const queryParams = new URLSearchParams({
|
||||
dmdCoNm: data.dmdCoNm || '',
|
||||
ctpvCd: data.ctpvCd || '',
|
||||
sggCd: data.sggCd || '',
|
||||
dmdInfoId: data.dmdInfoId || '',
|
||||
daddr: data.daddr || '',
|
||||
telno: data.telno || '',
|
||||
});
|
||||
router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`);
|
||||
}
|
||||
}, [successNextPage, router]);
|
||||
|
||||
const callback = useCallback((searchText: string, pageCnt: number) => {
|
||||
searchText = searchText.trim();
|
||||
if(searchText.length > 0) {
|
||||
getDemandCompList(searchText, pageCnt);
|
||||
}
|
||||
}, [getDemandCompList]);
|
||||
|
||||
const handleInsertSdmData = useCallback((data: import('@common/business_components/ui/sdmInsertForm/form').DemandCompType) => {
|
||||
const convertedData: DemandCompType = {
|
||||
...data,
|
||||
dmdInfoSn: '',
|
||||
dmdInfoFk: '',
|
||||
status: '',
|
||||
};
|
||||
formDataRef.current = convertedData;
|
||||
insertSdmData(convertedData).catch(error => {
|
||||
console.error('Error inserting SDM data:', error);
|
||||
});
|
||||
}, [insertSdmData]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<SdmInsertForm
|
||||
selectSidoData={selectSidoData}
|
||||
selectSigunData={selectSigunData}
|
||||
dataList={demandCompList}
|
||||
rtnValue={callback}
|
||||
rtnInputData={handleInsertSdmData}
|
||||
/>
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import { SdmLoginForm } from '@/components/ui/sdmLoginForm/component';
|
||||
|
||||
export default function SdmLogin() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 underline">
|
||||
수요처 로그인
|
||||
</h1>
|
||||
</div>
|
||||
<div className="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6">
|
||||
<SdmLoginForm />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
const NAVS = [
|
||||
{ id: 'card1', label: '물재이용이란' },
|
||||
{ id: 'card2', label: '물재이용 필요성' },
|
||||
{ id: 'card3', label: '빗물 재이용' },
|
||||
{ id: 'card4', label: '중수 재이용' },
|
||||
{ id: 'card5', label: '하수 재이용' },
|
||||
{ id: 'card6', label: '물재이용 목표' },
|
||||
];
|
||||
|
||||
export default function WrAboutPage() {
|
||||
const [activeIdx, setActiveIdx] = useState(0);
|
||||
const navRefs = useRef<(HTMLLIElement | null)[]>([]);
|
||||
const cardRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
// 클릭 시 이동
|
||||
const handleNavClick = (idx: number) => {
|
||||
setActiveIdx(idx);
|
||||
const targetCard = cardRefs.current[idx];
|
||||
if (targetCard) {
|
||||
targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
// 키보드 이벤트 처리
|
||||
const handleKeyDown = (e: React.KeyboardEvent, idx: number) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleNavClick(idx);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-200px 0px 0px 0px', // 상단에서 200px 지점을 기준으로
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const idx = cardRefs.current.findIndex(ref => ref === entry.target);
|
||||
if (idx !== -1) {
|
||||
setActiveIdx(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// 각 카드 요소를 관찰
|
||||
cardRefs.current.forEach((card) => {
|
||||
if (card) {
|
||||
observer.observe(card);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cardRefs.current.forEach((card) => {
|
||||
if (card) {
|
||||
observer.unobserve(card);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="relative w-[950px] mx-auto pt-[100px] font-sans">
|
||||
{/* 사이드바 */}
|
||||
<nav className="fixed left-[290px] top-[195px] w-[210px] z-50" aria-label="물재이용 정보 섹션 내비게이션">
|
||||
<ul role="list" className="relative space-y-6">
|
||||
{NAVS.map((nav, idx) => (
|
||||
<li
|
||||
key={nav.id}
|
||||
ref={(el) => { navRefs.current[idx] = el; }}
|
||||
role="listitem"
|
||||
tabIndex={0}
|
||||
aria-current={idx === activeIdx ? 'true' : 'false'}
|
||||
aria-label={`현재 위치: ${nav.label}`}
|
||||
onClick={() => handleNavClick(idx)}
|
||||
onKeyDown={(e) => handleKeyDown(e, idx)}
|
||||
className={`nav-item relative pl-6 text-2xl font-bold text-gray-700 cursor-pointer transition-all duration-300
|
||||
after:content-[''] after:absolute after:top-6 after:left-[0.5rem] after:w-[2px] after:h-[24px] after:bg-[#C5C5C5]
|
||||
${idx === activeIdx ? 'text-4xl font-bold text-gray-900' : ''}`}
|
||||
>
|
||||
<span className={`absolute left-[0.5rem] top-1 w-4 h-4 rounded-full -translate-x-1/2 transition-all duration-300
|
||||
${idx === activeIdx ? 'bg-blue-500 border-blue-500' : 'bg-white border-gray-400 border'}`}></span>
|
||||
{nav.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<article ref={(el) => { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title">
|
||||
<h2 id="card1-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용이란
|
||||
</h2>
|
||||
<section aria-labelledby="card1-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card1-desc" className="text-[#269DDA] text-4xl font-bold mb-8">물 재이용이 무엇인가요?</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
||||
물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를<br />
|
||||
물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,<br />
|
||||
하천 유지 등의 용도로 이용하는 것을 말합니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_01_1.png"
|
||||
alt="이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-8 rounded"
|
||||
/>
|
||||
<p className="text-center">
|
||||
환경부는 2011년에 '물의 재이용 촉진 및 지원에 관한 법률'을 제정하여<br />
|
||||
한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드2 */}
|
||||
<article ref={(el) => { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title">
|
||||
<h2 id="card2-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 필요성
|
||||
</h2>
|
||||
<section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card2-desc" className="text-[#EE7B62] text-4xl font-bold mb-8">물 재이용, 왜 필요한가요?</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
||||
빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br />
|
||||
물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_02_1.png"
|
||||
alt="이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-8 w-[80%]"
|
||||
/>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
||||
- 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.<br />
|
||||
- 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_02_2.png"
|
||||
alt="세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-8"
|
||||
/>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">
|
||||
- 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.<br />
|
||||
- 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_02_3.png"
|
||||
alt="물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[93%] mx-auto"
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드3 */}
|
||||
<article ref={(el) => { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title">
|
||||
<h2 id="card3-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
빗물 재이용
|
||||
</h2>
|
||||
<section aria-labelledby="card3-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card3-desc" className="text-[#269DDA] text-4xl font-bold mb-6">친환경 수자원 빗물,<br />편리하게 사용할 수 있어요!</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">가뭄을 극복하고 홍수를 예방하기 위해<br />비가 내릴 때 빗물을 모아두었다가 사용합니다.</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_03_1.png"
|
||||
alt="주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-6"
|
||||
/>
|
||||
<Image
|
||||
src="/img/wrAbout/img_03_2.png"
|
||||
alt="빗물이용"
|
||||
width={200}
|
||||
height={100}
|
||||
className="mb-5 w-[20%]"
|
||||
/>
|
||||
<p className="text-2xl font-bold leading-relaxed">
|
||||
- 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등<br />
|
||||
- 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)<br />
|
||||
- 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드4 */}
|
||||
<article ref={(el) => { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title">
|
||||
<h2 id="card4-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
중수 재이용
|
||||
</h2>
|
||||
<section aria-labelledby="card4-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card4-desc" className="text-[#9771B0] text-4xl font-bold mb-6">요모조모 쓸모 많은 중수,<br />알뜰하게 재이용할 수 있어요!</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">한번 사용한 물을 중수도 시설을 통해 재생하여<br />개별적 또는 지역적으로 재이용할 수 있습니다.</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_04_1.png"
|
||||
alt="욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-6"
|
||||
/>
|
||||
<Image
|
||||
src="/img/wrAbout/img_04_2.png"
|
||||
alt="중수재이용"
|
||||
width={200}
|
||||
height={100}
|
||||
className="mb-5 w-[20%]"
|
||||
/>
|
||||
<p className="text-2xl font-bold leading-relaxed">
|
||||
- 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등<br />
|
||||
- 의무 재이용량: 물 사용량의 10% 이상<br />
|
||||
- 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드5 */}
|
||||
<article ref={(el) => { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title">
|
||||
<h2 id="card5-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
하수 재이용
|
||||
</h2>
|
||||
<section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card5-desc" className="text-[#00A5B1] text-4xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1>
|
||||
<p className="text-2xl font-bold mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_05_1.png"
|
||||
alt="주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-6"
|
||||
/>
|
||||
<Image
|
||||
src="/img/wrAbout/img_05_2.png"
|
||||
alt="하수처리 재이용"
|
||||
width={200}
|
||||
height={100}
|
||||
className="mb-5 w-[20%]"
|
||||
/>
|
||||
<p className="text-2xl font-bold leading-relaxed">
|
||||
- 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)<br />
|
||||
- 의무 재이용량: 일 하수처리량의 10% 이상<br />
|
||||
- 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드6 */}
|
||||
<article ref={(el) => { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title">
|
||||
<h2 id="card6-title" className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 목표
|
||||
</h2>
|
||||
<section aria-labelledby="card6-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card6-desc" className="text-[#269DDA] text-4xl font-bold mb-6">2030년 물 재이용 목표를 위해<br />모두의 관심과 노력이 필요합니다.</h1>
|
||||
<p className="text-2xl font-bold mb-6 leading-relaxed">
|
||||
환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여<br />한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_06_1.png"
|
||||
alt="지속가능한 물 재이용 정착으로 건전한 물순환 확산"
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[80%] mx-auto mb-6"
|
||||
/>
|
||||
<div className="flex gap-3 flex-wrap justify-between bg-gray-100 p-6 rounded-lg mb-8">
|
||||
<div className="flex-1">
|
||||
<Image
|
||||
src="/img/wrAbout/icon_01.png"
|
||||
alt=""
|
||||
width={70}
|
||||
height={70}
|
||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||
/>
|
||||
<h3 className="text-center font-semibold text-nowrap mb-2">도시 물 순환이용<br />체계로의 전환</h3>
|
||||
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
|
||||
<li className="text-nowrap">- 지속가능한 물 순환이용체계 구축</li>
|
||||
<li>- 물 재이용시설 관리체계 강화</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<Image
|
||||
src="/img/wrAbout/icon_02.png"
|
||||
alt=""
|
||||
width={70}
|
||||
height={70}
|
||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||
/>
|
||||
<h3 className="text-center font-semibold text-nowrap mb-2">유역기반 하수처리수 재이용<br />수요확대 및 공급체계 구축</h3>
|
||||
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
|
||||
<li>- 유역기반 통합물관리 연계 강화</li>
|
||||
<li>- 하수처리수 재이용 공업용수 공급 활성화</li>
|
||||
<li>- 하수처리수 재이용을 활용한 물순환 촉진</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<Image
|
||||
src="/img/wrAbout/icon_03.png"
|
||||
alt=""
|
||||
width={70}
|
||||
height={70}
|
||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||
/>
|
||||
<h3 className="text-center font-semibold text-nowrap mb-2">물 재이용 인식제고<br />및 산업발전 기반 마련</h3>
|
||||
<ul className="text-sm text-gray-600 leading-relaxed space-y-1">
|
||||
<li>- 물 재이용 교육 및 대국민 홍보 강화</li>
|
||||
<li>- 핵심기술 육성 및 산업발전 지원 강화</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
src="/img/wrAbout/img_06_2.png"
|
||||
alt="물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto"
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function WrExamplePage() {
|
||||
return (
|
||||
<div className="bg-[#F1F6F9]">
|
||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 rounded-full"></span>
|
||||
물 재이용사례
|
||||
</h2>
|
||||
</div>
|
||||
<div className="bg-[#F1F6F9] text-gray-800">
|
||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||
{/* 카드 1 */}
|
||||
<div className="card-wrap mb-24" id="card1">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 1
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
경기권역 반도체 사업장 물 재이용 사업<br />
|
||||
: 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등)
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 배경 및 목적 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 배경 및 목적</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 주요 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]<br />
|
||||
2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)]
|
||||
</p>
|
||||
</section>
|
||||
{/* 특이사항 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행
|
||||
</p>
|
||||
</section>
|
||||
{/* 단계별 시각 정보 */}
|
||||
<section className="bg-white px-4 mt-8">
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_01_1.png"
|
||||
alt="사업단계 설명"
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[100%] mx-auto mb-8"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드2 */}
|
||||
<div className="card-wrap mb-24" id="card2">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 2
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
경기도 파주시 하수재이용<br />
|
||||
: 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급<br /><br />
|
||||
*LG디스플레이(P10공장), 희성전자, 엔비텍 등
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 방식 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 주요 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
|
||||
공급량 : 4만m³/일<br />
|
||||
관로 : 13.6km<br />
|
||||
사업비 : 651억원
|
||||
</p>
|
||||
</section>
|
||||
{/* 특이사항 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-0 items-start">
|
||||
{/* 좌측 이미지 */}
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_02_1.png"
|
||||
alt="적용 성공사례 지도"
|
||||
width={1000}
|
||||
height={1200}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 우측 표 */}
|
||||
<div className="space-y-3">
|
||||
{/* 하수처리수 공급 */}
|
||||
<div>
|
||||
<table className="min-w-full h-[500px]text-center border border-black">
|
||||
<colgroup>
|
||||
<col style={{ width: "20%" }} />
|
||||
<col style={{ width: "40%" }} />
|
||||
<col style={{ width: "40%" }} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={3} className="bg-[#ffc000] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">하수처리수 공급</td>
|
||||
</tr>
|
||||
<tr className="text-xl h-[30px]">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">위치</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">금촌하수</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">운정하수</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">시설용량</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">44,000㎥/일</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">60,000㎥/일</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">유입량</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">27,000㎥/일<br />자연유하</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">30,000㎥/일<br />이송펌프장</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/* 첫 화살표 (자연유하 / 관로이송) */}
|
||||
<div className="flex h-[30px] justify-between items-center my-3 px-4">
|
||||
<div className="flex text-xl items-center gap-1 text-orange-600 font-bold pl-6">
|
||||
<span>자연유하: 161m</span>
|
||||
<Image
|
||||
src="/img/wrExample/img_arrow_down_1.png"
|
||||
alt="오른쪽 화살표"
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex text-xl items-center gap-1 text-orange-600 font-bold">
|
||||
<Image
|
||||
src="/img/wrExample/img_arrow_down_1.png"
|
||||
alt="왼쪽 화살표"
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
<span>관로이송: 3.2km</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 물재이용 처리시설 */}
|
||||
<div>
|
||||
<table className="min-w-full text-center border border-black">
|
||||
<colgroup>
|
||||
<col style={{ width: "50%" }} />
|
||||
<col style={{ width: "50%" }} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={2} className="bg-[#00b050] text-xl h-[30px] text-white font-semibold text-center pt-[6px] pb-[6px] px-0">물재이용 처리시설</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">유입유량</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">57,000㎥/일</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">재이용시설</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">농축폐수처리</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">17,000㎥/일</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/* 두 번째 화살표 (공급관로) */}
|
||||
<div className="flex flex-col items-center my-3">
|
||||
<div className="flex h-[30px] items-center gap-1 text-xl text-blue-600 font-semibold">
|
||||
<span>공급관로 L=10.4km</span>
|
||||
<Image
|
||||
src="/img/wrExample/img_arrow_down_2.png"
|
||||
alt="아래 화살표"
|
||||
width={24}
|
||||
height={24}
|
||||
className="h-6 w-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수요처 공급 */}
|
||||
<div>
|
||||
<table className="min-w-full text-center border border-black">
|
||||
<colgroup>
|
||||
<col style={{ width: "50%" }} />
|
||||
<col style={{ width: "50%" }} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colSpan={2} className="bg-[#3399ff] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">수요처 공급</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">수요처</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">LG디스플레이 외 2개사</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">수요량</td>
|
||||
<td className="text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드3 */}
|
||||
<div className="card-wrap mb-24" id="card3">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 3
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
충남 보령 하수재이용<br />
|
||||
: 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 방식 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 주요 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
|
||||
공급량 : 1만m3/일<br />
|
||||
관로 : 8.7km<br />
|
||||
사업비 : 315억원(국비 216억원, 재원협의(안))
|
||||
</p>
|
||||
</section>
|
||||
{/* 특이사항 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
국내 유일 하수재이용 공업용수 공급 재정사업으로 2016년 충남 서부권 가뭄 시 지역사회 기여 차원에서 기관간 MOU (환경부, 충청남도, 보령시, 한국중부발전㈜)를 바탕
|
||||
으로 추진 되어 경제성보다 공익성을 바탕으로 국가전력 기반시설의 안정적 운영 도모
|
||||
</p>
|
||||
<div className="p-2">
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_03_1.png"
|
||||
alt="국가전력 기반시설 설명"
|
||||
width={700}
|
||||
height={350}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드4 */}
|
||||
<div className="card-wrap mb-24" id="card4">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 4
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
경기도 수원시 빗물이용과 중수도
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 내용</h2>
|
||||
<p className="text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감<br />
|
||||
※ 시설 가동중 도로 노면 온도의 7.2 ~ 26.3℃ 감소 효과 확인(21.7월, 수원시청).<br />
|
||||
다만, 가뭄기나 강우량이 적을 경우 빗물을 원활히 공급하기 다소 곤란<br /><br />
|
||||
- (중수도) 수원시청, 장안구청, 월드컵경기장에 빗물 연계 중수도를 화장실·조경용수 등으로 사용하여 수도요금 절감(연 7백~30백만원)
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 items-start p-2">
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_04_1.png"
|
||||
alt="월드컵경기장 인근도로 노면 살수 이미지"
|
||||
width={350}
|
||||
height={250}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
<span className="text-xl block text-center font-semibold my-2"><월드컵경기장 인근도로 노면 살수></span>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_04_2.png"
|
||||
alt="주유기 모양의 빗물 공급기 이미지"
|
||||
width={350}
|
||||
height={250}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
<span className="text-xl block text-center font-semibold my-2"><주유기 모양의 빗물 공급기></span>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_04_3.png"
|
||||
alt="빗물주유기에서 살수용수 공급 이미지"
|
||||
width={350}
|
||||
height={250}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
<span className="text-xl block text-center font-semibold my-2"><빗물주유기에서 살수용수 공급></span>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src="/img/wrExample/img_ex_04_4.png"
|
||||
alt="빗물 공급기 및 빗물분사장치 위치"
|
||||
width={350}
|
||||
height={250}
|
||||
className="w-[100%] mx-auto"
|
||||
/>
|
||||
<span className="text-xl block text-center font-semibold my-2"><빗물 공급기 및 빗물분사장치 위치></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function WrOverviewPage() {
|
||||
return (
|
||||
<div className="bg-[#F1F6F9]">
|
||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||
{/* 카드 1 - 물재이용 사업 추진 절차 */}
|
||||
<div className="card-wrap mb-24" id="card1">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 사업 추진 절차
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
<table className="table-fixed w-full text-center text-sm">
|
||||
<caption className="sr-only">물 재이용 추진 절차 상세 표</caption>
|
||||
<colgroup>
|
||||
<col style={{ width: "28%" }} />
|
||||
<col style={{ width: "2%" }} />
|
||||
<col style={{ width: "35%" }} />
|
||||
<col style={{ width: "5%" }} />
|
||||
<col style={{ width: "30%" }} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
{/* 1단계 */}
|
||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">타당성 조사</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 2단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">매칭시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획(안) 수립</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 3단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">하수도정보시스템 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 신청</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 4단계 */}
|
||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">국고지원 확정 통보</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 5단계 */}
|
||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">사업계획 수립(실시설계)</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 6단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">재원협의<br />(지방환경관서장)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 7단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">설치승인<br />(시도지사)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 8단계 */}
|
||||
<tr><td></td><td></td><td className="text-xl bg-gray-100 font-bold py-3">공사시행</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="text-xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 9단계 */}
|
||||
<tr><td className="text-xl bg-yellow-400 text-black font-bold py-3">(행정관) 기술검토 신청</td><td></td><td className="text-xl bg-gray-100 font-bold py-3">준공(국고정산)</td><td className="text-lg">→</td><td className="text-xl bg-green-200 font-bold py-3">기술검토<br />(한국환경공단)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드 2 - 물재이용 매칭 시스템 */}
|
||||
<div className="card-wrap mb-24" id="card2">
|
||||
<h2 className="relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 매칭 시스템이란?
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 개요 */}
|
||||
<section>
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">개요</h2>
|
||||
<p className="text-2xl font-bold text-gray-800 leading-relaxed">
|
||||
기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 구성 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">구성</h2>
|
||||
<ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
||||
<li>공급자: 하수처리장, 빗물이용시설, 중수도 등</li>
|
||||
<li>수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등</li>
|
||||
<li>시스템: 수질, 수량, 거리 기반 매칭, 중개</li>
|
||||
<li>운영주체: 한국환경공단</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* 절차 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="text-2xl font-bold text-gray-800 leading-relaxed">
|
||||
① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 기대효과 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">기대효과</h2>
|
||||
<ul className="list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
||||
<li>물 자원 수요-공급 조정을 통한 효율적 분배</li>
|
||||
<li>물 재이용시설 데이터 수집으로 체계적 관리</li>
|
||||
<li>지자체, 사업시행자 등 재이용 정보 획득 용이</li>
|
||||
</ul>
|
||||
<div className="text-center my-6">
|
||||
<Image
|
||||
src="/img/wrOverview/img_ov_01_1.png"
|
||||
alt="하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함."
|
||||
width={650}
|
||||
height={500}
|
||||
className="h-[500px] mx-auto mb-4 rounded"
|
||||
/>
|
||||
<p className="text-2xl font-bold text-gray-800 mt-10">(하수 재이용 시스템 개요도)</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
'use client';
|
||||
|
||||
import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface RegionData {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
values: number[];
|
||||
}
|
||||
|
||||
interface TableData {
|
||||
region: string;
|
||||
sewage: number;
|
||||
rainwater: number;
|
||||
reclaimed: number;
|
||||
thermal: number;
|
||||
}
|
||||
|
||||
|
||||
const WrStatusPage: React.FC = () => {
|
||||
const [selectedRegion, setSelectedRegion] = useState<string | null>(null);
|
||||
const [hoveredRegion, setHoveredRegion] = useState<string | null>(null);
|
||||
const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
const svgContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [chartData] = useState<any[]>([
|
||||
{
|
||||
label : '서울',
|
||||
하수 : 4,
|
||||
빗물 : 816,
|
||||
중수도 : 371,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '부산',
|
||||
하수 : 13,
|
||||
빗물 : 68,
|
||||
중수도 : 20,
|
||||
온배수 : 1
|
||||
},
|
||||
{
|
||||
label : '대구',
|
||||
하수 : 9,
|
||||
빗물 : 294,
|
||||
중수도 : 14,
|
||||
온배수 : 2
|
||||
},
|
||||
{
|
||||
label : '인천',
|
||||
하수 : 17,
|
||||
빗물 : 165,
|
||||
중수도 : 35,
|
||||
온배수 : 15
|
||||
},
|
||||
{
|
||||
label : '광주',
|
||||
하수 : 3,
|
||||
빗물 : 74,
|
||||
중수도 : 15,
|
||||
온배수 : 15
|
||||
},
|
||||
{
|
||||
label : '대전',
|
||||
하수 : 2,
|
||||
빗물 : 52,
|
||||
중수도 : 11,
|
||||
온배수 : 20
|
||||
},
|
||||
{
|
||||
label : '울산',
|
||||
하수 : 8,
|
||||
빗물 : 111,
|
||||
중수도 : 19,
|
||||
온배수 : 30
|
||||
},
|
||||
{
|
||||
label : '세종',
|
||||
하수 : 9,
|
||||
빗물 : 65,
|
||||
중수도 : 0,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '경기',
|
||||
하수 : 176,
|
||||
빗물 : 1009,
|
||||
중수도 : 252,
|
||||
온배수 : 40
|
||||
},
|
||||
{
|
||||
label : '강원',
|
||||
하수 : 69,
|
||||
빗물 : 69,
|
||||
중수도 : 22,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '충북',
|
||||
하수 : 38,
|
||||
빗물 : 17,
|
||||
중수도 : 91,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '충남',
|
||||
하수 : 67,
|
||||
빗물 : 54,
|
||||
중수도 : 188,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '전북',
|
||||
하수 : 55,
|
||||
빗물 : 33,
|
||||
중수도 : 220,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '전남',
|
||||
하수 : 83,
|
||||
빗물 : 16,
|
||||
중수도 : 75,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '경북',
|
||||
하수 : 81,
|
||||
빗물 : 37,
|
||||
중수도 : 70,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '경남',
|
||||
하수 : 75,
|
||||
빗물 : 27,
|
||||
중수도 : 140,
|
||||
온배수 : 0
|
||||
},
|
||||
{
|
||||
label : '제주',
|
||||
하수 : 8,
|
||||
빗물 : 47,
|
||||
중수도 : 64,
|
||||
온배수 : 0
|
||||
}
|
||||
]);
|
||||
|
||||
const chartConfig = chartData.reduce((config, element) => {
|
||||
config[element.label] = {
|
||||
label: element.label,
|
||||
color : "hsl(var(--chart-1))"
|
||||
};
|
||||
return config;
|
||||
}, {} as Record<string, { label: string; color: string; }>) satisfies ChartConfig;
|
||||
|
||||
const chartOption = {
|
||||
chartForm : CHART_FORM.BAR,
|
||||
chartConfig : chartConfig,
|
||||
chartData : chartData,
|
||||
chartTitle : '',
|
||||
chartClassName : "h-[300px] w-full",
|
||||
graphSize : 40,
|
||||
yFieldText : '개소수(건)',
|
||||
showLegend : true
|
||||
}
|
||||
|
||||
const colorLegend = [
|
||||
{ label: '하수', color: '#FF6B6B', key: 'sewage' },
|
||||
{ label: '빗물', color: '#4ECDC4', key: 'rainwater' },
|
||||
{ label: '중수도', color: '#45B7D1', key: 'reclaimed' },
|
||||
{ label: '온배수', color: '#FED16A', key: 'thermal' },
|
||||
];
|
||||
|
||||
const regionIdMap: Record<string, string> = {
|
||||
서울: '서울특별시',
|
||||
부산: '부산광역시',
|
||||
대구: '대구광역시',
|
||||
인천: '인천광역시',
|
||||
광주: '광주광역시',
|
||||
대전: '대전광역시',
|
||||
울산: '울산광역시',
|
||||
세종: '세종특별자치시',
|
||||
경기: '경기도',
|
||||
강원: '강원특별자치도',
|
||||
충북: '충청북도',
|
||||
충남: '충청남도',
|
||||
전북: '전라북도',
|
||||
전남: '전라남도',
|
||||
경북: '경상북도',
|
||||
경남: '경상남도',
|
||||
제주: '제주특별자치도'
|
||||
};
|
||||
|
||||
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 },
|
||||
|
||||
];
|
||||
|
||||
const regionData: RegionData[] = [
|
||||
{ name: '서울', x: 170, y: 140, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '인천', x: 120, y: 150, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '경기', x: 210, y: 210, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '강원', x: 330, y: 120, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '충북', x: 260, y: 280, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '충남', x: 100, y: 320, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '세종', x: 180, y: 305, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '대전', x: 195, y: 360, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '전북', x: 170, y: 450, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '전남', x: 160, y: 600, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '광주', x: 120, y: 530, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '경북', x: 400, y: 320, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '경남', x: 310, y: 530, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '대구', x: 350, y: 450, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '울산', x: 450, y: 500, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '부산', x: 420, y: 550, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
{ name: '제주', x: 60, y: 750, values: [0.3, 0.3, 0.2, 0.2] },
|
||||
];
|
||||
|
||||
const handleMarkerClick = (regionId: string) => {
|
||||
if (selectedRegion === regionId) {
|
||||
setSelectedRegion(null);
|
||||
} else {
|
||||
setSelectedRegion(regionId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const createDonutMarker = (svg: SVGSVGElement, x: number, y: number, name: string, values: number[]) => {
|
||||
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A'];
|
||||
const radius = 26;
|
||||
const innerRadius = 18;
|
||||
// const regionId = regionIdMap[name];
|
||||
const regionId = regionIdMap[name];
|
||||
if (!regionId) {
|
||||
console.warn(`Invalid region name: ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
g.setAttribute('transform', `translate(${x}, ${y})`);
|
||||
g.setAttribute('id', `marker-${name}`);
|
||||
g.style.cursor = 'pointer';
|
||||
g.setAttribute('role', 'button');
|
||||
g.setAttribute('tabindex', '0');
|
||||
g.setAttribute('aria-label', `${name} 지역 선택`);
|
||||
|
||||
const bg = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
bg.setAttribute('r', radius.toString());
|
||||
bg.setAttribute('fill', 'white');
|
||||
bg.setAttribute('pointer-events', 'all');
|
||||
g.appendChild(bg);
|
||||
|
||||
let startAngle = 0;
|
||||
values.forEach((value, idx) => {
|
||||
const endAngle = startAngle + value * 2 * Math.PI;
|
||||
const x1 = radius * Math.sin(startAngle);
|
||||
const y1 = -radius * Math.cos(startAngle);
|
||||
const x2 = radius * Math.sin(endAngle);
|
||||
const y2 = -radius * Math.cos(endAngle);
|
||||
const largeArc = endAngle - startAngle > Math.PI ? 1 : 0;
|
||||
const d = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} L 0 0 Z`;
|
||||
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.setAttribute('d', d);
|
||||
path.setAttribute('fill', colors[idx % colors.length] as string);
|
||||
g.appendChild(path);
|
||||
|
||||
startAngle = endAngle;
|
||||
});
|
||||
|
||||
const center = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
center.setAttribute('r', innerRadius.toString());
|
||||
center.setAttribute('fill', 'white');
|
||||
g.appendChild(center);
|
||||
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', '0');
|
||||
text.setAttribute('y', '2');
|
||||
text.setAttribute('text-anchor', 'middle');
|
||||
text.setAttribute('font-size', '13');
|
||||
text.setAttribute('font-weight', 'bold');
|
||||
text.setAttribute('dominant-baseline', 'middle');
|
||||
text.setAttribute('fill', 'black');
|
||||
text.textContent = name || '';
|
||||
g.appendChild(text);
|
||||
|
||||
g.addEventListener('mouseenter', (e) => {
|
||||
setHoveredRegion(regionId);
|
||||
setTooltipPosition({ x: e.clientX, y: e.clientY });
|
||||
});
|
||||
|
||||
g.addEventListener('mouseleave', () => {
|
||||
setHoveredRegion(null);
|
||||
setTooltipPosition(null);
|
||||
});
|
||||
|
||||
g.addEventListener('click', () => {
|
||||
handleMarkerClick(regionId);
|
||||
});
|
||||
|
||||
g.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
|
||||
const matrix = g.getScreenCTM();
|
||||
if (!matrix) return; // 또는 console.warn('matrix is null'); return;
|
||||
|
||||
const point = svg.createSVGPoint();
|
||||
point.x = 0;
|
||||
point.y = 0;
|
||||
const transformed = point.matrixTransform(matrix);
|
||||
|
||||
setTooltipPosition({ x: transformed.x, y: transformed.y });
|
||||
setHoveredRegion(regionId);
|
||||
handleMarkerClick(regionId);
|
||||
}
|
||||
});
|
||||
|
||||
svg.appendChild(g);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadSVG = async () => {
|
||||
try {
|
||||
const response = await fetch('/img/wrStatus/southKoreaMap.svg');
|
||||
const svgText = await response.text();
|
||||
|
||||
if (svgContainerRef.current) {
|
||||
svgContainerRef.current.innerHTML = svgText;
|
||||
|
||||
const svg = svgContainerRef.current.querySelector('svg') as SVGSVGElement;
|
||||
if (svg) {
|
||||
svg.style.width = '58%';
|
||||
svg.style.height = 'auto';
|
||||
svg.style.objectFit = 'contain';
|
||||
|
||||
// Set default fills
|
||||
const paths = svg.querySelectorAll('path');
|
||||
paths.forEach(path => {
|
||||
if (!path.getAttribute('fill') || path.getAttribute('fill') === 'none') {
|
||||
path.setAttribute('fill', '#ffffff');
|
||||
}
|
||||
});
|
||||
|
||||
// Create markers
|
||||
regionData.forEach(region => {
|
||||
createDonutMarker(svg, region.x, region.y, region.name, region.values);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SVG 로딩 실패:', error);
|
||||
if (svgContainerRef.current) {
|
||||
svgContainerRef.current.innerHTML = "<p className='text-red-500'>SVG 로딩 실패</p>";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadSVG();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const svg = svgContainerRef.current?.querySelector('svg');
|
||||
if (svg) {
|
||||
const paths = svg.querySelectorAll('path');
|
||||
paths.forEach(path => {
|
||||
const pathId = path.getAttribute('id');
|
||||
if (pathId) {
|
||||
if (selectedRegion === pathId) {
|
||||
path.setAttribute('fill', '#2563eb');
|
||||
} else {
|
||||
const original = '#ffffff';
|
||||
path.setAttribute('fill', original);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [selectedRegion]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-[#F4F7FB] text-gray-800">
|
||||
{/* 우측 콘텐츠 영역 */}
|
||||
<main className="flex-1 p-6 overflow-auto">
|
||||
{/* 상단 제목 */}
|
||||
<div className="mb-6 flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">물재이용 현황</h2>
|
||||
<div className="text-sm text-gray-500">관리자 ▼</div>
|
||||
</div>
|
||||
|
||||
{/* 콘텐츠 본문 */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{/* 지도 카드 */}
|
||||
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden max-h-[900px]">
|
||||
{/* 타이틀 영역 */}
|
||||
<div className="border-b border-[#DBE2E6] px-4 py-3">
|
||||
<h3 className="text-[12px] font-semibold">재이용시설개소수(2023)</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 개소수 조회</p>
|
||||
</div>
|
||||
|
||||
{/* 본문 콘텐츠 */}
|
||||
<div className="p-4">
|
||||
<div className="bg-[#E2EDF3] p-4 rounded shadow relative">
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
ref={svgContainerRef}
|
||||
className="w-full max-w-5xl max-h-[680px] overflow-y-auto block mx-auto"
|
||||
aria-label="대한민국 지역별 재이용시설 분포도"
|
||||
role="region"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{typeof window !== 'undefined' && hoveredRegion && tooltipPosition && (
|
||||
<div
|
||||
className="fixed bg-white text-[12px] text-gray-700 border border-gray-300 shadow-lg rounded px-3 py-2 z-50"
|
||||
style={{
|
||||
top: tooltipPosition.y + 10,
|
||||
left: tooltipPosition.x + 10,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div className="font-semibold mb-1">{hoveredRegion}</div>
|
||||
<div className="space-y-1">
|
||||
{colorLegend.map(({ label, color, key }) => (
|
||||
<div key={key} className="flex items-center space-x-2">
|
||||
<span className="text-[12px] inline-block w-3 h-3 rounded-sm" style={{ backgroundColor: color }}></span>
|
||||
<span className="text-[12px]">
|
||||
{label}:{' '}
|
||||
{tableData.find(d => d.region === hoveredRegion)?.[key as keyof TableData] ?? '-'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 범례 */}
|
||||
<div className="absolute bottom-2 right-2 bg-white rounded-lg shadow-md p-4 text-sm space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FF6B6B'}}></span>
|
||||
<span className="text-[12px] text-gray-900">하수재이용시설 (총 717건)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#4ECDC4'}}></span>
|
||||
<span className="text-[12px] text-gray-900">빗물이용시설 (총 3,730건)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#45B7D1'}}></span>
|
||||
<span className="text-[12px] text-gray-900">중수도시설 (총 1,074건)</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor: '#FED16A'}}></span>
|
||||
<span className="text-[12px] text-gray-900">온배수시설 (총 201건)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측 콘텐츠 (표 + 차트) */}
|
||||
<div className="col-span-1 flex flex-col space-y-4">
|
||||
{/* 재이용시설현황 표 */}
|
||||
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden">
|
||||
{/* 타이틀 + 버튼 영역 */}
|
||||
<div className="flex justify-between items-center border-b border-[#DBE2E6] px-4 py-3">
|
||||
<div>
|
||||
<h3 className="text-[12px] font-semibold">재이용시설현황(2023)</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 현황 비교</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="rounded border border-[#DBE2E6] shadow overflow-auto max-h-[292px] p-4">
|
||||
<table className="min-w-full text-sm text-center">
|
||||
<caption className="sr-only text-[12px]">행정구역별 하수, 빗물, 중수도, 온배수 재이용시설 수</caption>
|
||||
<thead className="bg-gray-50 text-xs text-gray-600 uppercase border-b">
|
||||
<tr>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">번호</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">시도</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">하수</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">빗물</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">중수도</th>
|
||||
<th className="text-[12px] font-semibold px-4 py-2">온배수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{tableData.map((row, index) => (
|
||||
<tr key={index} className="hover:bg-gray-50">
|
||||
<td className="text-[12px] px-4 py-2 text-center">
|
||||
{index+1}
|
||||
</td>
|
||||
<td className="text-[12px]">{row.region}</td>
|
||||
<td className="text-[12px]">{row.sewage}</td>
|
||||
<td className="text-[12px]">{row.rainwater}</td>
|
||||
<td className="text-[12px]">{row.reclaimed}</td>
|
||||
<td className="text-[12px]">{row.thermal}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 재이용통계 차트 */}
|
||||
<div className="bg-white rounded-xl shadow border border-[#DBE2E6] overflow-hidden">
|
||||
<div className="border-b border-[#DBE2E6] px-4 py-3">
|
||||
<h3 className="text-[12px] font-semibold">재이용통계(2023)</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">항목별 통계 보기</p>
|
||||
</div>
|
||||
<div className="p-4 bg-[#f9fbfd]">
|
||||
<div className="h-[305px] text-gray-500">
|
||||
<MainChart
|
||||
chartForm={chartOption.chartForm}
|
||||
chartConfig={chartOption.chartConfig}
|
||||
chartData={chartOption.chartData}
|
||||
chartTitle={chartOption.chartTitle}
|
||||
chartClassName={chartOption.chartClassName}
|
||||
graphSize={chartOption.graphSize}
|
||||
yFieldText={chartOption.yFieldText}
|
||||
showLegend={chartOption.showLegend}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WrStatusPage;
|
||||
@@ -0,0 +1,419 @@
|
||||
/**====================================
|
||||
* 보편적으로 쓰는 store 공간
|
||||
====================================**/
|
||||
|
||||
import { fetchType, getFetch } from '@/components/ui/api';
|
||||
|
||||
/**
|
||||
* STORE - TYPE
|
||||
*/
|
||||
|
||||
// selectBox
|
||||
export type SelectDataType = {
|
||||
text: string;
|
||||
val: string;
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
menuCd: number;
|
||||
menuKornNm: string;
|
||||
menuEngNm: string;
|
||||
parentId: number | null;
|
||||
sortSeq: number;
|
||||
iconNm?: string;
|
||||
children: MenuItem[] | null;
|
||||
levelNo: number;
|
||||
urlPath: string;
|
||||
menuLnkgTypeCd: string;
|
||||
menuLnkgSn: number;
|
||||
menuPath: string;
|
||||
qrcd: string;
|
||||
useYn: string;
|
||||
otptYn: string;
|
||||
newWndwYn: string;
|
||||
requiredUrlPath: boolean;
|
||||
}
|
||||
|
||||
// grid header
|
||||
export type TableUpperProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
upName?: string;
|
||||
upSequnce?: number;
|
||||
align? : string;
|
||||
row? : number;
|
||||
rowSpan?: number;
|
||||
colSpan?: number;
|
||||
}
|
||||
|
||||
// 시도 데이터
|
||||
export type sidoDataType = {
|
||||
adstDstrCd: number,
|
||||
sido: string,
|
||||
}
|
||||
|
||||
// 시군구 데이터
|
||||
export type sigunDataType = {
|
||||
adstDstrCd: number,
|
||||
gugun: string
|
||||
}
|
||||
|
||||
// 권역 데이터
|
||||
export type regionDataType = {
|
||||
rvbsnCd: string,
|
||||
rvbsn: string,
|
||||
}
|
||||
|
||||
// 단위유역 데이터
|
||||
export type unitWaterShedDataType = {
|
||||
rvbsnCd: string,
|
||||
lgbsn: string,
|
||||
}
|
||||
|
||||
// 중권역 데이터
|
||||
export type middleRegionDataType = {
|
||||
rvbsnCd: string,
|
||||
mdmbsn: string,
|
||||
}
|
||||
|
||||
// 환경청 데이터
|
||||
export type envPartDataType = {
|
||||
lclgvrnmntCd: string,
|
||||
lclgvrnmntNm: string,
|
||||
adstDliStrCd: string,
|
||||
adstDstrCd: string,
|
||||
}
|
||||
|
||||
export type envPartSelectDataType = SelectDataType & {
|
||||
adstDliStrCd: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* STORE - DATA
|
||||
*/
|
||||
|
||||
// 시도 데이터
|
||||
let selectSidoData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 시군구 데이터
|
||||
let selectSigunData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 권역 데이터
|
||||
const selectRegionData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 단위유역 데이터
|
||||
const selectUnitWaterShedData: SelectDataType[] = [
|
||||
];
|
||||
|
||||
// 환경청 데이터
|
||||
let selectEnvPartData: envPartSelectDataType[] = [
|
||||
{ text: "전체", val: '0', adstDliStrCd: '' }
|
||||
];
|
||||
|
||||
// 환경청 내 시도 데이터
|
||||
const selectEnvPartSidoData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 환경청 내 시군구 데이터
|
||||
const selectEnvPartSigunData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
// 구분 데이터
|
||||
// const selectPartData:SelectDataType[] = [
|
||||
// { text: "행정별", val: '0' },
|
||||
// { text: "유역별", val: '01' },
|
||||
// { text: "환경청별", val: '02' },
|
||||
// ];
|
||||
|
||||
// 상향 하향 데이터
|
||||
const selectUpdownData:SelectDataType[] = [
|
||||
{ text: "전체", val: '0' },
|
||||
{ text: "500이하", val: '499' },
|
||||
{ text: "500이상", val: '500' },
|
||||
];
|
||||
|
||||
// 운영 상태 데이터
|
||||
const selectOperationData:SelectDataType[] = [
|
||||
{ text: "전체", val: '0' },
|
||||
{ text: "가동중", val: '1' },
|
||||
{ text: "재가동", val: '2' },
|
||||
{ text: "가동중지", val: '3' },
|
||||
{ text: "시설폐쇄", val: '4' },
|
||||
];
|
||||
|
||||
// 시설 구분 데이터
|
||||
const selectFacilityPartData:SelectDataType[] = [
|
||||
{ text: "전체", val: '0' },
|
||||
{ text: "공공하수처리시설", val: 'SW' },
|
||||
{ text: "오수처리장", val: 'SS' },
|
||||
{ text: "개인하수처리시설", val: 'PB' },
|
||||
{ text: "분뇨처리장", val: 'NS' },
|
||||
{ text: "폐수처리장", val: 'LW' },
|
||||
{ text: "침출수처리장", val: 'LC' },
|
||||
{ text: "기타기초시설", val: 'ET' },
|
||||
];
|
||||
|
||||
// 년도 데이터
|
||||
const getYearData = () => {
|
||||
const year = new Date().getFullYear();
|
||||
const year1 = year - 1;
|
||||
const year2 = year - 2;
|
||||
const year3 = year - 3;
|
||||
const year4 = year - 4;
|
||||
const year5 = year - 5;
|
||||
return [
|
||||
{ text: year1 + "년", val: year1.toString() },
|
||||
{ text: year2 + "년", val: year2.toString() },
|
||||
{ text: year3 + "년", val: year3.toString() },
|
||||
{ text: year4 + "년", val: year4.toString() },
|
||||
{ text: year5 + "년", val: year5.toString() },
|
||||
]
|
||||
}
|
||||
|
||||
const selectSearchYear:SelectDataType[] = getYearData();
|
||||
|
||||
// 유량 기간 구분
|
||||
const selectFlowTerm : SelectDataType[] = [
|
||||
{ text: "연간", val: '0' },
|
||||
{ text: "하절기", val: '01' },
|
||||
{ text: "동절기", val: '02' },
|
||||
{ text: "청천시", val: '03' },
|
||||
{ text: "강우시", val: '04' },
|
||||
];
|
||||
|
||||
// 유량 기간 구분
|
||||
const selectFlowPanel : SelectDataType[] = [
|
||||
{ text: "전체", val: '0'},
|
||||
{ text: "3mm미만", val: '1'},
|
||||
{ text: "3~10mm미만", val: '2'},
|
||||
{ text: "10~20mm미만", val: '3'},
|
||||
{ text: "20~30mm미만", val: '4'},
|
||||
{ text: "30~40mm미만", val: '5'},
|
||||
{ text: "40~50mm미만", val: '6'},
|
||||
{ text: "50~60mm미만", val: '7'},
|
||||
{ text: "60mm이상", val: '8'},
|
||||
];
|
||||
|
||||
// 중권역 데이터
|
||||
const selectMiddleRegionData: SelectDataType[] = [
|
||||
{ text: "전체", val: '0' }
|
||||
];
|
||||
|
||||
/**
|
||||
* STORE - API
|
||||
*/
|
||||
|
||||
// 시도 API 호출 함수 추가
|
||||
const fetchSidoData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/city');
|
||||
const data = res.data;
|
||||
selectSidoData = [
|
||||
{ text: "전체", val: '0'},
|
||||
...data.map((item: sidoDataType) => ({
|
||||
text: item.sido || '',
|
||||
val: String(item.adstDstrCd) || ''
|
||||
}))
|
||||
];
|
||||
return selectSidoData;
|
||||
} catch (error) {
|
||||
console.error('시도 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectSidoData;
|
||||
}
|
||||
};
|
||||
|
||||
// 시군구 API 호출 함수 추가
|
||||
const fetchSigunData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/sgg');
|
||||
const data = res.data;
|
||||
selectSigunData = [
|
||||
{ text: "전체", val: '0' },
|
||||
...data
|
||||
.map((item: sigunDataType) => ({
|
||||
text: item.gugun || '',
|
||||
val: String(item.adstDstrCd) || ''
|
||||
}))
|
||||
];
|
||||
return selectSigunData;
|
||||
} catch (error) {
|
||||
console.error('시군구 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectSigunData;
|
||||
}
|
||||
};
|
||||
|
||||
// 권역 API 호출 함수 추가
|
||||
const fetchRegionData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/sarea');
|
||||
const data = res.data;
|
||||
return [
|
||||
{ text: "전체", val: '0'},
|
||||
...data.map((item: regionDataType) => ({
|
||||
text: item.rvbsn || '',
|
||||
val: String(item.rvbsnCd) || ''
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('권역 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectRegionData;
|
||||
}
|
||||
};
|
||||
|
||||
// 중권역 API 호출 함수 추가
|
||||
const fetchMiddleRegionData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/midArea');
|
||||
const data = res.data;
|
||||
return [
|
||||
{ text: "전체", val: '0' },
|
||||
...data
|
||||
.map((item: middleRegionDataType) => ({
|
||||
text: item.mdmbsn || '',
|
||||
val: String(item.rvbsnCd) || ''
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('중권역 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectMiddleRegionData;
|
||||
}
|
||||
};
|
||||
|
||||
// 단위유역 API 호출 함수 추가
|
||||
const fetchUnitWaterShedData = async (): Promise<SelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/largeArea');
|
||||
const data = res.data;
|
||||
return [
|
||||
{ text: "전체", val: '0' },
|
||||
...data
|
||||
.map((item: unitWaterShedDataType) => ({
|
||||
text: item.lgbsn || '',
|
||||
val: String(item.rvbsnCd) || ''
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('단위유역 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectUnitWaterShedData;
|
||||
}
|
||||
};
|
||||
|
||||
// 환경청 데이터
|
||||
const fetchEnvPartData = async (): Promise<envPartSelectDataType[]> => {
|
||||
try {
|
||||
const res = await getFetch(fetchType.COMMON, '/addr/govArea');
|
||||
const data = res.data;
|
||||
selectEnvPartData = [
|
||||
{ text: "전체", val: '0', adstDliStrCd: '' },
|
||||
...data
|
||||
.map((item: envPartDataType) => ({
|
||||
text: item.lclgvrnmntNm || '',
|
||||
val: String(item.lclgvrnmntCd) || '',
|
||||
adstDliStrCd: item.adstDliStrCd || ''
|
||||
}))
|
||||
];
|
||||
return selectEnvPartData;
|
||||
} catch (error) {
|
||||
console.error('환경청 데이터를 가져오는데 실패했습니다:', error);
|
||||
return selectEnvPartData;
|
||||
}
|
||||
};
|
||||
|
||||
/**====================================
|
||||
* Store - expaort
|
||||
====================================*/
|
||||
export type CommonAction = {
|
||||
initializeCommonData: () => Promise<void>;
|
||||
getMenuAuth: (menuCd:number) => Promise<void>;
|
||||
}
|
||||
|
||||
export type CommonType = {
|
||||
Common: {
|
||||
isInit: boolean;
|
||||
selectSidoData: SelectDataType[];
|
||||
selectSigunData: SelectDataType[];
|
||||
selectUpdownData: SelectDataType[];
|
||||
selectOperationData: SelectDataType[];
|
||||
selectFacilityPartData: SelectDataType[];
|
||||
selectSearchYear: SelectDataType[];
|
||||
selectUnitWaterShedData: SelectDataType[];
|
||||
selectRegionData: SelectDataType[];
|
||||
selectMiddleRegionData: SelectDataType[];
|
||||
selectEnvPartSidoData: SelectDataType[];
|
||||
selectEnvPartSigunData: SelectDataType[];
|
||||
selectFlowTerm: SelectDataType[];
|
||||
selectFlowPanel: SelectDataType[];
|
||||
selectEnvPartData: envPartSelectDataType[];
|
||||
btnClass: string;
|
||||
}
|
||||
}
|
||||
|
||||
export const CommonState:CommonType = {
|
||||
Common: {
|
||||
isInit: false,
|
||||
selectSidoData: selectSidoData,
|
||||
selectSigunData: selectSigunData,
|
||||
selectUpdownData: selectUpdownData,
|
||||
selectOperationData: selectOperationData,
|
||||
selectFacilityPartData: selectFacilityPartData,
|
||||
selectSearchYear: selectSearchYear,
|
||||
selectRegionData: selectRegionData,
|
||||
selectUnitWaterShedData: selectUnitWaterShedData,
|
||||
selectMiddleRegionData: selectMiddleRegionData,
|
||||
selectEnvPartSidoData : selectEnvPartSidoData,
|
||||
selectEnvPartSigunData : selectEnvPartSigunData,
|
||||
selectFlowTerm: selectFlowTerm,
|
||||
selectFlowPanel: selectFlowPanel,
|
||||
selectEnvPartData : selectEnvPartData,
|
||||
btnClass: "p-4 m-2 text-white rounded-sm text-lg font-medium"
|
||||
}
|
||||
}
|
||||
|
||||
export const CommonReducer:(set:any)=>CommonAction=(set: any) => {
|
||||
return {
|
||||
getMenuAuth : async (menuCd : number) =>{
|
||||
const response = await getFetch(
|
||||
"local",
|
||||
`/cms-service/api/v1/menu-authrts/pageinfo/${menuCd}`
|
||||
);
|
||||
set((state: CommonType) => {
|
||||
return {
|
||||
...state,
|
||||
auth: {
|
||||
mngAuthrtYn: response[0].mngAuthrtYn,
|
||||
inqAuthrtYn: response[0].mngAuthrtYn,
|
||||
regAuthrtYn: response[0].mngAuthrtYn,
|
||||
mdfcnAuthrtYn: response[0].mngAuthrtYn,
|
||||
delAuthrtYn: response[0].mngAuthrtYn,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
// 시도 변경 이벤트
|
||||
initializeCommonData: async () => {
|
||||
const promiseSet = await Promise.all([fetchSidoData(), fetchSigunData(), fetchRegionData(), fetchUnitWaterShedData(), fetchMiddleRegionData(), fetchEnvPartData()]);
|
||||
const [sidoData, sggData, regionData, unitData, midData, envData] = promiseSet;
|
||||
set((state: CommonType) => ({
|
||||
Common: {
|
||||
...state.Common,
|
||||
selectSidoData: sidoData,
|
||||
selectSigunData: sggData,
|
||||
selectRegionData: regionData,
|
||||
selectMiddleRegionData: midData,
|
||||
selectUnitWaterShedData: unitData,
|
||||
selectEnvPartData: envData
|
||||
}
|
||||
})
|
||||
)},
|
||||
}
|
||||
}
|
||||
|
||||
export type CommonStore = CommonType & CommonAction;
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
|
||||
import { type ReactNode, createContext, useContext, useRef } from 'react'
|
||||
import { useStore } from 'zustand'
|
||||
import { type DmStore, StoreZus } from './store'
|
||||
|
||||
|
||||
export type StoreApi = ReturnType<typeof StoreZus>
|
||||
|
||||
export const StoreContext = createContext<StoreApi | undefined>(
|
||||
undefined,
|
||||
)
|
||||
|
||||
export interface StoreProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const StoreProvider = ({
|
||||
children,
|
||||
}: StoreProviderProps) => {
|
||||
const storeRef = useRef<StoreApi | null>(null)
|
||||
if (!storeRef.current) {
|
||||
// Create store instance if not initialized
|
||||
storeRef.current = StoreZus() as StoreApi
|
||||
}
|
||||
return (
|
||||
<StoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</StoreContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useDmStore = <T,>(
|
||||
selector: (store: DmStore) => T,
|
||||
): T => {
|
||||
const storeContext = useContext(StoreContext)
|
||||
if (!storeContext) {
|
||||
throw new Error(`useStore must be used within StoreProvider`)
|
||||
}
|
||||
|
||||
return useStore(storeContext, selector)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# Zustand store
|
||||
## Zustand 등록방법
|
||||
**-- index.tsx --**
|
||||
### context 등록
|
||||
기본적으로 createContext를 통해 react에 context를 생성해서 react에서 사용할 수 있도록 해야함.
|
||||
``` js
|
||||
//StoreZus는 내부적으로 사용하기 위한 store파일에서 따로 생성함.
|
||||
export type StoreApi = ReturnType<typeof StoreZus>
|
||||
//react에서 사용하기 위한 context 생성.
|
||||
export const StoreContext = createContext<StoreApi | undefined>(
|
||||
undefined,
|
||||
)
|
||||
```
|
||||
|
||||
### component로 작성
|
||||
등록된 context를 component화 시키기 위한 작업
|
||||
```js
|
||||
//외부에서 호출시 <StoreProvider> 형태로 호출.
|
||||
//app이나 home에서 전체적인 사용이 되도록 작성해야함.
|
||||
export const StoreProvider = ({
|
||||
children,
|
||||
}: StoreProviderProps) => {
|
||||
const storeRef = useRef<StoreApi>(null)
|
||||
if (!storeRef.current) {
|
||||
//StoreZus는 store.tsx를 봐야함.
|
||||
storeRef.current = StoreZus()
|
||||
}
|
||||
return (
|
||||
<StoreContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</StoreContext.Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### store 등록
|
||||
react에서 사용하는 store에 현재 context 등록
|
||||
```js
|
||||
export const useWrStore = <T,>(
|
||||
selector: (store: WrStore) => T,
|
||||
): T => {
|
||||
const storeContext = useContext(StoreContext)
|
||||
if (!storeContext) {
|
||||
throw new Error(`useStore must be used within StoreProvider`)
|
||||
}
|
||||
|
||||
return useStore(storeContext, selector)
|
||||
}
|
||||
```
|
||||
|
||||
## Zustand 사용방법
|
||||
**-- store.tsx, 폴더별 index.tsx --**
|
||||
### 기본 store 생성
|
||||
index.tsx의 context에 사용되는 StoreZus를 추가하여 사용됨.
|
||||
|
||||
외부에서 생성된 state, actions를 사용 가능한 구조
|
||||
```js
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
import { tableActionsExport, tableStateExport, type tableState, type tableType } from './publicReuseFaciltyInsert/index';//외부 파일 위치.
|
||||
export type WrStore = tableType;//typescript이므로 사용되는 스크립트를 type1 & type2 형식으로 넣어야함.
|
||||
//index.tsx에서 사용하는 store 생성.
|
||||
export const StoreZus = (initState: tableState = tableStateExport) => {
|
||||
return createStore<WrStore>()((set) => {
|
||||
return {
|
||||
//아래에서 사용하는 state & actions은 외부 폴더에서 가져다가 사용.
|
||||
...initState,//외부에 있는 기본 초기값 state
|
||||
...tableActionsExport(set)//외부에 있는 actions
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
### 외부 스토어 작성 방법
|
||||
외부 스토어는 export를 어떻게 할건지만 정하면됨.
|
||||
|
||||
사용하는 store.tsx에서 import를 받기만 하면 사용가능.
|
||||
#### 타입을 외부로 빼는 방법
|
||||
```js
|
||||
export type tableState = {
|
||||
isInit: boolean;
|
||||
upHeadList: TableUpperProps[];
|
||||
publicReuseFacilityUper: PublicReuseFacility[];
|
||||
}
|
||||
type tableActions = {
|
||||
decrementList: () => void
|
||||
}
|
||||
|
||||
export type tableType = tableState & tableActions;
|
||||
```
|
||||
#### 초기값 state를 외부로 빼는 방법
|
||||
```js
|
||||
export const tableStateExport:tableState= {
|
||||
isInit:false,
|
||||
upHeadList: upHeadList,
|
||||
publicReuseFacilityUper: publicReuseFacilityUper,
|
||||
}
|
||||
```
|
||||
#### actions를 외부로 빼는 방법
|
||||
```js
|
||||
//tableActionsExport를 import 받아서 실행하여 return 된 값을 사용
|
||||
export const tableActionsExport:(set:any)=>tableActions=
|
||||
//set을 통해 react store에 등록
|
||||
(set: any) => {
|
||||
return {
|
||||
//decrementList Object 형태로 전달
|
||||
decrementList:
|
||||
//set 함수를 실행해서 react store에 등록
|
||||
()=>set(
|
||||
// store에 등록되는 함수 구조를 가지고 있음.
|
||||
(state:tableState) => {
|
||||
//actions 실질적인 로직 작성하는 곳.
|
||||
return (
|
||||
{
|
||||
upHeadList: state.upHeadList,
|
||||
publicReuseFacilityUper:
|
||||
state.publicReuseFacilityUper.slice(0, state.publicReuseFacilityUper.length - 1)
|
||||
})}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
import { fetchType, getFetch, postFetch } from "@/components/ui/api";
|
||||
import type { SelectDataType } from "@common/business_components/ui";
|
||||
import type { TableUpperProps } from "../common";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
/**====================================
|
||||
* Store - 타입
|
||||
=====================================*/
|
||||
// 수요처 정보
|
||||
export type DemandCompType = {
|
||||
index : number | '';
|
||||
dmdInfoId : string | '';
|
||||
dmdInfoFk : string | '';
|
||||
bzmnNo : string | '';
|
||||
dmdCoNm : string | '';
|
||||
daddr : string | '';
|
||||
telno : string | '';
|
||||
rgtr : string | '';
|
||||
useUsgCd : string | '';
|
||||
ctpvCd : string | '';
|
||||
sggCd : string | '';
|
||||
dmdAplyPswd : string | '';
|
||||
wntUntprc : number | '';
|
||||
dmndTmng : Date | '';
|
||||
dmndQty : number | '';
|
||||
grssClfg : number | '';
|
||||
tn : number | '';
|
||||
combResdch : number | '';
|
||||
tph : number | '';
|
||||
trbt : number | '';
|
||||
ph : number | '';
|
||||
blgcOxygDmndQnt : number | '';
|
||||
clIonQnt : number | '';
|
||||
smll : number | '';
|
||||
cdtnv : number | '';
|
||||
crmty : number | '';
|
||||
rmrk : string | '';
|
||||
reqDt : string | '';
|
||||
dmdInfoSn : number | '';
|
||||
status : string | '';
|
||||
}
|
||||
|
||||
export type DemandMatchings = {
|
||||
splyFcltFk : string | '';
|
||||
splyFcltFlg : string | '';
|
||||
pbnstRuseFcltBizFk : number | '';
|
||||
splyBgngDay : string | '';
|
||||
splyEndDay : string | '';
|
||||
splyCpct : number | '';
|
||||
splyFmYn : string | '';
|
||||
useUsgCd : string | '';
|
||||
dmdInfoSn : number | '';
|
||||
splyCndtList : string | '';
|
||||
mtchnCmptnYmd : string | '';
|
||||
rvwCmptnYmd : string | '';
|
||||
regDt : string | '';
|
||||
rgtr : string | '';
|
||||
mdfcnDt : string | '';
|
||||
mdfr : string | '';
|
||||
}
|
||||
|
||||
// 유사사례
|
||||
export type SimilarCaseType = {
|
||||
ruseBizNm : string | '';
|
||||
bizBgngDate : string | '';
|
||||
bizEndDate : string | '';
|
||||
splyFcltKnd : string | '';
|
||||
splyFcltNm : string | '';
|
||||
splyFcltCpct : string | '';
|
||||
dmdFcltNm : string | '';
|
||||
dmdFcltAddr : string | '';
|
||||
ruseFcltCpct : string | '';
|
||||
ruseFcltPplLen : string | '';
|
||||
ruseFcltUsg : string | '';
|
||||
ruseFcltSplyCpct : string | '';
|
||||
ruseFcltPrcsMsrc : string | '';
|
||||
grnteWqUsg : string | '';
|
||||
grnteWqGrssClfg : string | '';
|
||||
grnteWqCombResdch : string | '';
|
||||
grnteWqTrbt : string | '';
|
||||
grnteWqBod : string | '';
|
||||
grnteWqSmll : string | '';
|
||||
grnteWqCrmty : string | '';
|
||||
grnteWqTn : string | '';
|
||||
grnteWqTp : string | '';
|
||||
grnteWqPh : string | '';
|
||||
grnteWqClIonQnt : string | '';
|
||||
grnteWqCndctStat : string | '';
|
||||
grnteWqRmrk : string | '';
|
||||
reqBizReuseFcltCst : string | '';
|
||||
reqBizPplCst : string | '';
|
||||
reqBizArchCst : string | '';
|
||||
reqBizMchnCst : string | '';
|
||||
reqBizMtrngCst : string | '';
|
||||
reqBizGisCst : string | '';
|
||||
reqBizCctvCst : string | '';
|
||||
reqBizCmcnCst : string | '';
|
||||
reqBizWstCst : string | '';
|
||||
reqBizDesignCst : string | '';
|
||||
reqBizSpvsCst : string | '';
|
||||
reqBizSbsdCst : string | '';
|
||||
reqBizEtc : string | '';
|
||||
reqBizGrssCst : string | '';
|
||||
prcmPlanNlep : string | '';
|
||||
prcmPlanLcep : string | '';
|
||||
prcmPlanBrdnCst : string | '';
|
||||
prcmPlanEcqrRt : string | '';
|
||||
prcmPlanGrssCst : string | '';
|
||||
clclnNlep : string | '';
|
||||
clclnLcep : string | '';
|
||||
clclnBrdnCst : string | '';
|
||||
clclnEcqrRt : string | '';
|
||||
clclnGrssCst : string | '';
|
||||
}
|
||||
|
||||
// 사업비 상세정보
|
||||
export type MoneyDetailType = {
|
||||
reqBizGrssCst : string | '';
|
||||
reqBizReuseFcltCst : string | '';
|
||||
reqBizPplCst : string | '';
|
||||
reqBizArchCst : string | '';
|
||||
reqBizMchnCst : string | '';
|
||||
reqBizMtrngCst : string | '';
|
||||
reqBizGisCst : string | '';
|
||||
reqBizCctvCst : string | '';
|
||||
reqBizCmcnCst : string | '';
|
||||
reqBizWstCst : string | '';
|
||||
reqBizDesignCst : string | '';
|
||||
reqBizSpvsCst : string | '';
|
||||
reqBizSbsdCst : string | '';
|
||||
reqBizEtc : string | '';
|
||||
|
||||
/** 재원조달계획(백만원원) */
|
||||
prcmPlanGrssCst : string | '';
|
||||
prcmPlanNlep : string | '';
|
||||
prcmPlanLcep : string | '';
|
||||
prcmPlanBrdnCst : string | '';
|
||||
prcmPlanEcqrRt : string | '';
|
||||
/** 사업비 정산(백만원) */
|
||||
clclnGrssCst : string | '';
|
||||
clclnNlep : string | '';
|
||||
clclnLcep : string | '';
|
||||
clclnBrdnCst : string | '';
|
||||
clclnEcqrRt : string | '';
|
||||
}
|
||||
|
||||
/**====================================
|
||||
* Store - 데이터
|
||||
====================================*/
|
||||
// 수요처 정보
|
||||
const demandCompList : DemandCompType[] = [
|
||||
]
|
||||
|
||||
// 사용용도
|
||||
const useTypesData : SelectDataType[] = [
|
||||
{ text: '청소', val: "청소" },
|
||||
{ text: "화장실용수", val: "화장실용수" },
|
||||
{ text: "세척", val: "세척" },
|
||||
{ text: "살수", val: "살수" },
|
||||
{ text: "조경", val: "조경" },
|
||||
{ text: "농업", val: "농업" },
|
||||
{ text: "하천", val: "하천" },
|
||||
{ text: "지하수", val: "지하수" },
|
||||
{ text: "공업", val: "공업" },
|
||||
];
|
||||
|
||||
// 유사사례 모달 헤더
|
||||
export const similarCaseModalHeader: TableUpperProps[] = [
|
||||
{ id: "ruseBizNm", title: "사업명", row: 0, rowSpan: 2 },
|
||||
{ id: "bizBgngDate", title: "사업시작일", row: 0, rowSpan: 2 },
|
||||
{ id: "bizEndDate", title: "사업종료일", row: 0, rowSpan: 2 },
|
||||
{ id: "A", title: "공급처", row: 0, colSpan: 3 },
|
||||
{ id: "splyFcltKnd", title: "시설종류", row: 1 },
|
||||
{ id: "splyFcltNm", title: "공급처명", row: 1 },
|
||||
{ id: "splyFcltCpct", title: "시설용량\n(㎥/일)", row: 1 },
|
||||
{ id: "B", title: "수요처", row: 0, colSpan: 2 },
|
||||
{ id: "dmdFcltNm", title: "수요처명", row: 1 },
|
||||
{ id: "dmdFcltAddr", title: "주소", row: 1 },
|
||||
{ id: "C", title: "재이용시설", row: 0, colSpan: 5 },
|
||||
{ id: "ruseFcltCpct", title: "시설용량\n(㎥/일)", row: 1 },
|
||||
{ id: "ruseFcltPplLen", title: "관로\n(km)", row: 1 },
|
||||
{ id: "ruseFcltUsg", title: "용도", row: 1 },
|
||||
{ id: "ruseFcltSplyCpct", title: "공급량\n(㎥/일)", row: 1 },
|
||||
{ id: "ruseFcltPrcsMsrc", title: "처리공법", row: 1 },
|
||||
{ id: "D", title: "보증수질", row: 0, colSpan: 13 },
|
||||
{ id: "grnteWqUsg", title: "용도별", row: 1 },
|
||||
{ id: "grnteWqGrssClfg", title: "총대장균군수\n(개/100ml)", row: 1 },
|
||||
{ id: "grnteWqCombResdch", title: "결합잔류염소\n(mg/L)", row: 1 },
|
||||
{ id: "grnteWqTrbt", title: "탁도\n(NTU)", row: 1 },
|
||||
{ id: "grnteWqBod", title: "생물학적산소요구량\n(BOD)\n(mg/L)", row: 1 },
|
||||
{ id: "grnteWqSmll", title: "냄새\n(개/100ml)", row: 1 },
|
||||
{ id: "grnteWqCrmty", title: "색도\n(도)", row: 1 },
|
||||
{ id: "grnteWqTn", title: "총질소\n(T-N)\n(mg/L)", row: 1 },
|
||||
{ id: "grnteWqTp", title: "총인\n(T-P)\n(mg/L)", row: 1 },
|
||||
{ id: "grnteWqPh", title: "수소이온농도\n(pH)", row: 1 },
|
||||
{ id: "grnteWqClIonQnt", title: "염화물\n(mgCL/L)", row: 1 },
|
||||
{ id: "grnteWqCndctStat", title: "전기전도도\n(μS/cm)", row: 1 },
|
||||
{ id: "grnteWqRmrk", title: "비고", row: 1 },
|
||||
{ id: "reqBizGrssCst", title: "소요사업비\n(백만원)", row: 0, rowSpan: 2 },
|
||||
]
|
||||
|
||||
// 유사사례 데이터
|
||||
const similarCaseList : SimilarCaseType[] = [
|
||||
|
||||
]
|
||||
|
||||
// 사업비 상세정보 모달 헤더
|
||||
export const moneyDetailModalHeader: TableUpperProps[] = [
|
||||
{ id: "A", title: "소요사업비(백만원)", row: 0, colSpan: 14 },
|
||||
{ id: "reqBizGrssCst", title: "총", row: 1, rowSpan: 2 },
|
||||
{ id: "B", title: "토목", row: 1, colSpan: 2 },
|
||||
{ id: "reqBizReuseFcltCst", title: "재이용시설", row: 2 },
|
||||
{ id: "reqBizPplCst", title: "관로", row: 2 },
|
||||
{ id: "reqBizArchCst", title: "건축", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizMchnCst", title: "기계", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizMtrngCst", title: "전기,계측", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizGisCst", title: "GIS/DB", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizCctvCst", title: "CCTV", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizCmcnCst", title: "시공 및 준공", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizWstCst", title: "폐기물", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizDesignCst", title: "설계비", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizSpvsCst", title: "감리비", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizSbsdCst", title: "시설 부대비", row: 1, rowSpan: 2 },
|
||||
{ id: "reqBizEtc", title: "기타", row: 1, rowSpan: 2 },
|
||||
{ id: "C", title: "재원조달계획(백만원)", row: 0, colSpan: 5 },
|
||||
{ id: "prcmPlanGrssCst", title: "총", row: 1, rowSpan: 2 },
|
||||
{ id: "prcmPlanNlep", title: "국비", row: 1, rowSpan: 2 },
|
||||
{ id: "prcmPlanLcep", title: "지방비", row: 1, rowSpan: 2 },
|
||||
{ id: "prcmPlanBrdnCst", title: "원인자부담", row: 1, rowSpan: 2 },
|
||||
{ id: "prcmPlanEcqrRt", title: "국고비율", row: 1, rowSpan: 2 },
|
||||
{ id: "D", title: "사업비 정산(백만원)", row: 0, colSpan: 5 },
|
||||
{ id: "clclnGrssCst", title: "총", row: 1, rowSpan: 2 },
|
||||
{ id: "clclnNlep", title: "국비", row: 1, rowSpan: 2 },
|
||||
{ id: "clclnLcep", title: "지방비", row: 1, rowSpan: 2 },
|
||||
{ id: "clclnBrdnCst", title: "원인자부담", row: 1, rowSpan: 2 },
|
||||
{ id: "clclnEcqrRt", title: "국고비율", row: 1, rowSpan: 2 },
|
||||
]
|
||||
|
||||
// 사업비 상세정보
|
||||
const moneyDetailList : MoneyDetailType[] = [
|
||||
]
|
||||
|
||||
// 수요정보 신청현황 헤더
|
||||
export const upHeadSdmInfoStatus: TableUpperProps[] = [
|
||||
{ id: "index", title: "No."},
|
||||
{ id: "ctpvCd", title: "시도", row: 0, rowSpan: 2 },
|
||||
{ id: "sggCd", title: "시군구", row: 0, rowSpan: 2 },
|
||||
{ id: "dmdCoNm", title: "회사명", row: 0, rowSpan: 2 },
|
||||
{ id: "daddr", title: "공장주소", row: 0, rowSpan: 2 },
|
||||
{ id: "telno", title: "전화번호", row: 0, rowSpan: 2 },
|
||||
{ id: "dmndTmng", title: "신청일자", row: 0, rowSpan: 2 },
|
||||
{ id: "status", title: "진행상태", row: 0, rowSpan: 2 },
|
||||
]
|
||||
|
||||
// 수요정보 신청현황 데이터
|
||||
|
||||
/**====================================
|
||||
* Store - Api
|
||||
=====================================*/
|
||||
// 수요처 등록
|
||||
const postSdmData = async (data: DemandCompType) => {
|
||||
try {
|
||||
await postFetch("/reuseFc/request", data);
|
||||
toast.success("수요처 등록에 성공하였습니다.");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error("수요처 등록에 실패하였습니다.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 수요정보 등록
|
||||
const postSdmInfoData = async (data: DemandCompType) => {
|
||||
try {
|
||||
await postFetch("/reuseFc/request/demandInfo", data);
|
||||
toast.success('수요 정보 등록에 성공하였습니다.');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error('수요 정보 등록에 실패하였습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
const postSdmLoginData = async (data: DemandCompType) => {
|
||||
try {
|
||||
const res = await postFetch("/reuseFc/request/login", data);
|
||||
const jsonData = await res.json();
|
||||
|
||||
const status = jsonData.status;
|
||||
const loginData = jsonData.data;
|
||||
let loginSuccess = false;
|
||||
if(status === 200 && loginData != null) {
|
||||
alert("수요처 로그인에 성공하였습니다.");
|
||||
sessionStorage.setItem("user", JSON.stringify(loginData));
|
||||
loginSuccess = true;
|
||||
} else if(status === 401) {
|
||||
alert("패스워드가 틀렸습니다.");
|
||||
loginSuccess = false;
|
||||
} else {
|
||||
alert("등록된 수요처가 없습니다.");
|
||||
loginSuccess = false;
|
||||
}
|
||||
|
||||
return loginSuccess;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
alert("수요처 로그인에 실패하였습니다.");
|
||||
}
|
||||
}
|
||||
|
||||
const getDemandCompList = async (searchText: string, pageCnt: number) => {
|
||||
try {
|
||||
const url = '/reuseFc/request/' + (searchText ? searchText : '') + '/' + pageCnt;
|
||||
const res = await getFetch(fetchType.WR, url);
|
||||
return {
|
||||
data: res?.data || [],
|
||||
count: res?.count || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error+' 수요처 정보를 불러오지 못했습니다.');
|
||||
return { data: [], count: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
const getSdmInfoStatusList = async (dmdInfoId : string) => {
|
||||
try {
|
||||
const url = '/reuseFc/request/' + dmdInfoId;
|
||||
const res = await getFetch(fetchType.WR, url);
|
||||
return res?.data || [];
|
||||
} catch (error) {
|
||||
console.error(error+' 수요정보 신청현황을 불러오지 못했습니다.');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 유사사례 조회
|
||||
const fetchGetSimilarCaseList = async (part: string, value: number|string) => {
|
||||
try {
|
||||
const url = '/reuseFc/pubFcRuseCase/dm/' + part + '/' + (value ? value : 0);
|
||||
const res = await getFetch(fetchType.WR, url);
|
||||
return res?.data || [];
|
||||
} catch (error) {
|
||||
console.error(error+' 유사사례 조회에 실패하였습니다.');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**====================================
|
||||
* Store - expaort
|
||||
====================================*/
|
||||
export type SdmActions = {
|
||||
initializeSdmData: () => void;
|
||||
insertSdmData: (data: DemandCompType) => Promise<void>;
|
||||
insertSdmInfoData: (data: DemandCompType) => Promise<void>;
|
||||
getDemandCompList: (searchText: string, pageCnt: number) => Promise<void>;
|
||||
actionLogin: (data: DemandCompType) => Promise<void>;
|
||||
getSdmInfoStatusList: (dmdInfoId: string) => Promise<void>;
|
||||
getSimilarCaseList: (part: string, value: number|string) => Promise<void>;
|
||||
}
|
||||
|
||||
export type SdmType ={
|
||||
Sdm :{
|
||||
isInit: boolean;
|
||||
useTypesData: SelectDataType[];
|
||||
demandCompList: DemandCompType[];
|
||||
upHeadSimilarCaseList: TableUpperProps[];
|
||||
similarCaseList: SimilarCaseType[];
|
||||
upHeadMoneyDetailList : TableUpperProps[];
|
||||
moneyDetailList: MoneyDetailType[];
|
||||
|
||||
// 수요정보 신청현황
|
||||
sdmInfoStatusList: DemandCompType[];
|
||||
upHeadSdmInfoStatus: TableUpperProps[];
|
||||
|
||||
// 로그인관련
|
||||
successNextPage : boolean;
|
||||
successLogin: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const SdmState: SdmType = {
|
||||
Sdm:{
|
||||
isInit:false,
|
||||
useTypesData: useTypesData,
|
||||
demandCompList: demandCompList,
|
||||
upHeadSimilarCaseList: similarCaseModalHeader,
|
||||
similarCaseList: similarCaseList,
|
||||
upHeadMoneyDetailList : moneyDetailModalHeader,
|
||||
moneyDetailList: moneyDetailList,
|
||||
|
||||
// 수요정보 신청현황
|
||||
sdmInfoStatusList: [],
|
||||
upHeadSdmInfoStatus: upHeadSdmInfoStatus,
|
||||
|
||||
// 로그인관련
|
||||
successNextPage : false,
|
||||
successLogin: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const SdmReducer = (set: any): SdmActions => {
|
||||
return {
|
||||
initializeSdmData: async () => {
|
||||
},
|
||||
|
||||
insertSdmData: async (data: DemandCompType) => {
|
||||
const preRes = await getSdmInfoStatusList(data.dmdInfoId);
|
||||
if(preRes?.dmdInfoId) {
|
||||
toast.error('이미 등록된 수요처입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await postSdmData(data);
|
||||
if(res) {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
successNextPage: true,
|
||||
}
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
insertSdmInfoData: async (data: DemandCompType) => {
|
||||
postSdmInfoData(data);
|
||||
},
|
||||
|
||||
// 수요처 정보
|
||||
getDemandCompList: async (searchText: string, pageCnt: number) => {
|
||||
const res = await getDemandCompList(searchText, pageCnt);
|
||||
if(res.data.length === 0) {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
demandCompList: [],
|
||||
}
|
||||
}))
|
||||
return;
|
||||
} else {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
demandCompList: res.data,
|
||||
}
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
// 수요정보 신청현황
|
||||
getSdmInfoStatusList: async (dmdInfoId: string) => {
|
||||
const res = await getSdmInfoStatusList(dmdInfoId);
|
||||
if(res.length === 0) {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
sdmInfoStatusList: [],
|
||||
}
|
||||
}))
|
||||
return;
|
||||
}
|
||||
const sdmInfoStatusList : DemandCompType[] = [];
|
||||
|
||||
res.demandInfo.map((el: DemandCompType, index: number) => {
|
||||
const matchingInfo = res.demandMatchings.find((matching: DemandMatchings) => matching.dmdInfoSn === el.dmdInfoSn);
|
||||
const status = matchingInfo?.splyFcltFlg?.trim() === 'Sub' ? '검토' : matchingInfo?.splyFcltFlg?.trim() === 'matched' ? '매칭완료' : '신청';
|
||||
|
||||
const tmpData = {
|
||||
...el,
|
||||
index: ++index,
|
||||
bzmnNo: res.bzmnNo,
|
||||
dmdCoNm: res.dmdCoNm,
|
||||
dmdInfoId: res.dmdInfoId,
|
||||
daddr: res.daddr,
|
||||
telno: res.telno.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3'),
|
||||
rgtr: res.rgtr,
|
||||
ctpvCd: res.ctpvCd,
|
||||
sggCd: res.sggCd,
|
||||
useUsgCd: el.useUsgCd,
|
||||
dmndTmng: el.dmndTmng,
|
||||
dmdInfoSn : el.dmdInfoSn,
|
||||
status : status
|
||||
}
|
||||
sdmInfoStatusList.push(tmpData);
|
||||
})
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
sdmInfoStatusList: sdmInfoStatusList,
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
actionLogin: async (data: DemandCompType) => {
|
||||
const loginSuccess = await postSdmLoginData(data);
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
successLogin: loginSuccess,
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
||||
// 유사사례 조회
|
||||
getSimilarCaseList: async (part: string, value: number|string) => {
|
||||
const res = await fetchGetSimilarCaseList(part, value);
|
||||
if(res && res.length !== 0) {
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
similarCaseList: res,
|
||||
}
|
||||
}))
|
||||
return;
|
||||
}else{
|
||||
set((state: SdmType) => ({
|
||||
Sdm: {
|
||||
...state.Sdm,
|
||||
similarCaseList: [],
|
||||
}
|
||||
}))
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export type SdmStore = SdmType & SdmActions;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
import { CommonReducer, CommonState, type CommonStore, type CommonType } from './common';
|
||||
import { SdmReducer, SdmState, type SdmStore, type SdmType } from './sdmMng';
|
||||
|
||||
export type DmStore = CommonStore & SdmStore;
|
||||
export type DmState = CommonType & SdmType;
|
||||
|
||||
export const StoreZus = (initState: DmState= {
|
||||
...CommonState,
|
||||
...SdmState,
|
||||
}) => {
|
||||
return createStore<DmStore>()((set) => {
|
||||
return {
|
||||
...initState,
|
||||
...CommonReducer(set),
|
||||
...SdmReducer(set)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.formInsideTitle{
|
||||
background-color: gray;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.smallTitle{
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
import { Button } from "@common/components/ui";
|
||||
import Link from "next/link";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export function ToastFormUi(body:string,title:string,url:string,buttonName:string){
|
||||
|
||||
//const router = useRouter();
|
||||
|
||||
return(
|
||||
|
||||
toast.custom((t) => (
|
||||
|
||||
<div
|
||||
className={`${
|
||||
t.visible ? 'animate-enter' : 'animate-leave'
|
||||
} w-4/12 h-40 bg-white shadow-lg rounded-lg pointer-events-auto flex ring-1 ring-black ring-opacity-5`}
|
||||
>
|
||||
<div className="w-full flex-1 py-10">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 pt-0.5">
|
||||
|
||||
</div>
|
||||
<div className="ml-3 flex-1">
|
||||
<p className="text-xl font-medium underline">
|
||||
{title}
|
||||
</p>
|
||||
<p className="mt-1 text-2xl">
|
||||
{body}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex py-20">
|
||||
<Button
|
||||
size='sm'
|
||||
onClick={() => toast.dismiss(t.id)}
|
||||
>
|
||||
<Link href={url}> {buttonName}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex pl-4 pr-4 py-20">
|
||||
<Button
|
||||
onClick={() => toast.dismiss(t.id)}
|
||||
size='sm'
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//8880
|
||||
import { DEV_URL_FO, DEV_URL_WR } from "../config";
|
||||
|
||||
export const fetchType = {
|
||||
COMMON: "common",
|
||||
WR: "wr"
|
||||
};
|
||||
|
||||
interface RequestParams {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const getFetch = async (type: string, url: string) => {
|
||||
const baseUrl = type === fetchType.COMMON ? DEV_URL_FO : DEV_URL_WR;
|
||||
const response = await fetch(baseUrl + url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
const res = await response.json();
|
||||
return res;
|
||||
};
|
||||
|
||||
export const postFetch = async (url: string, params: RequestParams) => {
|
||||
const response = await fetch(DEV_URL_WR + url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
export const putFetch = async (url: string, params: RequestParams) => {
|
||||
const response = await fetch(DEV_URL_WR + url, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
export const deleteFetch = async (url: string, params: RequestParams) => {
|
||||
const response = await fetch(DEV_URL_WR + url, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useDmStore } from '@/app/store';
|
||||
import type { DemandCompType } from '@/app/store/sdmMng';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
type thisProps = {
|
||||
label: string | undefined;
|
||||
initValue: string | undefined;
|
||||
setValue : (dmdCoNm?: string) => void;
|
||||
setRtnData : (rtnData: DemandCompType[]) => void;
|
||||
}
|
||||
|
||||
export default function SearchInputText({label=undefined, initValue=undefined, setValue=()=>{}, setRtnData=()=>{}} :thisProps) {
|
||||
|
||||
const { getDemandCompList, Sdm } = useDmStore((state) => state);
|
||||
|
||||
const {
|
||||
demandCompList
|
||||
} = Sdm;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [textValue, setTextValue] = useState(initValue || '');
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setTextValue(value);
|
||||
setValue(value);
|
||||
if (value) {
|
||||
setIsOpen(true);
|
||||
getDemandCompList(value, 20);
|
||||
} else {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [getDemandCompList, setValue]);
|
||||
|
||||
const handleSuggestionClick = useCallback((dmdCoNm: string) => {
|
||||
setTextValue(dmdCoNm);
|
||||
setValue(dmdCoNm);
|
||||
setIsOpen(false);
|
||||
setRtnData(demandCompList);
|
||||
}, [demandCompList, setRtnData, setValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
<input
|
||||
type="text"
|
||||
value={textValue}
|
||||
onChange={handleInputChange}
|
||||
/* onFocus={() => setIsOpen(true)} */
|
||||
className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={`${label}을(를) 입력하세요`}
|
||||
/>
|
||||
{isOpen && (
|
||||
<div className=" z-1 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto">
|
||||
<div className="max-h-72">
|
||||
{demandCompList && demandCompList.length > 0 ? demandCompList.map((item : DemandCompType, index : number) => (
|
||||
<div
|
||||
key={index}
|
||||
className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"
|
||||
onClick={() => handleSuggestionClick(item.dmdCoNm)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleSuggestionClick(item.dmdCoNm);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.dmdInfoId.substring(10,14) !== '0000' ? (
|
||||
<>
|
||||
<span className="text-gray-500">사업자번호 : </span> {item.dmdInfoId.substring(0,10)}
|
||||
<span className="text-gray-500">종사업자번호 : </span> {item.dmdInfoId.substring(10,14)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-gray-500">사업자번호 : </span> {item.dmdInfoId.substring(0,10)}
|
||||
</>
|
||||
)}
|
||||
<span className="float-right">{item.dmdCoNm}</span>
|
||||
</div>
|
||||
)) : (
|
||||
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
|
||||
검색 결과가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PROXY_HOST, SERVER_API_FO_URL, SERVER_API_WR_URL } from "@/src/shared/config/env";
|
||||
|
||||
export const LOCAL_URL = PROXY_HOST;
|
||||
export const DEV_URL_FO = SERVER_API_FO_URL;
|
||||
export const DEV_URL_WR = SERVER_API_WR_URL;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { FormUi } from "@common/business_components/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import type { ControllerRenderProps, FieldError, FieldValues, Path } from "react-hook-form";
|
||||
|
||||
|
||||
interface AccessibleFormFieldProps<T extends FieldValues> {
|
||||
field: ControllerRenderProps<T, Path<T>>;
|
||||
label: string;
|
||||
required?: boolean;
|
||||
error?: FieldError;
|
||||
inputComponent: ReactNode;
|
||||
}
|
||||
|
||||
export function AccessibleFormField<T extends FieldValues>({
|
||||
label,
|
||||
required = false,
|
||||
error,
|
||||
inputComponent,
|
||||
}: AccessibleFormFieldProps<T>) {
|
||||
const labelText = required ? `${label} *` : label;
|
||||
|
||||
return (
|
||||
<FormUi label={labelText}>
|
||||
<div>
|
||||
{inputComponent}
|
||||
{error?.message && Array.from(new Set(error.message.split('\n'))).map((msg, idx) => (
|
||||
<p key={idx} className="text-red-500 text-sm mt-1" role="alert">
|
||||
{msg}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</FormUi>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
import ModalSimilarPage from "@/app/pages/sdmMng/sdmInfoInsert/modalSimilar/page";
|
||||
import type { FormData } from "@/app/pages/sdmMng/sdmInfoInsert/page";
|
||||
import { useDmStore } from "@/app/store";
|
||||
import { CANDIDATE, DatePicker, FormField, FormLabel, FormUi, SelectBox, UiModal } from "@common/business_components";
|
||||
import { Input } from "@common/components";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { AccessibleFormField } from "./AccessibleFormField";
|
||||
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
|
||||
import SearchInputText from "../common/searhDemandInput/component";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
|
||||
export interface SdmInfoRegFormProps {
|
||||
form: UseFormReturn<FormData>;
|
||||
onCheckboxChange?: (checked: boolean) => void;
|
||||
facilityName?: string;
|
||||
}
|
||||
|
||||
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
|
||||
|
||||
export function SdmInfoRegForm({ form, onCheckboxChange, facilityName }: SdmInfoRegFormProps) {
|
||||
const { initializeCommonData, Sdm } = useDmStore((state) => state);
|
||||
const { useTypesData } = Sdm;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [sdmNmValue, setSdmNmValue] = useState<string>(searchParams.get('dmdCoNm') || '');
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
||||
const [isSimilarCase, setIsSimilarCase] = useState(false);
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
//모달 조회에 필요한 데이터
|
||||
const [sply, setSply] = useState<number>(0);
|
||||
const [usageCd, setUsageCd] = useState<string>('청소');
|
||||
const [money, setMoney] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
initializeCommonData();
|
||||
}, [initializeCommonData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (useTypesData && useTypesData.length > 0) {
|
||||
setTimeout(() => {
|
||||
form.setValue('usePurpose', useTypesData[0]?.val?.toString() || '');
|
||||
}, 500);
|
||||
}
|
||||
}, [useTypesData, form]);
|
||||
|
||||
useEffect(() => {
|
||||
if (facilityName) {
|
||||
setSdmNmValue(facilityName);
|
||||
form.setValue('sdmNm', facilityName);
|
||||
setKey(prev => prev + 1);
|
||||
}
|
||||
}, [facilityName, form]);
|
||||
|
||||
const searchCandidate = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const searchSimilarCase = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsSimilarCase(true);
|
||||
}
|
||||
|
||||
const handleCheckboxChange = (checked: boolean) => {
|
||||
setIsBznmTotNoDisabled(checked);
|
||||
onCheckboxChange?.(checked);
|
||||
};
|
||||
|
||||
// 시설 선택시 이벤트
|
||||
const selectComp = useCallback((rtnData: DemandCompType[]) => {
|
||||
const rtnCompData = rtnData?.[0];
|
||||
form.setValue('sdmNm', rtnCompData?.dmdCoNm || '');
|
||||
}, [form]);
|
||||
|
||||
const formFields: Array<{
|
||||
name: keyof FormData;
|
||||
label: string;
|
||||
component: FieldComponent | undefined;
|
||||
formIndex: number;
|
||||
required?: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: "sdmNm",
|
||||
label: "수요처선택",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<div key={key}>
|
||||
<SearchInputText
|
||||
label="수요처명"
|
||||
setValue={(val) => {
|
||||
setSdmNmValue(val || '');
|
||||
field.onChange(val || '');
|
||||
}}
|
||||
setRtnData={selectComp}
|
||||
initValue={facilityName || sdmNmValue}
|
||||
{...field}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "demandYmd",
|
||||
label: "요구시기",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<div className="w-full">
|
||||
<DatePicker
|
||||
mode="single"
|
||||
selected={field.value as Date}
|
||||
onSelect={(date) => {
|
||||
field.onChange(date as Date);
|
||||
}}
|
||||
className="w-full"
|
||||
defaultMonth={field.value as Date}
|
||||
field={{
|
||||
value: field.value as Date,
|
||||
onChange: (date: Date | undefined) => {
|
||||
field.onChange(date as Date);
|
||||
}
|
||||
}}
|
||||
selectClass={undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "demandQnty",
|
||||
label: "요구수량(㎥/d)",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="요구수량(㎥/d)을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
setSply(Number(e.target.value));
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "usePurpose",
|
||||
label: "사용용도",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<SelectBox
|
||||
selectClass={undefined}
|
||||
label=""
|
||||
selectArray={useTypesData}
|
||||
onSelectValue={(val) => {
|
||||
field.onChange(val.toString());
|
||||
setUsageCd(val.toString());
|
||||
}}
|
||||
className=""
|
||||
value={field.value as string}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "money",
|
||||
label: "희망단가(원/m3)",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<AccessibleFormField
|
||||
field={field}
|
||||
label="희망단가(원/㎥)"
|
||||
required={true}
|
||||
error={form.formState.errors.money}
|
||||
inputComponent={
|
||||
<FormUi
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
isDisabled={isBznmTotNoDisabled}
|
||||
>
|
||||
<Input
|
||||
placeholder="희망단가(원/㎥)을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
setMoney(Number(e.target.value));
|
||||
}}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</FormUi>
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "reqWaterQlty",
|
||||
label: "요구수질",
|
||||
formIndex: 0,
|
||||
required: true,
|
||||
component: undefined
|
||||
},
|
||||
{
|
||||
name: "grssClfg",
|
||||
label: "총대장균군수(개/100ml)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="총대장균군수를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "tn",
|
||||
label: "총질소(T-N)(mg/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="총질소를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "combResdch",
|
||||
label: "결합잔류염소(mg/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="결합잔류염소를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "tph",
|
||||
label: "총인(T-P)(mg/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="총인을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "trbt",
|
||||
label: "탁도(NTU)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="탁도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "ph",
|
||||
label: "수소이온농도(pH)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="수소이온농도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "bod",
|
||||
label: "생물학적 산소요구량(BOD)(mg/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="생물학적 산소요구량을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "clIonQnt",
|
||||
label: "염화물(mgCL/L)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="염화물을 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "smll",
|
||||
label: "냄새",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="냄새를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "cdtnv",
|
||||
label: "전기전도도(㎲/cm)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="전기전도도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "crmty",
|
||||
label: "색도(도)",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="색도를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "rmrk",
|
||||
label: "비고",
|
||||
formIndex: 1,
|
||||
required: true,
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="비고를 입력하세요"
|
||||
type="text"
|
||||
className="!text-xl text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "candidateTitle",
|
||||
label: CANDIDATE || "공급처 후보지 선택(필수아님)",
|
||||
formIndex: 2,
|
||||
component: (field) => (
|
||||
<div>
|
||||
<div className="-mt-14 float-right flex gap-10">
|
||||
<Button className={'bg-gray-200'} onClick={searchCandidate}>
|
||||
후보지 조회
|
||||
</Button>
|
||||
<Button className={'bg-gray-200'} onClick={searchSimilarCase}>
|
||||
유사사례
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-center gap-10 mt-5">
|
||||
<Input
|
||||
placeholder="후보지1"
|
||||
type="text"
|
||||
className="!text-xl w-[130px] text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<Input
|
||||
placeholder="후보지2"
|
||||
type="text"
|
||||
className="!text-xl w-[130px] text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
<Input
|
||||
placeholder="후보지3"
|
||||
type="text"
|
||||
className="!text-xl w-[130px] text-gray-600"
|
||||
value={field.value as string}
|
||||
onChange={field.onChange}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
ref={field.ref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
];
|
||||
|
||||
const normalFields = formFields.filter(f => f.formIndex === 0 && f.component);
|
||||
const groupFields = formFields.filter(f => f.formIndex === 1 && f.component);
|
||||
const candidateFields = formFields.filter(f => f.formIndex === 2 && f.component);
|
||||
|
||||
return (
|
||||
<>
|
||||
{normalFields.map(({ name, label, component, required }) => (
|
||||
component ? (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
name === 'money' ? component(field) : (
|
||||
// <FormUi label={label}>
|
||||
// {component(field)}
|
||||
// </FormUi>
|
||||
<AccessibleFormField
|
||||
field={field}
|
||||
label={label}
|
||||
required={required}
|
||||
error={form.formState.errors[name]}
|
||||
inputComponent={component(field)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
) : null
|
||||
))}
|
||||
|
||||
{groupFields.length > 0 && (
|
||||
<div className="mt-10">
|
||||
<FormLabel className="text-xl">요구수질</FormLabel>
|
||||
<div className="flex flex-wrap justify-start gap-10 mt-3 mb-3">
|
||||
{groupFields.map(({ name, label, component, required }) => (
|
||||
<div key={name} className="min-w-[180px]">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
// <FormUi label={label}>
|
||||
// {component?.(field)}
|
||||
// </FormUi>
|
||||
<AccessibleFormField
|
||||
field={field}
|
||||
label={label}
|
||||
required={required}
|
||||
error={form.formState.errors[name]}
|
||||
inputComponent={component?.(field)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{candidateFields.map(({ name, label, component, required }, index) => (
|
||||
component ? (
|
||||
<FormField
|
||||
key={name + index}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
// <FormUi label={label}>
|
||||
// {component(field)}
|
||||
// </FormUi>
|
||||
<AccessibleFormField
|
||||
field={field}
|
||||
label={label}
|
||||
required={required}
|
||||
error={form.formState.errors[name]}
|
||||
inputComponent={component?.(field)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : null
|
||||
))}
|
||||
|
||||
{isSimilarCase && (
|
||||
<UiModal
|
||||
body={<ModalSimilarPage sply={sply} usageCd={usageCd} money={money} />}
|
||||
bodyClass="sm:max-w-[100vw] w-[1400px] h-[900px]"
|
||||
open={isSimilarCase}
|
||||
onOpenChange={(open) => !open && setIsSimilarCase(false)}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompType } from "@/app/store/sdmMng";
|
||||
import { BZNM_TOT_NO, FormField, FormUi } from "@common/business_components";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { sha512 } from 'js-sha512';
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, useForm, type ControllerRenderProps } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
export function SdmLoginForm() {
|
||||
type FormData = z.infer<ReturnType<typeof createFormSchema>>;
|
||||
type FieldComponent = (field: ControllerRenderProps<FormData, keyof FormData>) => JSX.Element;
|
||||
const createFormSchema = () => z.object({
|
||||
dmdInfoId: z.string()
|
||||
.min(1, "사업자등록번호를 입력해주세요")
|
||||
.regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다"),
|
||||
bznmTotNo: isBznmTotNoDisabled
|
||||
? z.string().optional()
|
||||
: z.string().length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다"),
|
||||
password: z.string()
|
||||
.min(1, "비밀번호를 입력해주세요")
|
||||
.min(8, "비밀번호는 8자 이상 입력해주세요")
|
||||
.max(16, "비밀번호는 16자 이내로 입력해주세요"),
|
||||
rgtr: z.string().optional(),
|
||||
dmdAplyPswd: z.string().optional(),
|
||||
});
|
||||
|
||||
const { actionLogin, initializeCommonData } = useDmStore((state) => state);
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
||||
const formSchema = createFormSchema();
|
||||
|
||||
useEffect(() => {
|
||||
initializeCommonData();
|
||||
}, [initializeCommonData]);
|
||||
|
||||
const defaultValues: FormData = {
|
||||
dmdInfoId: '',
|
||||
bznmTotNo: '',
|
||||
password: '',
|
||||
rgtr: '',
|
||||
dmdAplyPswd: '',
|
||||
};
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const handleCheckboxChange = (checked: boolean) => {
|
||||
setIsBznmTotNoDisabled(checked);
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
const data = form.getValues();
|
||||
const passwordStr = sha512.hex(data.password)
|
||||
data.dmdAplyPswd = passwordStr;
|
||||
const param = {
|
||||
dmdInfoId: data.dmdInfoId + (isBznmTotNoDisabled ? '0000' : data.bznmTotNo),
|
||||
dmdAplyPswd: data.dmdAplyPswd,
|
||||
}
|
||||
actionLogin(param as unknown as DemandCompType);
|
||||
};
|
||||
|
||||
const formFields: Array<{
|
||||
name: keyof FormData;
|
||||
label: string;
|
||||
component: FieldComponent | undefined;
|
||||
}> = [
|
||||
{
|
||||
name: "dmdInfoId",
|
||||
label: "사업자등록번호",
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="사업자등록번호를 입력하세요"
|
||||
type="text"
|
||||
className="!mt-5 !mb-5 !text-xl text-gray-600"
|
||||
{...field}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "bznmTotNo",
|
||||
label: BZNM_TOT_NO || "종사업자등록번호",
|
||||
component: (field) => (
|
||||
<FormUi
|
||||
label="종사업자등록번호"
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
isDisabled={isBznmTotNoDisabled}
|
||||
>
|
||||
<Input
|
||||
placeholder="여러사업장이 있을 경우 입력해주세요"
|
||||
type="text"
|
||||
className="!mt-5 !text-xl text-gray-600"
|
||||
disabled={isBznmTotNoDisabled}
|
||||
{...field}
|
||||
/>
|
||||
</FormUi>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: "password",
|
||||
label: "비밀번호",
|
||||
component: (field) => (
|
||||
<Input
|
||||
placeholder="비밀번호를 입력하세요"
|
||||
type="password"
|
||||
className="!mt-5 !mb-5 !text-xl text-gray-600"
|
||||
{...field}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<FormProvider {...form}>
|
||||
{formFields.map(({ name, label, component }) => (
|
||||
<FormField
|
||||
key={name}
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
label==='종사업자등록번호' ?
|
||||
<>
|
||||
{component && component(field)}
|
||||
</>
|
||||
: <FormUi label={label}>
|
||||
{component && component(field)}
|
||||
</FormUi>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</FormProvider>
|
||||
<div className="flex justify-center pt-4">
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
className="w-32 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
|
||||
>
|
||||
로그인
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{rules:{
|
||||
// other rules..
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
}},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,128 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config";
|
||||
|
||||
// const CLAIM_NAME = "Authorization";
|
||||
// const AUTH_USER_ID = "token-id";
|
||||
const serverApiUrl = process.env.NEXT_PUBLIC_CS_SERVER_API_URL;
|
||||
const GEOSERVER_URL = process.env.NEXT_PUBLIC_GEOSERVER_URL || "";
|
||||
const LX_GEOSERVER_URL = process.env.NEXT_PUBLIC_LX_GEOSERVER_URL || "";
|
||||
const SEWERAGE_API_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || "";
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
console.log(`[Middleware] Request URL: ${req.url}`);
|
||||
const searchParams = req.nextUrl.searchParams;
|
||||
|
||||
// if (pathname.startsWith("/server")) {
|
||||
// const queryParams = searchParams.toString();
|
||||
// const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
// queryParams ? `?${queryParams}` : ""
|
||||
// }`;
|
||||
// console.log(`[Middleware] Rewriting from '/server' path to: ${newUrl}`);
|
||||
// return NextResponse.rewrite(new URL(newUrl));
|
||||
// }
|
||||
|
||||
if (pathname.startsWith("/server")) {
|
||||
const decodedParams = Array.from(searchParams.entries())
|
||||
.map(([key, value]) => `${key}=${decodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
|
||||
console.log(' sesesesesesevereveraserwereqwrewbrtkehjwrtg server')
|
||||
const newUrl = `${serverApiUrl}${pathname.replace("/server", "")}${
|
||||
decodedParams ? `?${decodedParams}` : ""
|
||||
}`;
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
|
||||
if (pathname.startsWith("/attach-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
// const token =
|
||||
// "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhMGVjN2EwNy03NTJhLTRmN2YtODlhNi04NTRlNjNkMmUzYTAiLCJhdXRob3JpdGllcyI6IlJPTEVfQURNSU4sUk9MRV9BTk9OWU1PVVMiLCJtbmdyWW4iOiJZIiwiZXhwIjoxNzUyMTM1OTk3fQ.itGY6M6mC2iwKvEpXw2KS6354T_B_OFz4hD26ResIgwJhpwiDJCMuSXD-rZ3AarmAIKd0udpCuJR7UlvFMN2ww";
|
||||
// const userId = "a0ec7a07-752a-4f7f-89a6-854e63d2e3a0";
|
||||
const token = req.cookies.get(CLAIM_NAME)?.value;
|
||||
const userId = req.cookies.get(AUTH_USER_ID)?.value;
|
||||
const newUrl = `${serverApiUrl}${pathname}`;
|
||||
|
||||
//wr:dev: pathname /attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad
|
||||
//http://dev.onasys.co.kr:8000/attach-service/api/v1/tus/upload/5e60b70a-956e-40c3-be5a-cdd3100bf8ad
|
||||
if (token) requestHeaders.set(CLAIM_NAME, token);
|
||||
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
|
||||
|
||||
console.log(
|
||||
`[Middleware] Rewriting from '/attach-service' path to: ${newUrl}`
|
||||
);
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/user-service")) {
|
||||
const requestHeaders = new Headers(req.headers);
|
||||
const newUrl = `${serverApiUrl}${pathname}`;
|
||||
|
||||
console.log(
|
||||
`[Middleware] Rewriting from '/user-service' path to: ${newUrl}`
|
||||
);
|
||||
return NextResponse.rewrite(new URL(newUrl), {
|
||||
headers: requestHeaders
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/geoserver")) {
|
||||
let queryParams = searchParams.toString();
|
||||
const typename = searchParams.get("TYPENAME");
|
||||
console.log("typename ", typename);
|
||||
|
||||
let newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||
|
||||
if (
|
||||
typename == "hasudo:tb_il_swage_prcs_fclt" //하수처리시설
|
||||
|| typename == "hasudo:tb_ms_swage_prcs_zone" //하수처리구역
|
||||
|| typename == "hasudo:tb_ms_sbmrg_dngr_map" //침수위험지도
|
||||
|| typename == "hasudo:tb_ms_flood_dngr_map" //도시침수지도
|
||||
) {
|
||||
newUrl = `${GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
else if (
|
||||
typename == "hasudo:tb_ms_swage_ppl" //관로
|
||||
|| typename == "hasudo:tb_ms_swage_mhl" //맨홀
|
||||
|| typename == "hasudo:tb_ms_swage_pmstn" //펌프장
|
||||
|| typename == "hasudo:tb_ms_stofcbr" //우수토실
|
||||
) {
|
||||
newUrl = `${LX_GEOSERVER_URL}${pathname.replace("/geoserver", "")}${queryParams ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
else if (
|
||||
typename == "hasudo:om_dt_untrt_swg_rain" //중점우수토실
|
||||
|| typename == "hasudo:om_wqlt_station" //관측소
|
||||
|| typename == "hasudo:tb_wr_bdst_ruse_fclt" //재이용시설
|
||||
|| typename == "hasudo:tb_wr_rnwtr_utztn_fclt" //빗물이용시설
|
||||
|| typename == "hasudo:tb_wr_rswtr_fclt" //중수도시설
|
||||
|| typename == "hasudo:tb_wr_htdfl_fclt" //온배수시설
|
||||
|| typename == "hasudo:tb_wr_ruse_fclt_dmd_co" //수요처
|
||||
|| typename == "hasudo:tb_wr_tmpr_dmd_co" //잠재수요처
|
||||
){
|
||||
const url = req.nextUrl.clone();
|
||||
url.searchParams.set('TYPENAME', typename.replace("hasudo:",""));
|
||||
queryParams = url.searchParams.toString();
|
||||
newUrl = `${SEWERAGE_API_URL}/v1/map/geometry${queryParams ? `?${queryParams}` : ""}`;
|
||||
}
|
||||
|
||||
console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`);
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
if (pathname.startsWith("/sewerage-service")) {
|
||||
const queryParams = searchParams.toString();
|
||||
const newUrl = `${SEWERAGE_API_URL}${pathname.replace("/sewerage-service/api", "")}${queryParams ? `?${queryParams}` : ""
|
||||
}`;
|
||||
console.log(`[Middleware] Rewriting from '/geoserver' path to: ${newUrl}`);
|
||||
return NextResponse.rewrite(new URL(newUrl));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/server/:path*", "/user-service/:path*", "/attach-service/:path*", "/geoserver/:path*", "/sewerage-service/:path*"],
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const subDomain =
|
||||
process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:3000";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
async rewrites() {
|
||||
return {
|
||||
beforeFiles: [
|
||||
{
|
||||
source: "/_next/static/:path*",
|
||||
destination: `${subDomain}/_next/static/:path*`,
|
||||
has: [
|
||||
{
|
||||
type: "header",
|
||||
key: "referer",
|
||||
value: "(.*)/pst(.*)",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
afterFiles: [
|
||||
{
|
||||
source: "/pst/:path*",
|
||||
destination: `${subDomain}/pst/:path*`,
|
||||
},
|
||||
],
|
||||
fallback: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "dm",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/business_components": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"js-sha512": "^0.9.0",
|
||||
"lucide-react": "^0.474.0",
|
||||
"next": "15.1.7",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 838 KiB |
|
After Width: | Height: | Size: 593 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 197 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 348 B |