Merge branch 'iipbs/sn/dev' into iipbs/wr/dev
This commit is contained in:
+7
-5
@@ -4,10 +4,7 @@
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# build
|
||||
.next/
|
||||
dist/
|
||||
.idea/
|
||||
|
||||
# build
|
||||
.next
|
||||
@@ -22,9 +19,14 @@ apps/fo/.next/**/*
|
||||
#.env*
|
||||
.qodo
|
||||
|
||||
# next-agents-md
|
||||
.next-docs/
|
||||
|
||||
pnpm-lock.yaml
|
||||
settings.json
|
||||
.npmrc
|
||||
.cursor
|
||||
.agent
|
||||
|
||||
|
||||
apps/**/tsconfig.tsbuildinfo
|
||||
logs/
|
||||
|
||||
@@ -3,4 +3,8 @@ BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
if [ "$BRANCH" != "main" ]; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Branch: $BRANCH"
|
||||
echo "Changed packages (dry-run):"
|
||||
pnpm turbo run build --filter="...[origin/$BRANCH]" --dry-run
|
||||
echo "Starting build..."
|
||||
pnpm turbo run build --filter="...[origin/$BRANCH]"
|
||||
|
||||
@@ -3,3 +3,6 @@ NEXT_PUBLIC_PORT=
|
||||
NEXT_PUBLIC_PROXY_HOST=
|
||||
NEXT_PUBLIC_SITE_ID=
|
||||
NEXT_PUBLIC_SERVER_API_URL=
|
||||
|
||||
# 401 시 브라우저 리다이렉트 대상
|
||||
NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL=/auth/login
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -5,16 +5,15 @@ import { useRouter } from "next/navigation";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
// 로그인 페이지에서는 인증 체크를 건너뜀
|
||||
if (window.location.pathname === '/auth/login') {
|
||||
if (isAtLoginRedirectUrl("/auth/login")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,13 +34,8 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
setUserInfo(null);
|
||||
if (!isAtLoginRedirectUrl("/auth/login")) {
|
||||
router.push("/auth/login");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +145,11 @@ function LoginPage() {
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
유역하수도 통합플랫폼
|
||||
유역하수도통합플랫폼
|
||||
<br />
|
||||
<span>통합관리시스템</span>
|
||||
<span>하수도 포탈</span>
|
||||
</h1>
|
||||
<p>유역하수도 통합플랫폼에 오신 것을 환영합니다.</p>
|
||||
<p>유역하수도통합플랫폼 하수도 포탈에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
experimental: {
|
||||
proxyTimeout: 600000,
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
logging: {
|
||||
fetches: {
|
||||
fullUrl: true,
|
||||
},
|
||||
},
|
||||
async rewrites() {
|
||||
return {
|
||||
afterFiles: [
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.7",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -4,13 +4,17 @@ import { useEffect } from "react";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { getAuthLoginRedirectUrl, isAtLoginRedirectUrl } from "@common/network";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
if (isAtLoginRedirectUrl(getAuthLoginRedirectUrl())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
@@ -28,8 +32,6 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
+2
-1
@@ -38,4 +38,5 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.example
|
||||
/start_cs.log
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"@types/node": "^22.13.1",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import {
|
||||
ENDPOINT,
|
||||
AUTH_USER_ID,
|
||||
AUTH_LOGIN_REDIRECT_URL,
|
||||
} from "@/shared/config";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
@@ -30,14 +36,9 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
router.push("/auth/login");
|
||||
setUserInfo(null);
|
||||
if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
window.location.assign(AUTH_LOGIN_REDIRECT_URL);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -39,10 +39,10 @@ function LoginlogPage() {
|
||||
const [presetDateRange, setPresetDateRange] = useState("");
|
||||
const [dateRange, setDateRange] = useState<DateRange>(defaultDateRange);
|
||||
const [userId, setUserId] = useState("");
|
||||
const [errOcrnYn, setErrOcrnYn] = useState("N");
|
||||
const [errOcrnYn, setErrOcrnYn] = useState("ALL");
|
||||
const [searchParams, setSearchParams] = useState<LoginlogParams>({
|
||||
userId: "",
|
||||
errOcrnYn: "N",
|
||||
errOcrnYn: "",
|
||||
searchDateFrom: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
||||
sort: "lgnDt,desc",
|
||||
@@ -87,7 +87,7 @@ function LoginlogPage() {
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
userId,
|
||||
errOcrnYn,
|
||||
errOcrnYn: errOcrnYn === "ALL" ? "" : errOcrnYn,
|
||||
searchDateFrom: dateRange.from
|
||||
? dayjs(dateRange.from).format("YYYY-MM-DD")
|
||||
: "",
|
||||
@@ -102,12 +102,12 @@ function LoginlogPage() {
|
||||
|
||||
const handleReset = () => {
|
||||
setUserId("");
|
||||
setErrOcrnYn("N");
|
||||
setErrOcrnYn("ALL");
|
||||
setDateRange(defaultDateRange);
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
userId: "",
|
||||
errOcrnYn: "N",
|
||||
errOcrnYn: "",
|
||||
searchDateFrom: dayjs().subtract(7, "day").format("YYYY-MM-DD"),
|
||||
searchDateTo: dayjs().format("YYYY-MM-DD"),
|
||||
});
|
||||
@@ -180,6 +180,7 @@ function LoginlogPage() {
|
||||
<SelectValue placeholder="선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ALL">전체</SelectItem>
|
||||
<SelectItem value="N">정상</SelectItem>
|
||||
<SelectItem value="Y">오류</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
@@ -2,6 +2,8 @@ export const PORT = process.env.NEXT_PUBLIC_PORT || "3000";
|
||||
export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
export const AUTH_LOGIN_REDIRECT_URL =
|
||||
process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login";
|
||||
export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS";
|
||||
export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||
export const ACCESS_TOKEN =
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@types/node": "^22.17.0",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-next": "15.4.0",
|
||||
"eslint-config-next": "15.3.8",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { client } from "@/shared/api";
|
||||
import type { IUserInfo } from "@/shared/store";
|
||||
import { getCookie, useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { LoginApi } from "../api/login-service";
|
||||
|
||||
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import {
|
||||
ENDPOINT,
|
||||
AUTH_USER_ID,
|
||||
AUTH_LOGIN_REDIRECT_URL,
|
||||
} from "@/shared/config";
|
||||
|
||||
const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const router = useRouter();
|
||||
const { userInfo, setUserInfo } = useUserStore((state) => state);
|
||||
|
||||
async function authCheck() {
|
||||
if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = getCookie(AUTH_USER_ID);
|
||||
if (!userId) {
|
||||
throw new Error("Missing required authentication tokens");
|
||||
@@ -32,14 +36,9 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
try {
|
||||
await LoginApi.silentRefresh();
|
||||
await authCheck();
|
||||
} catch (refreshError) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
setUserInfo(null);
|
||||
router.push("/auth/login");
|
||||
setUserInfo(null);
|
||||
if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) {
|
||||
window.location.assign(AUTH_LOGIN_REDIRECT_URL);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -3,10 +3,14 @@ export const PROXY_HOST =
|
||||
process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`;
|
||||
|
||||
export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL;
|
||||
export const NEXT_PUBLIC_DC_SERVICE_API_URL = process.env.NEXT_PUBLIC_DC_SERVICE_API_URL || `http://localhost:${PORT}`;
|
||||
export const NEXT_PUBLIC_DC_SERVICE_API_URL =
|
||||
process.env.NEXT_PUBLIC_DC_SERVICE_API_URL || `http://localhost:${PORT}`;
|
||||
export const AUTH_LOGIN_REDIRECT_URL =
|
||||
process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login";
|
||||
|
||||
// Flask API URL
|
||||
export const FLASK_API_URL = process.env.NEXT_PUBLIC_FLASK_API_URL || "http://localhost:5000";
|
||||
export const FLASK_API_URL =
|
||||
process.env.NEXT_PUBLIC_FLASK_API_URL || "http://localhost:5000";
|
||||
|
||||
//export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "DC";
|
||||
export const SITE_ID = "DC";
|
||||
|
||||
+4
-4
@@ -1,12 +1,13 @@
|
||||
NEXT_PUBLIC_SITE_ID = DM
|
||||
NEXT_PUBLIC_PORT = 3015
|
||||
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_PORT = 3005
|
||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3005
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_PROXY_HOST = http://localhost:3015
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
|
||||
# NEXT_PUBLIC_FO_SERVER_API_URL = http://192.168.1.222:8883/operation-service/api/v1
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service/api/v1
|
||||
@@ -19,7 +20,6 @@ NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&ver
|
||||
NEXT_PUBLIC_GATEWAY_URL=http://con.mind-one.co.kr:11083 # 개발 서버 API
|
||||
#NEXT_PUBLIC_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 # 박이정
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
|
||||
|
||||
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
|
||||
|
||||
+10
-8
@@ -1,23 +1,25 @@
|
||||
NEXT_PUBLIC_SITE_ID = DM
|
||||
NEXT_PUBLIC_PORT = 11035
|
||||
NEXT_PUBLIC_PROXY_HOST = http://con.mind-one.co.kr:11035
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service/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_PORT = 11315
|
||||
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_PROXY_HOST = http://con.mind-one.co.kr:11315
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_DM_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service/api/v1
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
|
||||
#NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
|
||||
#NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service # 개발 서버 API
|
||||
NEXT_PUBLIC_GATEWAY_URL=http://con.mind-one.co.kr:11083 # 개발 서버 API
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
|
||||
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
|
||||
|
||||
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
|
||||
|
||||
@@ -8,16 +8,16 @@ NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
|
||||
#NEXT_PUBLIC_PROXY_HOST = https://devreuse.hasuon.or.kr
|
||||
#NEXT_PUBLIC_CM_LOGIN_PAGE_URL = https://devqueen.hasuon.or.kr/auth/login
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1
|
||||
NEXT_PUBLIC_CS_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
NEXT_PUBLIC_FO_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/operation-service/api/v1
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1
|
||||
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_WR_SERVER_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/reuse-service/api/v1
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service
|
||||
NEXT_PUBLIC_GATEWAY_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact
|
||||
NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x}
|
||||
NEXT_PUBLIC_GATEWAY_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
|
||||
|
||||
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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
|
||||
@@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
import Base from "../base";
|
||||
import "../globals.css";
|
||||
import { StoreProvider } from "../store";
|
||||
|
||||
export default function BaseLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<StoreProvider>
|
||||
<Base>{children}</Base>
|
||||
</StoreProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
"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 { DemandCompBizType } from '@/app/store/sdmMng';
|
||||
import TransValueFormat from '@/app/common/transValueFormat';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const TABLE_STYLES = {
|
||||
header: "text-center border border-[var(--border)] bg-[rgb(234,243,251)] text-[rgb(100,119,132)] whitespace-pre-line",
|
||||
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 router = useRouter();
|
||||
|
||||
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<DemandCompBizType>({} as DemandCompBizType);
|
||||
const [transformedList, setTransformedList] = useState<DemandCompBizType[]>([]);
|
||||
|
||||
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: DemandCompBizType) => ({
|
||||
...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}>{ sdmApplyDetail?.grssClfg!=-1?<TransValueFormat value={sdmApplyDetail?.grssClfg ?? ''} format="number"/>:''} {sdmApplyDetail?.grssClfg?sdmApplyDetail.grssClfg!=-1? '이하':'불검출':'-'} </TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>결합잔류염소(mg/L)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.combResdch ?? ''} format="number"/>} {sdmApplyDetail?.combResdch?'이하':'-'}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>탁도(NTU)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.trbt ?? ''} format="number"/>} {sdmApplyDetail?.trbt?'이하':'-'}</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"/>} {sdmApplyDetail?.blgcOxygDmndQnt?'이하':'-'}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>냄새</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.smll} format="number"/>} {sdmApplyDetail?.smll?'이하':'-'}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>색도(도)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.crmty ?? ''} format="number"/>} {sdmApplyDetail?.crmty?'이하':'-'}</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"/>} {sdmApplyDetail?.tn?'이하':'-'}</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"/>} {sdmApplyDetail?.tph?'이하':'-'}</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"/>} {sdmApplyDetail?.clIonQnt?'이하':'-'}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>전기전도도(㎲/cm)</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{<TransValueFormat value={sdmApplyDetail?.cdtnv ?? ''} format="number"/>} {sdmApplyDetail?.cdtnv?'이하':'-'}</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}>{ sdmApplyDetail?.splyCndtList?.[0]?.ruseUtztnFcltNm ?? ""}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지2</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.splyCndtList?.[1]?.ruseUtztnFcltNm ?? ""}</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.header}>후보지3</TableCell>
|
||||
<TableCell colSpan={1} className={TABLE_STYLES.cell}>{sdmApplyDetail?.splyCndtList?.[2]?.ruseUtztnFcltNm ?? ""}</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}>{sdmApplyDetail?.ruseUtztnFcltNm??''}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</ListDiv>
|
||||
</ContentsDiv>
|
||||
{!isLogin?
|
||||
<UiModal
|
||||
body={<SdmLogin/>}
|
||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||
open={!isLogin}
|
||||
onOpenChange={(open) => {
|
||||
if(!open){
|
||||
setIsLogin(false)
|
||||
router.push('/');
|
||||
}
|
||||
}}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
:null}
|
||||
<Toaster position="top-center" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
"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] h-full">
|
||||
<Title title="유사사례 조회" />
|
||||
<div className="overflow-x-auto h-[480px] 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 overflow-y-hidden">
|
||||
<div className="w-[130%]">
|
||||
<UiTable
|
||||
headName={"사업비 상세정보"}
|
||||
tableData={moneyDetailList}
|
||||
headlist={upHeadMoneyDetailList}
|
||||
pageSize={0}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
onPageChange={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 수요정보등록 화면
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 수요정보등록: POST /reuseFc/request/demandInfo
|
||||
*/
|
||||
import { useDmStore } from "@/app/store";
|
||||
import { sdmInfoFormSchema, type DemandCompBizType, type SdmInfoFormData } 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 { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import SdmLogin from "../sdmLogin/page";
|
||||
|
||||
|
||||
|
||||
function SdmInfoInsertContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const { insertSdmInfoData, Sdm, setSubsInfo } = useDmStore((state) => state);
|
||||
const { successLogin } = Sdm;
|
||||
const [ismoneyDisabled, setIsmoneyDisabled] = useState(false);
|
||||
const formSchema = sdmInfoFormSchema;
|
||||
|
||||
const [isLogin, setIsLogin] = useState(false);
|
||||
const [facilityName, setFacilityName] = useState<string>("");
|
||||
const [defaultValues, setDefaultValues] = useState<SdmInfoFormData>({
|
||||
sdmNm: "",
|
||||
demandYmd: new Date(),
|
||||
demandQnty: "",
|
||||
usePurpose: "",
|
||||
money: "",
|
||||
/**요구수질 */
|
||||
reqWaterQlty: "",
|
||||
grssClfg: null,
|
||||
tn: null,
|
||||
combResdch: null,
|
||||
tph: null,
|
||||
trbt: null,
|
||||
ph: null,
|
||||
bod: null,
|
||||
/**컬럼확인필요 */
|
||||
clIonQnt: null,
|
||||
smll: "",
|
||||
cdtnv: null,
|
||||
crmty: null,
|
||||
rmrk: "",
|
||||
/** 후보지 제목 */
|
||||
candidateTitle: [
|
||||
{ ruseUtztnFcltNm: "", ruseUtztnFcltCd: "" },
|
||||
{ ruseUtztnFcltNm: "", ruseUtztnFcltCd: "" },
|
||||
{ ruseUtztnFcltNm: "", ruseUtztnFcltCd: "" },
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const user = sessionStorage.getItem("user");
|
||||
|
||||
|
||||
setDefaultValues((prev) => ({
|
||||
...prev,
|
||||
sdmNm: searchParams.get("dmdCoNm") || "",
|
||||
}));
|
||||
if (searchParams.get("dmdInfoId")) {
|
||||
// const data = {
|
||||
// dmdCoNm: searchParams.get("dmdCoNm") || '',
|
||||
// ctpvCd: searchParams.get("ctpvCd") || '',
|
||||
// sggCd: searchParams.get("sggCd") || '',
|
||||
// dmdInfoId: searchParams.get("dmdInfoId") || '',
|
||||
// daddr: searchParams.get("daddr") || '',
|
||||
// telno: searchParams.get("telno") || '',
|
||||
// }
|
||||
|
||||
// sessionStorage.setItem("user", JSON.stringify(data || {}));
|
||||
}
|
||||
//sessionStorage.setItem("user", JSON.parse(user||'') || "");
|
||||
setIsLogin(user || searchParams.get("dmdInfoId") ? true : false);
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const local = localStorage.getItem("user");
|
||||
if (local) {
|
||||
setIsLogin(true);
|
||||
}
|
||||
setSubsInfo([]); // subsInfo 초기화 후보지 처음 화면 들어올때 초기화(스토어내 객체만)
|
||||
}, [setSubsInfo]);
|
||||
|
||||
const form = useForm<SdmInfoFormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues,
|
||||
mode: "onSubmit",
|
||||
});
|
||||
|
||||
const subsInfo = useDmStore((state) => state.map.subsInfo);
|
||||
|
||||
useEffect(() => {
|
||||
//map.subsInfo 스토어 객체 값이 변경되면 해당 값이 폼에 자동으로 들어가도록 수정 대신 값이 있어야함
|
||||
if (subsInfo.length > 0) {
|
||||
form.setValue("candidateTitle", subsInfo);
|
||||
}
|
||||
}, [subsInfo, form]);
|
||||
|
||||
//후보지 팝업 값 셋팅
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data?.type === "SET_SUBS_INFO") {
|
||||
setSubsInfo(event.data.payload); // zustand로 값 설정
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, [setSubsInfo]);
|
||||
|
||||
// 로그인 성공 시 모달 닫기 및 폼 업데이트
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Form state changed:", form.formState.errors);
|
||||
}, [form.formState.errors]);
|
||||
|
||||
// 체크박스 상태가 변경될 때마다 money 필드의 에러를 초기화
|
||||
useEffect(() => {
|
||||
if (ismoneyDisabled) {
|
||||
form.clearErrors("money");
|
||||
form.setValue("money", "");
|
||||
}
|
||||
}, [ismoneyDisabled, form]);
|
||||
const router = useRouter();
|
||||
const onSubmit: SubmitHandler<SdmInfoFormData> = async (data) => {
|
||||
try {
|
||||
const dmSubsInfo = data.candidateTitle?.filter(
|
||||
(item) => item.ruseUtztnFcltNm && item.ruseUtztnFcltCd
|
||||
);
|
||||
// Transform form data to madata.candidateTitletch API schema
|
||||
const apiData: DemandCompBizType = {
|
||||
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: data.grssClfg === null ? -1 : Number(data.grssClfg),
|
||||
tn: Number(data.tn) || null,
|
||||
combResdch: Number(data.combResdch) || null,
|
||||
tph: Number(data.tph) || null,
|
||||
trbt: Number(data.trbt) || null,
|
||||
ph: data.ph || "",
|
||||
blgcOxygDmndQnt: Number(data.bod) || null,
|
||||
clIonQnt: Number(data.clIonQnt) || null,
|
||||
smll: data.smll || "",
|
||||
cdtnv: Number(data.cdtnv) || null,
|
||||
crmty: Number(data.crmty) || null,
|
||||
rmrk: data.rmrk || "",
|
||||
bzmnNo: "",
|
||||
dmdAplyPswd: "",
|
||||
index: 0,
|
||||
reqDt: "",
|
||||
dmdInfoSn: 0,
|
||||
status: "",
|
||||
splyCndtList: dmSubsInfo || [],
|
||||
};
|
||||
|
||||
await insertSdmInfoData(apiData);
|
||||
setTimeout(() => {
|
||||
router.push("/");
|
||||
}, 1000); // 0.8초 후 이동
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F2F2F2]">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 underline">
|
||||
수요정보 등록
|
||||
</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
|
||||
color="sky"
|
||||
size={"sm"}
|
||||
type="submit"
|
||||
className="w-40 font-semibold py-2 px-4 rounded-md "
|
||||
>
|
||||
수요정보 등록
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isLogin ? (
|
||||
<UiModal
|
||||
body={<SdmLogin />}
|
||||
bodyClass="sm:max-w-[90vw] w-[530px] h-[430px]"
|
||||
open={!isLogin}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
toast.dismiss();
|
||||
router.push("/");
|
||||
}
|
||||
}}
|
||||
noHeaderYn="Y"
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
) : null}
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MyForm() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<SdmInfoInsertContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useDmStore } from "@/app/store";
|
||||
import SdmInsertForm from '@common/business_components/ui/sdmInsertForm';
|
||||
import type { DemandCompType } from "@common/business_components/ui/sdmInsertForm/form";
|
||||
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;
|
||||
console.log(data);
|
||||
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,
|
||||
};
|
||||
formDataRef.current = convertedData;
|
||||
insertSdmData(convertedData)
|
||||
}, [insertSdmData]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F2F2F2]">
|
||||
<div className="pt-12">
|
||||
<SdmInsertForm
|
||||
selectSidoData={selectSidoData}
|
||||
selectSigunData={selectSigunData}
|
||||
dataList={demandCompList}
|
||||
rtnValue={callback}
|
||||
rtnInputData={handleInsertSdmData}
|
||||
openNetwork={true}
|
||||
/>
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
"use client";
|
||||
/**
|
||||
* 수요처 로그아웃 화면
|
||||
*
|
||||
* 🔗 사용 API
|
||||
*
|
||||
*/
|
||||
import { useDmStore } from "@/app/store";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function SdmLogout() {
|
||||
const router = useRouter();
|
||||
const { actionLogout } = useDmStore((state) => state);
|
||||
|
||||
useEffect(() => {
|
||||
actionLogout();
|
||||
router.push("/pages/sdmMng/sdmInfoInsert");
|
||||
}, [actionLogout, router]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,339 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
|
||||
const NAVS = [
|
||||
{ id: 'card1', label: '물재이용이란' },
|
||||
{ id: 'card2', label: '물재이용 필요성' },
|
||||
{ id: 'card3', label: '빗물 재이용' },
|
||||
{ id: 'card4', label: '중수 재이용' },
|
||||
{ id: 'card5', label: '하수 재이용' },
|
||||
{ id: 'card6', label: '물재이용 목표' },
|
||||
];
|
||||
|
||||
export default function WrAboutPage() {
|
||||
const [activeIdx, setActiveIdx] = useState(0);
|
||||
const navRefs = useRef<(HTMLLIElement | null)[]>([]);
|
||||
const cardRefs = useRef<(HTMLElement | null)[]>([]);
|
||||
|
||||
// 클릭 시 이동
|
||||
const handleNavClick = (idx: number) => {
|
||||
setActiveIdx(idx);
|
||||
const targetCard = cardRefs.current[idx];
|
||||
if (targetCard) {
|
||||
targetCard.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
};
|
||||
|
||||
// 키보드 이벤트 처리
|
||||
const handleKeyDown = (e: React.KeyboardEvent, idx: number) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleNavClick(idx);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const observerOptions = {
|
||||
root: null,
|
||||
rootMargin: '-200px 0px 0px 0px', // 상단에서 200px 지점을 기준으로
|
||||
threshold: 0
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const idx = cardRefs.current.findIndex(ref => ref === entry.target);
|
||||
if (idx !== -1) {
|
||||
setActiveIdx(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
// 각 카드 요소를 관찰
|
||||
const cardsSnapshot = [...cardRefs.current];
|
||||
|
||||
cardsSnapshot.forEach((card) => {
|
||||
if (card) {
|
||||
observer.observe(card);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cardsSnapshot.forEach((card) => {
|
||||
if (card) {
|
||||
observer.unobserve(card);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='bg-[#F2F2F2]'>
|
||||
<main className="relative w-[950px] mx-auto pt-[100px] font-sans">
|
||||
{/* 사이드바 */}
|
||||
<nav className="fixed left-[290px] top-[195px] w-[210px] z-50" aria-label="물재이용 정보 섹션 내비게이션">
|
||||
<ul role="list" className="relative space-y-6">
|
||||
{NAVS.map((nav, idx) => (
|
||||
<li
|
||||
key={nav.id}
|
||||
ref={(el) => { navRefs.current[idx] = el; }}
|
||||
role="listitem"
|
||||
tabIndex={0}
|
||||
aria-current={idx === activeIdx ? 'true' : 'false'}
|
||||
aria-label={`현재 위치: ${nav.label}`}
|
||||
onClick={() => handleNavClick(idx)}
|
||||
onKeyDown={(e) => handleKeyDown(e, idx)}
|
||||
className={`font-NotoSansKR nav-item relative pl-6 text-2xl font-bold text-[#383838] cursor-pointer transition-all duration-300
|
||||
after:content-[''] after:absolute after:top-6 after:left-[0.5rem] after:w-[2px] after:h-[24px] after:bg-[#C5C5C5] last:after:hidden
|
||||
${idx === activeIdx ? 'text-4xl font-bold text-[#383838]' : ''}`}
|
||||
>
|
||||
<span className={`absolute left-[0.5rem] top-1 w-4 h-4 rounded-full -translate-x-1/2 transition-all duration-300
|
||||
${idx === activeIdx ? 'bg-blue-500 border-blue-500' : 'bg-white border-gray-400 border'}`}></span>
|
||||
{nav.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<article ref={(el) => { cardRefs.current[0] = el; }} className="card-wrap pl-[100px] mb-24 !pt-0" id="card1" aria-labelledby="card1-title">
|
||||
<h2 id="card1-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용이란
|
||||
</h2>
|
||||
<section aria-labelledby="card1-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card1-desc" className="font-NotoSansKR text-[#269DDA] text-5xl font-bold mb-8">물 재이용이 무엇인가요?</h1>
|
||||
<p className="font-NotoSansKR text-[#231815] text-2xl mb-10 leading-relaxed">
|
||||
물 재이용은 빗물, 오수, 하수처리수, 폐수처리수 및 발전소 온배수를<br />
|
||||
물 재이용시설을 이용하여 처리하고, 그 처리된 물을 생활, 공업, 농업, 조경,<br />
|
||||
하천 유지 등의 용도로 이용하는 것을 말합니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_01_1.png"
|
||||
alt="이 이미지는 물 재이용 시스템의 개요를 나타냅니다. 가정, 공공기관, 하수처리장 등에서 발생한 생활하수, 빗물, 하수처리수를 물 재이용 시설에서 처리한 후, 청소나 화장실, 농업, 살수차, 하천 유지, 공장, 공원 및 골프장 등 다양한 용도로 활용하는 과정을 도식화한 것입니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-8 rounded"
|
||||
/>
|
||||
<p className="font-NotoSansKR text-center font-normal text-2xl">
|
||||
환경부는 2011년에 '물의 재이용 촉진 및 지원에 관한 법률'을 제정하여<br />
|
||||
한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드2 */}
|
||||
<article ref={(el) => { cardRefs.current[1] = el; }} className="card-wrap pl-[100px] mb-24" id="card2" aria-labelledby="card2-title">
|
||||
<h2 id="card2-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 필요성
|
||||
</h2>
|
||||
<section aria-labelledby="card2-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card2-desc" className="font-NotoSansKR text-[#D63F1F] text-5xl font-semibold mb-8">물 재이용, 왜 필요한가요?</h1>
|
||||
<p className="font-NotoSansKR text-2xl font-normal mb-10 leading-relaxed">
|
||||
빗물, 중수, 하수처리수는 지속적으로 이용 가능한 순환자원입니다.<br />
|
||||
물 재이용을 통한 효율적인 수자원 활용은 물 스트레스국가인 우리나라에서<br />가뭄대응능력 향상 및 하천수질보전을 위해 반드시 필요한 일입니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_02_1.png"
|
||||
alt="이 이미지는 기후변화로 인한 온도 상승 추이를 설명한다. 왼쪽에는 지구 그림과 온도계가 함께 표시되어 있으며, 지구 평균 기온은 지난 100년간 약 0.9℃ 상승했음을 나타낸다. 오른쪽에는 대한민국 지도와 함께 온도계가 있으며, 지난 100년간 약 1.8℃ 상승했음을 보여준다. 이는 지구 평균보다 2배 높은 수치이며, 자료 출처는 「기후변화 평가보고서 2020, 환경부·기상청」이다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-8 w-[80%]"
|
||||
/>
|
||||
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">
|
||||
- 우리나라의 기후변화 진행속도는 세계 평균을 상회하고 있습니다.<br />
|
||||
- 기후변화로 인한 국지적인 가뭄은 더욱 자주, 더욱 심하게 일어나고 있으며, 우리의 물 안보를 위협하고 있습니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_02_2.png"
|
||||
alt="세계 평균 1인당 강수량은 15,044m³이며, 대한민국은 2,546m³로 약 1/6 수준입니다. 계절별 강수량은 봄 10%, 여름 70%, 가을 15%, 겨울 5%입니다. 이는 여름철에 강수량이 집중되는 문제를 나타냅니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-8"
|
||||
/>
|
||||
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">
|
||||
- 우리나라의 1인당 강수량은 2,546㎥ 으로 세계 평균 15,044㎥의 약 1/6 수준입니다.<br />
|
||||
- 우리나라 강수량의 70%는 여름철에 집중되어 있어 이용 가능한 수자원확보가 어렵습니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_02_3.png"
|
||||
alt="물 재이용은 환경을 보호하며 지속 가능한 발전이 가능하게 하고, 안정적인 수자원 확보, 깨끗하고 경제적인 공업용수 공급, 하천 수질 개선을 통한 생태계 회복에 기여합니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[93%] mx-auto"
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드3 */}
|
||||
<article ref={(el) => { cardRefs.current[2] = el; }} className="card-wrap pl-[100px] mb-24" id="card3" aria-labelledby="card3-title">
|
||||
<h2 id="card3-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
빗물 재이용
|
||||
</h2>
|
||||
<section aria-labelledby="card3-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card3-desc" className="font-NotoSansKR leading-snug text-[#269DDA] text-5xl font-bold mb-6">친환경 수자원 빗물,<br />편리하게 사용할 수 있어요!</h1>
|
||||
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">가뭄을 극복하고 홍수를 예방하기 위해<br />비가 내릴 때 빗물을 모아두었다가 사용합니다.</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_03_1.png"
|
||||
alt="주택 지붕에 내린 빗물을 저장조에 모아 조경용수, 화장실용수, 세척·살수용수로 재활용하는 과정을 보여줍니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-6"
|
||||
/>
|
||||
<Image
|
||||
src="/img/wrAbout/img_03_2.png"
|
||||
alt="빗물이용"
|
||||
width={200}
|
||||
height={100}
|
||||
className="mb-5 w-[20%]"
|
||||
/>
|
||||
<p className="font-NotoSansKR text-2xl leading-relaxed">
|
||||
- 의무 설치대상: 지붕면적 1,000㎡ 이상의 운동장, 체육관, 공공업무시설 등<br />
|
||||
- 설 치 기 준: 집수시설, 처리시설, 빗물 저류조(지붕 집수면적 x 0.05m 이상)<br />
|
||||
- 활 용 용 도: 골프장 등의 조경용수, 화장실용수, 세척·살수용수 등
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드4 */}
|
||||
<article ref={(el) => { cardRefs.current[3] = el; }} className="card-wrap pl-[100px] mb-24" id="card4" aria-labelledby="card4-title">
|
||||
<h2 id="card4-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
중수 재이용
|
||||
</h2>
|
||||
<section aria-labelledby="card4-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card4-desc" className="font-NotoSansKR leading-snug text-[#9771B0] text-5xl font-bold mb-6">요모조모 쓸모 많은 중수,<br />알뜰하게 재이용할 수 있어요!</h1>
|
||||
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">한번 사용한 물을 중수도 시설을 통해 재생하여<br />개별적 또는 지역적으로 재이용할 수 있습니다.</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_04_1.png"
|
||||
alt="욕실, 세면대, 주방, 세탁기 등에서 발생한 생활용수를 중수도 시설에서 처리하여 화장실 변기용수와 조경용수로 재이용하는 시스템 흐름도입니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-6"
|
||||
/>
|
||||
<Image
|
||||
src="/img/wrAbout/img_04_2.png"
|
||||
alt="중수재이용"
|
||||
width={200}
|
||||
height={100}
|
||||
className="mb-5 w-[20%]"
|
||||
/>
|
||||
<p className="font-NotoSansKR text-2xl leading-relaxed">
|
||||
- 의무 설치대상: 일정규모 이상의 대규모점포, 숙박/목욕장, 공장, 각종 개발사업 등<br />
|
||||
- 의무 재이용량: 물 사용량의 10% 이상<br />
|
||||
- 활 용 용 도: 청소·화장실용수, 세척·살수용수, 조경용수, 공업용수 등
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드5 */}
|
||||
<article ref={(el) => { cardRefs.current[4] = el; }} className="card-wrap pl-[100px] mb-24" id="card5" aria-labelledby="card5-title">
|
||||
<h2 id="card5-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
하수 재이용
|
||||
</h2>
|
||||
<section aria-labelledby="card5-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card5-desc" className="font-NotoSansKR leading-snug text-[#00818A] text-5xl font-bold mb-6">아직 할 일이 많은 하수처리수,<br />풍족하게 재이용할 수 있어요!</h1>
|
||||
<p className="font-NotoSansKR text-2xl mb-10 leading-relaxed">사용하고 버려져 공공하수처리장으로 모여든 물은<br />용도에 맞는 처리 후 재이용 할 수 있습니다.</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_05_1.png"
|
||||
alt="주택과 건물에서 발생한 하수가 공공하수처리장을 거쳐 처리된 후, 물 재이용 시설에서 정화되어 공업용수, 하천유지용수, 농업용수, 세척·살수용수로 재활용됩니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto mb-6"
|
||||
/>
|
||||
<Image
|
||||
src="/img/wrAbout/img_05_2.png"
|
||||
alt="하수처리 재이용"
|
||||
width={200}
|
||||
height={100}
|
||||
className="mb-5 w-[20%]"
|
||||
/>
|
||||
<p className="font-NotoSansKR text-2xl leading-relaxed">
|
||||
- 의무 이용대상: 공공하수처리장(시설용량 5,000㎥/일 이상)<br />
|
||||
- 의무 재이용량: 일 하수처리량의 10% 이상<br />
|
||||
- 활 용 용 도: 공업용수, 하천유지용수, 농업용수 등
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{/* 카드6 */}
|
||||
<article ref={(el) => { cardRefs.current[5] = el; }} className="card-wrap pl-[100px] mb-24" id="card6" aria-labelledby="card6-title">
|
||||
<h2 id="card6-title" className="font-NotoSansKR relative text-center text-4xl font-medium text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 목표
|
||||
</h2>
|
||||
<section aria-labelledby="card6-desc" className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px] ">
|
||||
<h1 id="card6-desc" className="font-NotoSansKR leading-snug text-[#269DDA] text-5xl font-bold mb-6">2030년 물 재이용 목표를 위해<br />모두의 관심과 노력이 필요합니다.</h1>
|
||||
<p className="font-NotoSansKR text-2xl mb-6 leading-relaxed">
|
||||
환경부는 2030년까지의 제2차 물 재이용 기본계획을 수립하여<br />한국환경공단과 함께 물 재이용 확대 및 촉진을 위해 노력하고 있습니다.
|
||||
</p>
|
||||
<Image
|
||||
src="/img/wrAbout/img_06_1.png"
|
||||
alt="지속가능한 물 재이용 정착으로 건전한 물순환 확산"
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[80%] mx-auto mb-6"
|
||||
/>
|
||||
<div className="flex gap-3 flex-wrap justify-between bg-gray-100 p-6 rounded-lg mb-8">
|
||||
<div className="flex-1 text-center">
|
||||
<Image
|
||||
src="/img/wrAbout/icon_01.png"
|
||||
alt=""
|
||||
width={70}
|
||||
height={70}
|
||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||
/>
|
||||
<h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">도시 물 순환이용<br />체계로의 전환</h3>
|
||||
<ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1">
|
||||
<li className="text-nowrap">- 지속가능한 물 순환이용체계 구축</li>
|
||||
<li>- 물 재이용시설 관리체계 강화</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<Image
|
||||
src="/img/wrAbout/icon_02.png"
|
||||
alt=""
|
||||
width={70}
|
||||
height={70}
|
||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||
/>
|
||||
<h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">유역기반 하수처리수 재이용<br />수요확대 및 공급체계 구축</h3>
|
||||
<ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1">
|
||||
<li>- 유역기반 통합물관리 연계 강화</li>
|
||||
<li>- 하수처리수 재이용 공업용수 공급 활성화</li>
|
||||
<li>- 하수처리수 재이용을 활용한 물순환 촉진</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<Image
|
||||
src="/img/wrAbout/icon_03.png"
|
||||
alt=""
|
||||
width={70}
|
||||
height={70}
|
||||
className="mx-auto mb-4 w-[auto] h-[70px]"
|
||||
/>
|
||||
<h3 className="font-NotoSansKR text-center font-medium text-nowrap mb-2 text-2xl">물 재이용 인식제고<br />및 산업발전 기반 마련</h3>
|
||||
<ul className="font-NotoSansKR text-xl text-gray-600 leading-relaxed space-y-1">
|
||||
<li>- 물 재이용 교육 및 대국민 홍보 강화</li>
|
||||
<li>- 핵심기술 육성 및 산업발전 지원 강화</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
src="/img/wrAbout/img_06_2.png"
|
||||
alt="물 재이용량은 2014년 13.3억톤에서 2030년 25.9억톤까지 증가할 것으로 예상됩니다. 그에 따른 기대 효과로는 연간 상수대체 17.9억톤, 물산업 시장성장 약 2조3,541억 원, 건천화된 73개 하천 해소, 약 20,550명의 고용 효과가 포함됩니다."
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-[90%] mx-auto"
|
||||
/>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,364 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function WrExamplePage() {
|
||||
return (
|
||||
<div className="bg-[#F2F2F2]">
|
||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 rounded-full"></span>
|
||||
물 재이용사례
|
||||
</h2>
|
||||
</div>
|
||||
<div className="bg-[#F2F2F2] text-gray-800">
|
||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||
{/* 카드 1 */}
|
||||
<div className="card-wrap mb-24" id="card1">
|
||||
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 1
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
경기권역 반도체 사업장 물 재이용 사업<br />
|
||||
: 삼성전자 반도체 사업장 하수재이용수 공급(재이용시설 설치, 공급망 구축 등)
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 배경 및 목적 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 배경 및 목적</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
삼성전자는 환경경영전략 실현과 반도체 공장의 안정적 용수 공급을 위해 하수 재이용수를 공업용수로 도입하기로 결정
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 주요 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
1단계 사업[화성·오산(12만㎥/일) → 삼성 화성·기흥(29년 공급)]<br />
|
||||
2단계 사업[수원(21만㎥/일) → 삼성 평택(30년 공급)]
|
||||
</p>
|
||||
</section>
|
||||
{/* 특이사항 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
본 사업은 광역 하수 재이용 공급망 구축의 첫 사업으로 초기 민관협의체를 구성·운영 후 관계기관(환경부, 경기도, 지자체, 공단, 수공, 삼성전자) 간 MOU 체결을 통해 사업을 추진하였으며, 반도체 사업장에 하수 재이용수를 공업용수로 공급하는 국내 첫 사업으로 위협부담 해소를 위해 사업을 단계별로 시행
|
||||
</p>
|
||||
</section>
|
||||
{/* 단계별 시각 정보 */}
|
||||
<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="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 2
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
경기도 파주시 하수재이용<br />
|
||||
: 운정·금촌하수처리시설의 하수처리수를 막공법(U/F, R/O)으로 재처리하여 파주 LCD 산업단지내 3개 업체*에 공업용수 공급<br /><br />
|
||||
*LG디스플레이(P10공장), 희성전자, 엔비텍 등
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 방식 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
민간투자사업(시행사 : 파주그린허브(주), 운영사 : 테크로스)
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 주요 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="font-NotoSansKR 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="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
하수처리수 재이용시설 자체 농축폐수 처리공정 개발 및 적용 성공사례
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-0 items-start">
|
||||
{/* 좌측 이미지 */}
|
||||
<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="font-NotoSansKR bg-[#ffc000] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">하수처리수 공급</td>
|
||||
</tr>
|
||||
<tr className="text-xl h-[30px]">
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">위치</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">금촌하수</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">운정하수</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">시설용량</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">44,000㎥/일</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">60,000㎥/일</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">유입량</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">27,000㎥/일<br />자연유하</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">30,000㎥/일<br />이송펌프장</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/* 첫 화살표 (자연유하 / 관로이송) */}
|
||||
<div className="flex h-[30px] justify-between items-center my-3 px-4">
|
||||
<div className="flex text-xl items-center gap-1 text-orange-600 font-bold pl-6">
|
||||
<span className="font-NotoSansKR">자연유하: 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 className="font-NotoSansKR">관로이송: 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="font-NotoSansKR bg-[#00b050] text-xl h-[30px] text-white font-semibold text-center pt-[6px] pb-[6px] px-0">물재이용 처리시설</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">유입유량</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">57,000㎥/일</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">재이용시설</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">농축폐수처리</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">17,000㎥/일</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{/* 두 번째 화살표 (공급관로) */}
|
||||
<div className="flex flex-col items-center my-3">
|
||||
<div className="flex h-[30px] items-center gap-1 text-xl text-blue-600 font-semibold">
|
||||
<span className="font-NotoSansKR">공급관로 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="font-NotoSansKR bg-[#3399ff] h-[30px] text-xl text-white font-semibold text-center pt-[6px] pb-[6px] px-0">수요처 공급</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">수요처</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">LG디스플레이 외 2개사</td>
|
||||
</tr>
|
||||
<tr className="text-sm">
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">수요량</td>
|
||||
<td className="font-NotoSansKR text-xl h-[30px] border-black border p-1 font-bold">40,000㎥/일</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드3 */}
|
||||
<div className="card-wrap mb-24" id="card3">
|
||||
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 3
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
충남 보령 하수재이용<br />
|
||||
: 보령·신보령화력발전소에 하루 1만톤의 탈황용수 공급을 위해 보령하수처리시설의 하수처리수 재이용 사업 추진
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 방식 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 방식</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
2017년 한국환경공단과 사업대행 위 · 수탁 협약 체결
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 주요 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
처리공정 : 전처리분리막(U/F) + 역삼투서비(R/O)<br />
|
||||
공급량 : 1만m3/일<br />
|
||||
관로 : 8.7km<br />
|
||||
사업비 : 315억원(국비 216억원, 재원협의(안))
|
||||
</p>
|
||||
</section>
|
||||
{/* 특이사항 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">특이사항</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-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="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
사업사례 4
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 사업명 */}
|
||||
<section>
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업명</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
경기도 수원시 빗물이용과 중수도
|
||||
</p>
|
||||
</section>
|
||||
{/* 사업 내용 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 내용</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-semibold text-gray-800 leading-relaxed">
|
||||
(빗물) 월드컵경기장 등 4개소에 노면 빗물분사시스템 및 빗물 공급기를 설치하여 혹서기 도시 열섬효과 및 도로 비산먼지 저감<br />
|
||||
※ 시설 가동중 도로 노면 온도의 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="font-NotoSansKR 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="font-NotoSansKR 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="font-NotoSansKR 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="font-NotoSansKR text-xl block text-center font-semibold my-2"><빗물 공급기 및 빗물분사장치 위치></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export default function WrOverviewPage() {
|
||||
return (
|
||||
<div className="bg-[#F2F2F2]">
|
||||
<div className="relative w-[950px] mx-auto pt-[50px] font-sans">
|
||||
{/* 카드 1 - 물재이용 사업 추진 절차 */}
|
||||
<div className="card-wrap mb-24" id="card1">
|
||||
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 사업 추진 절차
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] py-[50px]">
|
||||
<table className="table-fixed w-full text-center text-sm">
|
||||
<caption className="sr-only">물 재이용 추진 절차 상세 표</caption>
|
||||
<colgroup>
|
||||
<col style={{ width: "28%" }} />
|
||||
<col style={{ width: "2%" }} />
|
||||
<col style={{ width: "35%" }} />
|
||||
<col style={{ width: "5%" }} />
|
||||
<col style={{ width: "30%" }} />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
{/* 1단계 */}
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">타당성 조사</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 2단계 */}
|
||||
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">매칭시스템 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">사업계획(안) 수립</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 3단계 */}
|
||||
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">하수도정보시스템 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">국고지원 신청</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 4단계 */}
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">국고지원 확정 통보</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 5단계 */}
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">사업계획 수립(실시설계)</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 6단계 */}
|
||||
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">재원협의(지방환경관서장)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 7단계 */}
|
||||
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">설치승인(시도지사)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 8단계 */}
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">공사시행</td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td className="font-NotoSansKR text-2xl font-bold text-center">↓</td><td></td><td></td></tr>
|
||||
{/* 9단계 */}
|
||||
<tr><td className="font-NotoSansKR text-2xl bg-[#F4D62D] text-black font-bold py-5">(행정관) 기술검토 신청</td><td></td><td className="font-NotoSansKR text-[17px] bg-[#EAEFF3] font-bold py-5">준공(국고정산)</td><td className="font-NotoSansKR text-2xl font-bold">→</td><td className="font-NotoSansKR text-2xl text-[#fff] bg-[#00B076] font-bold py-5">기술검토(한국환경공단)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카드 2 - 물재이용 매칭 시스템 */}
|
||||
<div className="mt-36 card-wrap mb-24" id="card2">
|
||||
<h2 className="font-NotoSansKR relative text-center text-4xl font-bold text-gray-900 mb-6">
|
||||
<span className="absolute left-1/2 top-[-15px] -translate-x-1/2 w-24 h-2 bg-blue-500 rounded-full"></span>
|
||||
물재이용 매칭 시스템이란?
|
||||
</h2>
|
||||
<div className="bg-white border border-gray-300 rounded-xl p-[60px] pt-[70px] pb-[70px]">
|
||||
{/* 개요 */}
|
||||
<section>
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">개요</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-bold text-gray-800 leading-relaxed">
|
||||
기후변화 및 지자체의 개발속도 차이에 따라 물 자원의 수요-공급 불균형이 발생하여 빗물, 중수도, 하수처리 재처리수 등의 공급 여유가 있는 지자체의 재이용수를 부족한 지자체 수요처에 공급하여 불균형 해소를 통한 수자원 절약 및 물 자원 순환
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 구성 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">구성</h2>
|
||||
<ul className=" font-NotoSansKR list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
||||
<li>공급자: 하수처리장, 빗물이용시설, 중수도 등</li>
|
||||
<li>수요자: 산업체(냉각수), 공공시설(조경, 도로청소), 농민(농업용수) 등</li>
|
||||
<li>시스템: 수질, 수량, 거리 기반 매칭, 중개</li>
|
||||
<li>운영주체: 한국환경공단</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* 절차 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">사업 주요 내용</h2>
|
||||
<p className="font-NotoSansKR text-2xl font-bold text-gray-800 leading-relaxed">
|
||||
① 공급자 등록 → ② 수요자 조건 입력 → ③ 매칭 검토 → ④ 시스템 매칭 → ⑤ 계약·공급 이행
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 기대효과 */}
|
||||
<section className="mt-8">
|
||||
<h2 className="font-NotoSansKR text-2xl text-[#657D91] font-bold bg-[#EAEFF3] p-3 rounded mb-4">기대효과</h2>
|
||||
<ul className="font-NotoSansKR list-[square] pl-6 text-2xl font-bold text-gray-800 leading-relaxed space-y-2">
|
||||
<li>물 자원 수요-공급 조정을 통한 효율적 분배</li>
|
||||
<li>물 재이용시설 데이터 수집으로 체계적 관리</li>
|
||||
<li>지자체, 사업시행자 등 재이용 정보 획득 용이</li>
|
||||
</ul>
|
||||
<div className="text-center my-6">
|
||||
<Image
|
||||
src="/img/wrOverview/img_ov_01_1.png"
|
||||
alt="하수 재이용 시스템 흐름도. 저장탱크에서 재처리된 물이 손 씻기, 수도, 세정 등에 공급되며 재이용 활성화를 통해 최적의 물공급 방안이 마련되는 과정을 시각화함."
|
||||
width={650}
|
||||
height={500}
|
||||
className="h-[500px] mx-auto mb-4 rounded"
|
||||
/>
|
||||
<p className="font-NotoSansKR text-2xl font-bold text-gray-800 mt-10">(하수 재이용 시스템 개요도)</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,387 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { regionData, regionIdMap } from '@/app/common/comFunction';
|
||||
import { useDmStore } from '@/app/store';
|
||||
import type { DmStatusDataType } from '@/app/store/wrStatus/type';
|
||||
import { CHART_FORM, MainChart, type ChartConfig } from '@common/business_components';
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { setCookie } from "../../../../src/shared/lib";
|
||||
|
||||
setCookie("site-id-key", process.env.NEXT_PUBLIC_SITE_ID || "DM");
|
||||
|
||||
interface WrStatusChartProps {
|
||||
chartForm: string;
|
||||
chartConfig: ChartConfig;
|
||||
chartData: any;
|
||||
chartTitle: string;
|
||||
chartClassName?: string;
|
||||
graphSize?: number;
|
||||
yFieldText? : string | undefined;
|
||||
yFieldText2? : string | undefined;
|
||||
showLegend? : boolean;
|
||||
setXAxisYearTick ?: boolean;
|
||||
}
|
||||
|
||||
export default function WrStatusPage() {
|
||||
|
||||
|
||||
|
||||
|
||||
const { Common, dmStatus, getDmStatusData,initializeCommonData} = useDmStore((state) => state);
|
||||
const { year:defaultYear, statusData,facilityTotals ,chartData} = dmStatus;
|
||||
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 [chartOption, setChartOption] = useState<WrStatusChartProps>({} as WrStatusChartProps);
|
||||
|
||||
const svgContainerRef = useRef<HTMLDivElement>(null);
|
||||
const colorLegend = [
|
||||
{ label: '하수', color: '#FF6B6B', key: 'sewage' },
|
||||
{ label: '빗물', color: '#4ECDC4', key: 'rainwater' },
|
||||
{ label: '중수도', color: '#45B7D1', key: 'reclaimed' },
|
||||
];
|
||||
const facilityTotalsColor = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FED16A'];
|
||||
// const [defaultYear, setDefaultYear] = useState<string>("");
|
||||
// const defaultYear= '2023';
|
||||
|
||||
|
||||
const handleMarkerClick = (regionId: string) => {
|
||||
if (selectedRegion === regionId) {
|
||||
setSelectedRegion(null);
|
||||
} else {
|
||||
setSelectedRegion(regionId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
initializeCommonData();
|
||||
}, [initializeCommonData]);
|
||||
|
||||
const createDonutMarker = useCallback((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);
|
||||
}, [handleMarkerClick, regionIdMap]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if(Common.selectSidoData&&Common.selectSidoData.length>1) {
|
||||
getDmStatusData(Common.selectSidoData);
|
||||
}
|
||||
},[Common.selectSidoData, getDmStatusData])
|
||||
|
||||
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();
|
||||
}, [createDonutMarker, regionData]);
|
||||
|
||||
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]);
|
||||
useEffect(() => {
|
||||
if(chartData.length>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 Option = {
|
||||
chartForm : CHART_FORM.BAR,
|
||||
chartConfig : chartConfig,
|
||||
chartData : chartData,
|
||||
chartTitle : '',
|
||||
chartClassName : "w-full h-[350px] ",
|
||||
graphSize : 10,
|
||||
yFieldText : '시설(개소)',
|
||||
showLegend : true
|
||||
}
|
||||
setChartOption(Option)
|
||||
}else{
|
||||
setChartOption({} as WrStatusChartProps)
|
||||
}
|
||||
}, [chartData]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-[#F2F2F2] 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>
|
||||
|
||||
{/* 콘텐츠 본문 */}
|
||||
<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">{`재이용시설현황(${defaultYear})`}</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+'' +color} 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}:{' '}
|
||||
{statusData?.find(d => d.region === hoveredRegion)?.[key as keyof DmStatusDataType] || '-'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 범례 */}
|
||||
<div className="absolute bottom-2 right-2 bg-white rounded-lg shadow-md p-4 text-sm space-y-2">
|
||||
{facilityTotals?.map((item, index) => (
|
||||
<div key ={'fcltTotal' +index}className="flex items-center space-x-2">
|
||||
<span className="inline-block w-4 h-4 rounded-sm" style={{backgroundColor:facilityTotalsColor[index]}}></span>
|
||||
<span className="text-[12px] text-gray-900">{item}</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-[#436d85] px-4 py-3">
|
||||
<div>
|
||||
<h3 className="text-[12px] font-semibold">{`재이용시설현황(${defaultYear})`}</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설현황[개소(천m³/년)]</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>
|
||||
<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">
|
||||
{statusData?.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>
|
||||
</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">{`재이용통계(${defaultYear})`}</h3>
|
||||
<p className="text-[12px] text-[#999] font-medium">행정구역별 재이용시설 현황 개소수 그래프</p>
|
||||
</div>
|
||||
<div className="p-4 bg-[#f9fbfd]">
|
||||
<div className="h-[305px] text-gray-500">
|
||||
{chartOption?
|
||||
<MainChart
|
||||
chartForm={chartOption.chartForm}
|
||||
chartConfig={chartOption.chartConfig}
|
||||
chartData={chartOption.chartData?.filter(item=>item.label!= '')}
|
||||
chartTitle={chartOption.chartTitle}
|
||||
chartClassName={chartOption.chartClassName}
|
||||
graphSize={chartOption.graphSize}
|
||||
yFieldText={chartOption.yFieldText}
|
||||
showLegend={chartOption.showLegend}
|
||||
/>
|
||||
:<></>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+9
-4
@@ -71,16 +71,18 @@ export default function SimilarCaseModal({
|
||||
onClose,
|
||||
rows,
|
||||
detailsByCaseId,
|
||||
onSearch
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
rows: SimilarCaseRow[];
|
||||
detailsByCaseId: Record<string, SimilarCaseDetail | undefined>;
|
||||
onSearch :(criterion: Criterion) => void;
|
||||
}) {
|
||||
const titleId = useId();
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [criterion, setCriterion] = useState<Criterion>("");
|
||||
const [criterion, setCriterion] = useState<Criterion>("supplyAmt");
|
||||
const [selectedCaseId, setSelectedCaseId] = useState<string>("");
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
@@ -88,7 +90,8 @@ export default function SimilarCaseModal({
|
||||
// 모달 열릴 때 초기화
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setCriterion("");
|
||||
setCriterion("supplyAmt");
|
||||
onSearch("supplyAmt");
|
||||
setSelectedCaseId("");
|
||||
}, [open]);
|
||||
|
||||
@@ -192,12 +195,14 @@ export default function SimilarCaseModal({
|
||||
const v = e.target.value as Criterion;
|
||||
setCriterion(v);
|
||||
setSelectedCaseId("");
|
||||
|
||||
if(v){
|
||||
onSearch(v);
|
||||
}
|
||||
}}
|
||||
className="h-10 w-[180px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option value="">유사기준선택</option>
|
||||
<option value="supplyAmt">공급량</option>
|
||||
<option value="treatment">처리공법</option>
|
||||
<option value="usePurpose">이용용도</option>
|
||||
</select>
|
||||
</div>
|
||||
+3
@@ -633,6 +633,9 @@ export default function DemandRegisterPage() {
|
||||
onClose={() => setOpenSimilar(false)}
|
||||
rows={similarRows}
|
||||
detailsByCaseId={similarDetailsByCaseId}
|
||||
onSearch={async (criterion) => {
|
||||
console.log(criterion);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
+8
-2
@@ -3,6 +3,7 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import SubPageHeader from "../../_components/SubPageHeader";
|
||||
//import { useDmStore } from "@/app/store";
|
||||
|
||||
type WaterDetail = {
|
||||
toc: string;
|
||||
@@ -114,12 +115,17 @@ type PairField = { label: string; value: string };
|
||||
export default function DemandStatusPage() {
|
||||
const rows = useMemo(() => MOCK, []);
|
||||
|
||||
// ✅ rows가 빈 경우도 대비 (런타임 오류 방지)
|
||||
// rows가 빈 경우도 대비 (런타임 오류 방지)
|
||||
const [selectedId, setSelectedId] = useState<number | null>(rows[0]?.id ?? null);
|
||||
|
||||
// ✅ 상세영역 스크롤 이동용
|
||||
// 상세영역 스크롤 이동용
|
||||
const detailRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// update
|
||||
/* const updateSdmInfoData = useDmStore(
|
||||
(state) => state.updateSdmInfoData
|
||||
); */
|
||||
|
||||
// ✅ selected 계산 안정화
|
||||
const selected = useMemo(() => {
|
||||
if (!rows.length) return null;
|
||||
@@ -29,10 +29,15 @@ const Map = () => {
|
||||
ruseUtztnFcltCd: fclt.pbnstRuseFcltId,
|
||||
}));
|
||||
|
||||
if (!window.opener || typeof window.opener.postMessage !== "function") {
|
||||
alert("부모 창을 찾을 수 없습니다. 팝업이 'noopener'로 열렸거나, 보안 정책으로 차단되었을 수 있어요.");
|
||||
return;
|
||||
}
|
||||
|
||||
window.opener.postMessage(
|
||||
{
|
||||
type: "SET_SUBS_INFO",
|
||||
payload: subsInfoList, // 또는 필요한 데이터
|
||||
payload: subsInfoList, // 배열로 전송 (최대 3개)
|
||||
},
|
||||
"*"
|
||||
);
|
||||
@@ -52,9 +57,23 @@ const Map = () => {
|
||||
|
||||
//하수처리시설코드로 재이용시설 목록 조회
|
||||
const reuseData = async (ruseFcltPk: any) => {
|
||||
if (!ruseFcltPk) {
|
||||
console.warn("[reuseData] ruseFcltPk가 없습니다.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.log("[reuseData] API 호출 - ruseFcltPk:", ruseFcltPk);
|
||||
const response = await mapService.getPbnstRuseFclts(ruseFcltPk);
|
||||
const data = response.data || [];
|
||||
console.log("[reuseData] API 응답 전체:", response);
|
||||
// 응답이 배열이면 바로 사용, 객체면 .data 추출
|
||||
const data = Array.isArray(response)
|
||||
? response
|
||||
: Array.isArray(response?.data)
|
||||
? response.data
|
||||
: response?.data
|
||||
? [response.data]
|
||||
: [];
|
||||
console.log("[reuseData] 파싱된 data:", data);
|
||||
setSelectedFclts((prev) => {
|
||||
const newItems = data.filter(
|
||||
(item: any) =>
|
||||
@@ -62,7 +81,7 @@ const Map = () => {
|
||||
Object.keys(item).length > 0 &&
|
||||
!prev.some((f) => f.pbnstRuseFcltId === item.pbnstRuseFcltId)
|
||||
);
|
||||
console.log(newItems);
|
||||
console.log("[reuseData] 추가될 newItems:", newItems);
|
||||
return [...prev, ...newItems];
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -75,11 +94,45 @@ const Map = () => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// 부모창에 초기 선택값 요청 (팝업 열릴 때 기존 선택 목록 받아오기)
|
||||
useEffect(() => {
|
||||
const origin = window.location.origin;
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: "REQUEST_INIT_SUBS_INFO" }, origin);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 부모창에서 초기 선택값 수신
|
||||
useEffect(() => {
|
||||
const origin = window.location.origin;
|
||||
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (event.origin !== origin) return;
|
||||
|
||||
const data = event.data;
|
||||
if (!data || typeof data !== "object") return;
|
||||
|
||||
if (data.type === "INIT_SUBS_INFO") {
|
||||
const list = Array.isArray(data.payload) ? data.payload : [];
|
||||
setSelectedFclts(
|
||||
list.map((x) => ({
|
||||
ruseUtztnFcltNm: x.ruseUtztnFcltNm ?? "",
|
||||
pbnstRuseFcltId: x.ruseUtztnFcltCd ?? "",
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handler);
|
||||
return () => window.removeEventListener("message", handler);
|
||||
}, []);
|
||||
|
||||
// 레이어 생성 후 초기화 처리
|
||||
const onCheckedLayersChange = () => {
|
||||
//수요처 ID
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dmdInfoId = urlParams.get("dmdInfoId");
|
||||
console.log("[하이라이트] dmdInfoId:", dmdInfoId);
|
||||
|
||||
//수요처 위치로 화면 이동
|
||||
const maps = gisMaps.current;
|
||||
@@ -88,22 +141,42 @@ const Map = () => {
|
||||
const featureId = `${layerNm}.${dmdInfoId}`;
|
||||
let foundFeature: any = null;
|
||||
|
||||
// 현재 로드된 레이어 목록 출력
|
||||
const allLayerNames = maps
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.map((l: any) => l.get("lyrTyNm") || l.get("name") || "(unnamed)");
|
||||
console.log("[하이라이트] 전체 레이어 목록:", allLayerNames);
|
||||
console.log("[하이라이트] 탐색 대상 레이어:", layerNm);
|
||||
console.log("[하이라이트] 탐색 대상 featureId:", featureId);
|
||||
|
||||
maps
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.forEach((layer: any) => {
|
||||
if (layer.get("lyrTyNm") == layerNm) {
|
||||
console.log("[하이라이트] 레이어 발견:", layerNm);
|
||||
const source = layer.getSource?.();
|
||||
if (source?.getFeatureById) {
|
||||
const feature = source.getFeatureById(String(featureId).trim());
|
||||
console.log("[하이라이트] feature 탐색결과:", feature);
|
||||
if (feature) {
|
||||
foundFeature = feature;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// feature 못 찾을 때 소스 내 전체 feature ID 출력
|
||||
if (source?.getFeatures) {
|
||||
const allIds = source.getFeatures().map((f: any) => f.getId());
|
||||
console.log("[하이라이트] 소스 내 전체 feature ID:", allIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!foundFeature) {
|
||||
console.warn("[하이라이트] feature를 찾지 못했습니다. 위 레이어 목록/ID를 확인하세요.");
|
||||
}
|
||||
|
||||
if (foundFeature) {
|
||||
const geometry = foundFeature.getGeometry();
|
||||
|
||||
@@ -133,7 +206,7 @@ const Map = () => {
|
||||
{ width: 30 }
|
||||
);
|
||||
} else {
|
||||
alert("수요처의 좌표정보를 찾을수 없으므로, 상단 조회조건의 시설명을 입력하여 위치를 조회하시기 바랍니다.");
|
||||
console.warn("수요처의 좌표정보를 찾을 수 없습니다. dmdInfoId:", dmdInfoId);
|
||||
}
|
||||
|
||||
const formatNumber = (val: any) => {
|
||||
@@ -185,6 +258,7 @@ const Map = () => {
|
||||
|
||||
// 검색해서 재이용시설 선택하면 해당 시설로 화면 이동 처리
|
||||
useEffect(() => {
|
||||
console.warn(selectDataPk);
|
||||
if (!selectDataPk || Object.keys(selectDataPk).length === 0) return;
|
||||
const maps = gisMaps.current;
|
||||
let foundFeature: any = null;
|
||||
@@ -226,8 +300,12 @@ const Map = () => {
|
||||
|
||||
// 재이용시설 클릭 시 선택 시설목록에 추가
|
||||
const onFeatureSelect = (data: any) => {
|
||||
// feature의 모든 속성 출력 (어떤 키가 있는지 확인)
|
||||
const properties = data.getProperties?.() ?? {};
|
||||
console.log("[onFeatureSelect] feature 속성 전체:", properties);
|
||||
// 처리시설코드
|
||||
const swtrmtFcltCd = data.get("swtrmt_fclt_cd");
|
||||
console.log("[onFeatureSelect] swtrmt_fclt_cd:", swtrmtFcltCd);
|
||||
reuseData(swtrmtFcltCd);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,36 +22,32 @@ export default function SelectedFcltList({
|
||||
|
||||
const TableContent = (
|
||||
<>
|
||||
<thead className="bg-gray-100 text-gray-700 text-[13px]">
|
||||
<thead className="bg-gray-100 text-gray-600 text-[11px] font-medium border-b border-cyan-800">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left border-b">시설명</th>
|
||||
<th className="px-3 py-2 text-right border-b">
|
||||
하수처리량
|
||||
<br />
|
||||
(㎥/일)
|
||||
<th className="px-2 py-1.5 text-left w-1/3">시설명</th>
|
||||
<th className="px-2 py-1.5 text-right whitespace-nowrap">
|
||||
처리량(㎥/일)
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right border-b">
|
||||
재이용량
|
||||
<br />
|
||||
(㎥/일)
|
||||
<th className="px-2 py-1.5 text-right whitespace-nowrap">
|
||||
재이용량(㎥/일)
|
||||
</th>
|
||||
<th className="px-3 py-2 border-b">삭제</th>
|
||||
<th className="px-2 py-1.5 text-center w-10">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item) => (
|
||||
<tr key={item.pbnstRuseFcltId} className="border-b last:border-none">
|
||||
<td className="px-3 py-2 text-[12px]">{item.ruseUtztnFcltNm}</td>
|
||||
<td className="px-3 py-2 text-right text-[12px]">
|
||||
<tr key={item.pbnstRuseFcltId} className="border-b border-gray-100 last:border-none hover:bg-gray-50/50">
|
||||
<td className="px-2 py-1.5 text-[11px] font-medium text-gray-800 break-keep">{item.ruseUtztnFcltNm}</td>
|
||||
<td className="px-2 py-1.5 text-right text-[11px] text-gray-600">
|
||||
{Number(item.splyExtraCpct || 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-[12px]">
|
||||
<td className="px-2 py-1.5 text-right text-[11px] text-gray-600">
|
||||
{Number(item.ruseCpct || 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
<td className="px-2 py-1.5 text-center">
|
||||
<button
|
||||
onClick={() => handleDelete(item.pbnstRuseFcltId)}
|
||||
className="text-red-500 hover:text-red-700 text-sm font-bold"
|
||||
className="text-gray-400 hover:text-red-500 text-[11px] font-bold transition-colors"
|
||||
aria-label="삭제"
|
||||
title="삭제"
|
||||
>
|
||||
@@ -64,15 +60,13 @@ export default function SelectedFcltList({
|
||||
</>
|
||||
);
|
||||
|
||||
if (data.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed right-40 top-[8rem] w-[40rem] max-h-[130rem] overflow-auto bg-white shadow-lg rounded p-4 border border-gray-300 z-50">
|
||||
<h3 className="bg-cyan-700 text-white text-xl font-bold mb-2 p-2">
|
||||
<div className="fixed right-20 top-[3.5rem] w-[22rem] max-h-[80vh] overflow-hidden bg-white/95 backdrop-blur-sm shadow-[0_4px_12px_rgba(0,0,0,0.1)] rounded-lg border border-gray-200 z-50 flex flex-col">
|
||||
<h3 className="bg-cyan-700 text-white text-[13px] font-semibold p-2">
|
||||
선택된 시설
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-gray-800 border border-gray-200">
|
||||
<div className="overflow-y-auto w-full">
|
||||
<table className="w-full text-sm text-gray-800 border-t border-gray-200 table-fixed">
|
||||
{TableContent}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DEV_URL_WR, DEV_URL_SN } from "@/components/ui/config";
|
||||
export type LyrStngProps = {
|
||||
lyr_sn: string
|
||||
lyr_nm: string
|
||||
upper_lyr_sn: string
|
||||
upper_lyr_sn: string
|
||||
drawOrdr: string
|
||||
flterSql: string
|
||||
gobjTy: string
|
||||
@@ -29,7 +29,7 @@ export type LyrStngProps = {
|
||||
export const mapService = {
|
||||
//레이어 설정 조회
|
||||
getLyrStngs: async () => {
|
||||
const response = await fetch(`${DEV_URL_SN}/api/v1/map/lyrStngs/DM_NETWORK`, {
|
||||
const response = await fetch(`${DEV_URL_SN}/map/lyrStngs/DM_NETWORK`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
@@ -42,7 +42,7 @@ export const mapService = {
|
||||
|
||||
return response.json();
|
||||
},
|
||||
getPbnstRuseFclts: async (ruseFcltPk:any) => {
|
||||
getPbnstRuseFclts: async (ruseFcltPk: any) => {
|
||||
const response = await fetch(`${DEV_URL_WR}/reuseFc/pubFc/${ruseFcltPk}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
||||
@@ -0,0 +1,895 @@
|
||||
// app/(with-layout)/apply/register/page.tsx
|
||||
/**
|
||||
* 수요정보등록(수정) 화면
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 수요정보등록: POST /reuseFc/request/demandInfo
|
||||
* - 수요정보조회: GET /reuseFc/request/' + dmdInfoId - getSdmInfoStatusList
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useRef, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import WaterQualityRefModal from "../modal/WaterQualityRefModal";
|
||||
import CandidateSiteModal, { type CandidateSite } from "@/components/CandidateSiteModal";
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { SdmInfoFormData } from "@/app/store/sdmMng";
|
||||
import SimilarCaseModal, { type SimilarCaseDetail, type SimilarCaseRow } from "@/app/(landing)/reuse/apply/modal/SimilarCaseModal";
|
||||
//import type { SubsInfo } from "@/app/store/map/type";
|
||||
|
||||
type PHRange = { min: string; max: string };
|
||||
|
||||
export default function DemandRegisterPage() {
|
||||
const router = useRouter();
|
||||
const today = useMemo(() => {
|
||||
const d = new Date();
|
||||
const yyyy = d.getFullYear();
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}, []);
|
||||
|
||||
const [openRef, setOpenRef] = useState(false);
|
||||
|
||||
const [openCand, setOpenCand] = useState(false);
|
||||
const [selectedCands, setSelectedCands] = useState<CandidateSite[]>([]);
|
||||
|
||||
// 기본정보
|
||||
const [reqDate, setReqDate] = useState<string>(today);
|
||||
const [reqQty, setReqQty] = useState<string>("");
|
||||
const [usePurpose, setUsePurpose] = useState<string>("");
|
||||
const [hopePrice, setHopePrice] = useState<string>("");
|
||||
|
||||
// 요구수질
|
||||
const [coliform, setColiform] = useState<string>(""); // 총대장균군수
|
||||
const [noDetect, setNoDetect] = useState<boolean>(false);
|
||||
|
||||
const [tn, setTn] = useState<string>("");
|
||||
const [residualCl, setResidualCl] = useState<string>("");
|
||||
const [tp, setTp] = useState<string>("");
|
||||
const [turbidity, setTurbidity] = useState<string>("");
|
||||
const [ph, setPh] = useState<PHRange>({ min: "", max: "" });
|
||||
const [bod, setBod] = useState<string>("");
|
||||
const [chloride, setChloride] = useState<string>("");
|
||||
const [odor, setOdor] = useState<string>("불쾌하지 않을 것");
|
||||
const [ec, setEc] = useState<string>("");
|
||||
const [color, setColor] = useState<string>("");
|
||||
const [remark, setRemark] = useState<string>("");
|
||||
|
||||
// 공급처 후보지 선택(필수아님)
|
||||
const [cand1, setCand1] = useState("");
|
||||
const [cand2, setCand2] = useState("");
|
||||
const [cand3, setCand3] = useState("");
|
||||
//const [candidateList, setCandidateList] = useState<SubsInfo[]>([]);
|
||||
|
||||
const useTypesData = useDmStore((state) => state.Sdm.useTypesData);
|
||||
const getUseTypesData = useDmStore((state) => state.getUseTypesData);
|
||||
const [openSimilar, setOpenSimilar] = useState(false);
|
||||
|
||||
// 후보지 팝업 메시지 핸들러
|
||||
const candsRef = useRef<{ name: string; code: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const origin = window.location.origin;
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const data = event.data;
|
||||
if (!data || typeof data !== "object") return;
|
||||
|
||||
if (data.type === "SET_SUBS_INFO") {
|
||||
// 팝업에서 배열로 전송 (최대 3개)
|
||||
const list = Array.isArray(data.payload) ? data.payload : [];
|
||||
setCand1(list[0]?.ruseUtztnFcltNm ?? "");
|
||||
setCand2(list[1]?.ruseUtztnFcltNm ?? "");
|
||||
setCand3(list[2]?.ruseUtztnFcltNm ?? "");
|
||||
// 추후 REQUEST_INIT_SUBS_INFO 응답용 ref 갱신
|
||||
candsRef.current = list.map((x: any) => ({
|
||||
name: x.ruseUtztnFcltNm ?? "",
|
||||
code: x.ruseUtztnFcltCd ?? "",
|
||||
}));
|
||||
}
|
||||
|
||||
if (data.type === "REQUEST_INIT_SUBS_INFO") {
|
||||
// 팝업이 현재 선택된 후보지 초기값을 요청하면 응답
|
||||
const initList = candsRef.current
|
||||
.filter((x) => x.name)
|
||||
.map((x) => ({
|
||||
ruseUtztnFcltNm: x.name,
|
||||
ruseUtztnFcltCd: x.code,
|
||||
}));
|
||||
const target = event.source as Window | null;
|
||||
target?.postMessage(
|
||||
{ type: "INIT_SUBS_INFO", payload: initList },
|
||||
{ targetOrigin: origin }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, []);
|
||||
|
||||
// 유사사례
|
||||
const similarCaseList = useDmStore((state) => state.Sdm.similarCaseList);
|
||||
const getSimilarCaseList = useDmStore((state) => state.getSimilarCaseList);
|
||||
const [farmText, setFarmText] = useState("이하");
|
||||
|
||||
const insertSdmInfoData = useDmStore(
|
||||
(state) => state.insertSdmInfoData
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getUseTypesData();
|
||||
}, [getUseTypesData]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (!event.data) return;
|
||||
|
||||
if (event.data.type === "SET_SUBS_INFO") {
|
||||
const list = event.data.payload || [];
|
||||
|
||||
// 최대 3개
|
||||
setCand1(list[0]?.ruseUtztnFcltNm || "");
|
||||
setCand2(list[1]?.ruseUtztnFcltNm || "");
|
||||
setCand3(list[2]?.ruseUtztnFcltNm || "");
|
||||
//setCandidateList(list);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// option
|
||||
// 'req':필수 / 'num' : 숫자 / 'num+[숫자] : 자릿수
|
||||
const validation = (val, text, option: string[] = []) => {
|
||||
if (option.includes('req')) {
|
||||
if (val.trim() === "" || val === undefined) {
|
||||
alert(`${text} 항목은 필수 입력값입니다.`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (val.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const numOption = option.find((opt) => opt.startsWith("num"));
|
||||
if (numOption) {
|
||||
const isDot = numOption.includes("Dot");
|
||||
|
||||
// 3. 숫자 체크
|
||||
const regex = isDot ? /^\d+(\.\d+)?$/ : /^\d+$/;
|
||||
|
||||
if (!regex.test(val)) {
|
||||
alert(
|
||||
`${text}은(는) ${isDot ? "숫자 또는 소수" : "숫자"
|
||||
}만 입력 가능합니다.`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 자리수 체크
|
||||
const lengthStr = numOption.replace("numDot", "").replace("num", "");
|
||||
|
||||
if (lengthStr) {
|
||||
const length = Number(lengthStr);
|
||||
|
||||
if (isDot) {
|
||||
// 🔥 소수 자리수 체크 (소수점 기준)
|
||||
const decimalPart = val.split(".")[1];
|
||||
|
||||
if (decimalPart && decimalPart.length > length) {
|
||||
alert(`${text}은(는) 소수점 ${length}자리까지 가능합니다.`);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 정수 자리수 체크
|
||||
if (val.length !== length) {
|
||||
alert(`${text}은(는) ${length}자리여야 합니다.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const rows: SimilarCaseRow[] = similarCaseList.map((el, idx) => ({
|
||||
caseId: String(idx),
|
||||
projectName: el.ruseBizNm,
|
||||
period: `${el.bizBgngDate} ~ ${el.bizEndDate}`,
|
||||
|
||||
supplyFacilityType: el.splyFcltKnd,
|
||||
supplyName: el.splyFcltNm,
|
||||
supplyCapacity: Number(el.splyFcltCpct),
|
||||
|
||||
demandName: el.dmdFcltNm,
|
||||
demandAddress: el.dmdFcltAddr,
|
||||
|
||||
facilityCapacity: Number(el.ruseFcltCpct),
|
||||
pipeKm: Number(el.ruseFcltPplLen),
|
||||
purpose: el.ruseFcltUsg,
|
||||
|
||||
supplyAmt: Number(el.ruseFcltSplyCpct),
|
||||
treatment: el.ruseFcltPrcsMsrc,
|
||||
usePurpose: el.ruseFcltUsg,
|
||||
}));
|
||||
|
||||
const detailsByCaseId: Record<string, SimilarCaseDetail> = {};
|
||||
similarCaseList.forEach((el, idx) => {
|
||||
detailsByCaseId[String(idx)] = {
|
||||
coliform: el.grnteWqGrssClfg,
|
||||
residualCl: el.grnteWqCombResdch,
|
||||
turbidity: el.grnteWqTrbt,
|
||||
bod: el.grnteWqBod,
|
||||
odor: el.grnteWqSmll,
|
||||
color: el.grnteWqCrmty,
|
||||
tn: el.grnteWqTn,
|
||||
tp: el.grnteWqTp,
|
||||
ph: el.grnteWqPh,
|
||||
chloride: el.grnteWqClIonQnt,
|
||||
ec: el.grnteWqCndctStat,
|
||||
note: el.grnteWqRmrk,
|
||||
|
||||
costTotal: Number(el.reqBizGrssCst),
|
||||
costFacility: Number(el.reqBizReuseFcltCst),
|
||||
costPipe: Number(el.reqBizPplCst),
|
||||
costDesign: Number(el.reqBizDesignCst),
|
||||
costSupervision: Number(el.reqBizSpvsCst),
|
||||
costOther: Number(el.reqBizEtc),
|
||||
|
||||
fundingTotal: Number(el.prcmPlanGrssCst),
|
||||
fundingNational: Number(el.prcmPlanNlep),
|
||||
fundingLocal: Number(el.prcmPlanLcep),
|
||||
fundingSelf: Number(el.prcmPlanBrdnCst),
|
||||
fundingRate: Number(el.prcmPlanEcqrRt),
|
||||
};
|
||||
});
|
||||
|
||||
// 데이터 저장
|
||||
const saveInfo = async () => {
|
||||
if (!validation(reqQty, '요구수량', ['req', 'numDot'])) return false;
|
||||
if (!validation(usePurpose, '사용용도', ['req'])) return false;
|
||||
if (!validation(hopePrice, '희망단가', ['req', 'numDot'])) return false;
|
||||
/* if(!noDetect && !validation(coliform, '총대장균군수', ['req','numDot'])) return false;
|
||||
if(!validation(residualCl, ['req', '결합잔류염소', 'numDot'])) return false;
|
||||
if(!validation(turbidity, '탁도', ['req', 'numDot'])) return false;
|
||||
if(!validation(bod, '생물화학적산소요구량', ['req', 'numDot'])) return false;
|
||||
if(!validation(odor, '냄새', ['req'])) return false; */
|
||||
|
||||
// 필수는 아니지만 숫자로는 들어와야함.
|
||||
/* if(!validation(tn, '총질소', ['numDot'])) return false;
|
||||
if(!validation(residualCl, '결합잔류염소', ['numDot'])) return false;
|
||||
if(!validation(tp, '총인', ['numDot'])) return false;
|
||||
if(!validation(ph.min, 'ph', ['numDot'])) return false;
|
||||
if(!validation(ph.max, 'ph', ['numDot'])) return false;
|
||||
if(!validation(chloride, '염화물', ['numDot'])) return false;
|
||||
if(!validation(ec, '전기전도도', ['numDot'])) return false; */
|
||||
|
||||
const userStr = sessionStorage.getItem("user");
|
||||
const user = userStr ? JSON.parse(userStr) : null;
|
||||
|
||||
if (user) {
|
||||
// 후보지 목록 (candsRef에 코드까지 보관됨)
|
||||
const splyCndtList = candsRef.current
|
||||
.filter((x) => x.name)
|
||||
.map((x) => ({
|
||||
ruseUtztnFcltNm: x.name,
|
||||
ruseUtztnFcltCd: x.code,
|
||||
}));
|
||||
|
||||
const payload: SdmInfoFormData = {
|
||||
dmndTmng: new Date(reqDate),
|
||||
dmdInfoId: user.dmdInfoId,
|
||||
dmndQty: Number(reqQty),
|
||||
wntUntprc: Number(hopePrice),
|
||||
useUsgCd: usePurpose,
|
||||
// 수질
|
||||
grssClfg: Number(coliform),
|
||||
tn: Number(tn) || null,
|
||||
combResdch: Number(residualCl) || null,
|
||||
tph: Number(tp) || null,
|
||||
trbt: Number(turbidity),
|
||||
ph: ph.min || '' + ((ph.min || ph.max) ? '~' : '') + ph.max || '',
|
||||
blgcOxygDmndQnt: Number(bod),
|
||||
clIonQnt: Number(chloride) || null,
|
||||
smll: odor,
|
||||
cdtnv: Number(ec) || null,
|
||||
crmty: Number(color) || null,
|
||||
rmrk: remark,
|
||||
splyCndtList, // 후보지 목록 추가
|
||||
}
|
||||
const success = await insertSdmInfoData(payload);
|
||||
if (success) {
|
||||
router.push("/apply/status");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const searchCandidate = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const dmdInfoId = JSON.parse(
|
||||
sessionStorage.getItem("user") || "{}"
|
||||
)?.dmdInfoId?.toString();
|
||||
|
||||
const url = `/map/popup?dmdInfoId=${encodeURIComponent(dmdInfoId)}`;
|
||||
const width = window.screen.availWidth;
|
||||
const height = window.screen.availHeight;
|
||||
const left = 0;
|
||||
const top = 0;
|
||||
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
||||
window.open(url, "candidatePopup", popupOptions);
|
||||
};
|
||||
|
||||
const getCriterionCode = (criterion: string) => {
|
||||
switch (criterion) {
|
||||
case "supplyAmt":
|
||||
return "S";
|
||||
case "money":
|
||||
return "M";
|
||||
case "usePurpose":
|
||||
return "U";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const searchSimilarCase = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpenSimilar(true);
|
||||
};
|
||||
|
||||
const initQualityForm = () => {
|
||||
setColiform("");
|
||||
setNoDetect(false);
|
||||
setTn("");
|
||||
setResidualCl("");
|
||||
setTp("");
|
||||
setTurbidity("");
|
||||
setPh({ min: "", max: "" });
|
||||
setBod("");
|
||||
setChloride("");
|
||||
setOdor("불쾌하지 않을 것");
|
||||
setEc("");
|
||||
setColor("");
|
||||
setRemark("");
|
||||
setFarmText("이하");
|
||||
}
|
||||
|
||||
const fncSetOptionValue = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
initQualityForm();
|
||||
|
||||
switch (value) {
|
||||
// 청소화장실용도
|
||||
case '01':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setColor('20');
|
||||
break;
|
||||
// 세척살수용도
|
||||
case '02':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
break;
|
||||
// 조경용도
|
||||
case '03':
|
||||
setTp('0.2');
|
||||
setTurbidity('2');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
setChloride('250');
|
||||
break;
|
||||
// 지하수
|
||||
case '05':
|
||||
break;
|
||||
// 농업용도
|
||||
case '06':
|
||||
setTurbidity('5');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('8');
|
||||
setEc('2000');
|
||||
setFarmText("이하 (** 간접:2000,직접:700 )");
|
||||
break;
|
||||
// 하천 등 유지보수
|
||||
case '07':
|
||||
setColiform('1000');
|
||||
setTn('10');
|
||||
setTp('0.5');
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
setBod('5');
|
||||
break;
|
||||
// 공업
|
||||
case '08':
|
||||
setPh({ min: '5.8', max: '8.5' });
|
||||
break;
|
||||
default:
|
||||
console.log('잘못된 선택입니다.');
|
||||
}
|
||||
|
||||
setUsePurpose(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<main className="bg-white">
|
||||
|
||||
{/* 서브 타이틀 + 브레드크럼 */}
|
||||
<SubPageHeader
|
||||
title="수요정보등록"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/reuse" },
|
||||
{ label: "수요신청", href: "/reuse/apply" },
|
||||
{ label: "수요정보등록" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 컨텐츠 영역 */}
|
||||
<div>
|
||||
|
||||
{/* 기본정보 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
<FormRow label="요구시기" required>
|
||||
<DateInput value={reqDate} onChange={setReqDate} />
|
||||
</FormRow>
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="요구수량(m²/d)" required>
|
||||
<TextInput value={reqQty} onChange={setReqQty} placeholder="" />
|
||||
</FormRow>
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="사용용도" required>
|
||||
<select
|
||||
value={usePurpose}
|
||||
onChange={(e) => fncSetOptionValue(e)}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{useTypesData.map((item) => (
|
||||
<option key={item.val} value={item.val}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormRow>
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormRow label="희망단가(원)" required>
|
||||
<TextInput value={hopePrice} onChange={setHopePrice} />
|
||||
</FormRow>
|
||||
</div>
|
||||
|
||||
{/* 요구수질 섹션 타이틀 바 */}
|
||||
<div className="grid grid-cols-12">
|
||||
<div className="col-span-12 lg:col-span-2 inline-flex justify-center items-center gap-3 px-6 py-6 text-center">
|
||||
<span className="text-lg font-semibold text-neutral-800">요구수질</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpenRef(true)}
|
||||
className="rounded border-[2px] border-[#DE3412] px-2 py-3 text-[15px] font-medium text-[#4F4F4F] hover:bg-orange-50"
|
||||
>
|
||||
수질기준 참고
|
||||
</button>
|
||||
{/* 수질기준참고 모달 */}
|
||||
<WaterQualityRefModal
|
||||
open={openRef}
|
||||
onClose={() => setOpenRef(false)}
|
||||
imageSrc="/images/landing/water_quality_ref_1.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 요구수질 테이블 */}
|
||||
<div className="border border-neutral-200 bg-white">
|
||||
{/* 1행 */}
|
||||
<QualityRow
|
||||
leftLabel="총대장균군수(개/mg)"
|
||||
|
||||
leftControl={
|
||||
<div className="flex items-center gap-4">
|
||||
<TextInput
|
||||
value={coliform}
|
||||
onChange={setColiform}
|
||||
disabled={noDetect}
|
||||
className={noDetect ? "bg-neutral-100 text-neutral-400" : ""}
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-neutral-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={noDetect}
|
||||
onChange={(e) => {
|
||||
setNoDetect(e.target.checked);
|
||||
if (e.target.checked) setColiform("");
|
||||
}}
|
||||
className="h-4 w-4 rounded border-neutral-300"
|
||||
/>
|
||||
불검출
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
rightLabel="총질소(T-N)(mg/L)"
|
||||
rightControl={
|
||||
<InlineSuffixInput value={tn} onChange={setTn} suffix="이하" />
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 2행 */}
|
||||
<QualityRow
|
||||
leftLabel="결합잔류염소(mg/L)"
|
||||
/* leftRequired */
|
||||
leftControl={<TextInput value={residualCl} onChange={setResidualCl} />}
|
||||
rightLabel="총인(T-P)(mg/L)"
|
||||
rightControl={
|
||||
<InlineSuffixInput value={tp} onChange={setTp} suffix="이하" />
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 3행 */}
|
||||
<QualityRow
|
||||
leftLabel="탁도(NTU)"
|
||||
/* leftRequired */
|
||||
leftControl={
|
||||
<InlineSuffixInput value={turbidity} onChange={setTurbidity} suffix="이하" />
|
||||
}
|
||||
rightLabel="수소이온농도(pH)"
|
||||
rightControl={
|
||||
<div className="flex items-center gap-2">
|
||||
<TextInput
|
||||
value={ph.min}
|
||||
onChange={(v) => setPh((p) => ({ ...p, min: v }))}
|
||||
className="w-32"
|
||||
/>
|
||||
<span className="text-neutral-500">~</span>
|
||||
<TextInput
|
||||
value={ph.max}
|
||||
onChange={(v) => setPh((p) => ({ ...p, max: v }))}
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 4행 */}
|
||||
<QualityRow
|
||||
leftLabel={
|
||||
<>
|
||||
생물화학적산소요구량
|
||||
<br />
|
||||
(BOD)(mg/L)
|
||||
</>
|
||||
}
|
||||
/* leftRequired */
|
||||
leftControl={<InlineSuffixInput value={bod} onChange={setBod} suffix="이하" />}
|
||||
rightLabel="염화물(mgCL/L)"
|
||||
rightControl={<InlineSuffixInput value={chloride} onChange={setChloride} suffix="이하" />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 5행 */}
|
||||
<QualityRow
|
||||
leftLabel="냄새"
|
||||
/* leftRequired */
|
||||
leftControl={
|
||||
<select
|
||||
value={odor}
|
||||
onChange={(e) => setOdor(e.target.value)}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option>불쾌하지 않을 것</option>
|
||||
<option>약간 있음</option>
|
||||
<option>있음</option>
|
||||
</select>
|
||||
}
|
||||
rightLabel="전기전도도(µs/cm)"
|
||||
rightControl={<InlineSuffixInput value={ec} onChange={setEc} suffix={farmText} />}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 6행 */}
|
||||
<QualityRow
|
||||
leftLabel="색도(도)"
|
||||
/* leftRequired */
|
||||
leftControl={<InlineSuffixInput value={color} onChange={setColor} suffix="이하" />}
|
||||
rightLabel="비고"
|
||||
rightControl={
|
||||
<textarea
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
className="h-11 w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm outline-none focus:border-neutral-500"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{/* 후보지 선택 */}
|
||||
<div className="grid grid-cols-12">
|
||||
<LabelCell>
|
||||
공급처 후보지 선택
|
||||
<br />
|
||||
(필수아님)
|
||||
</LabelCell>
|
||||
|
||||
<div className="col-span-12 md:col-span-10 p-6">
|
||||
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:gap-4">
|
||||
<TextInput value={cand1} onChange={setCand1} className="max-w-[220px]" />
|
||||
<TextInput value={cand2} onChange={setCand2} className="max-w-[220px]" />
|
||||
<TextInput value={cand3} onChange={setCand3} className="max-w-[220px]" />
|
||||
|
||||
<div className="flex items-center gap-3 xl:ml-2">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-700"
|
||||
onClick={(e) => {
|
||||
searchCandidate(e);
|
||||
|
||||
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
|
||||
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
|
||||
}}>
|
||||
후보지 조회
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#888] px-5 text-base text-white hover:bg-neutral-600"
|
||||
onClick={(e) => {
|
||||
// TODO: 유사사례 연결
|
||||
searchSimilarCase(e);
|
||||
}}
|
||||
>
|
||||
유사사례
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex justify-center py-12">
|
||||
<button
|
||||
type="button"
|
||||
className="h-11 rounded-md bg-[#0B4EA2] px-10 text-lg text-white hover:opacity-90"
|
||||
onClick={() => {
|
||||
// TODO: 저장 처리
|
||||
saveInfo();
|
||||
}}
|
||||
>
|
||||
수요정보 등록
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<SimilarCaseModal
|
||||
open={openSimilar}
|
||||
onClose={() => setOpenSimilar(false)}
|
||||
rows={rows} // 실제 데이터
|
||||
detailsByCaseId={detailsByCaseId}
|
||||
onSearch={async (criterion) => {
|
||||
const code = getCriterionCode(criterion);
|
||||
if (!code) return;
|
||||
// 값은 상황에 맞게 (예시: usePurpose)
|
||||
console.log(code+' req: '+reqQty+' purpose :'+usePurpose);
|
||||
await getSimilarCaseList(code, code == 'S' ? reqQty : code == 'U' ? usePurpose : '');
|
||||
}}
|
||||
/>
|
||||
|
||||
<CandidateSiteModal
|
||||
open={openCand}
|
||||
onClose={() => setOpenCand(false)}
|
||||
initialSelected={selectedCands}
|
||||
onApply={(list) => {
|
||||
setSelectedCands(list);
|
||||
|
||||
// ✅ cand1~3 입력칸에 반영(시설명만 넣는 버전)
|
||||
setCand1(list[0]?.name ?? "");
|
||||
setCand2(list[1]?.name ?? "");
|
||||
setCand3(list[2]?.name ?? "");
|
||||
|
||||
setOpenCand(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ----------------------------- UI helpers ----------------------------- */
|
||||
|
||||
function Divider() {
|
||||
return <div className="h-px w-full bg-neutral-200" />;
|
||||
}
|
||||
|
||||
function LabelCell({
|
||||
children,
|
||||
required,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
required?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="col-span-12 md:col-span-2 flex items-center justify-center text-center bg-emerald-50/70 px-6 py-6 text-lg font-semibold text-neutral-800">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{children}
|
||||
{required ? <span className="text-red-500">*</span> : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormRow({
|
||||
label,
|
||||
required,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
required?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
<LabelCell required={required}>{label}</LabelCell>
|
||||
<div className="col-span-12 md:col-span-10 p-6">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QualityRow({
|
||||
leftLabel,
|
||||
leftRequired,
|
||||
leftControl,
|
||||
rightLabel,
|
||||
rightControl,
|
||||
}: {
|
||||
leftLabel: React.ReactNode;
|
||||
leftRequired?: boolean;
|
||||
leftControl: React.ReactNode;
|
||||
rightLabel: React.ReactNode;
|
||||
rightControl: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
{/* 좌측 라벨 */}
|
||||
<div className="col-span-12 lg:col-span-2 flex items-center justify-center text-center bg-emerald-50/70 px-6 py-6 text-lg font-semibold text-neutral-800">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="leading-6">{leftLabel}</span>
|
||||
{leftRequired ? <span className="text-red-500">*</span> : null}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 좌측 입력 */}
|
||||
<div className="col-span-12 lg:col-span-4 p-6">{leftControl}</div>
|
||||
|
||||
{/* 우측 라벨 */}
|
||||
<div className="col-span-12 lg:col-span-2 flex items-center justify-center text-center bg-emerald-50/70 px-6 py-6 text-lg font-semibold text-neutral-800">
|
||||
<span className="leading-6">{rightLabel}</span>
|
||||
</div>
|
||||
|
||||
{/* 우측 입력 */}
|
||||
<div className="col-span-12 lg:col-span-4 p-6">{rightControl}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
className,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={[
|
||||
"h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none",
|
||||
"focus:border-neutral-500",
|
||||
disabled ? "cursor-not-allowed" : "",
|
||||
className ?? "",
|
||||
].join(" ")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineSuffixInput({
|
||||
value,
|
||||
onChange,
|
||||
suffix,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
suffix: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<TextInput value={value} onChange={onChange} className="max-w-[200px]" />
|
||||
<span className="text-sm text-neutral-600">{suffix}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DateInput({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const openPicker = () => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
// Chrome/Edge 등 지원
|
||||
if (typeof (el as any).showPicker === "function") {
|
||||
(el as any).showPicker();
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback (일부 브라우저)
|
||||
el.focus();
|
||||
el.click();
|
||||
};
|
||||
return (
|
||||
<div className="relative w-full max-w-[200px]">
|
||||
<input
|
||||
ref={ref}
|
||||
type="date"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={[
|
||||
"h-11 w-full rounded-md border border-neutral-300 bg-white px-3 pr-10 text-sm outline-none focus:border-neutral-500",
|
||||
// ✅ 기본 달력 아이콘 숨김 (Tailwind arbitrary selector)
|
||||
"[&::-webkit-calendar-picker-indicator]:opacity-0",
|
||||
"[&::-webkit-calendar-picker-indicator]:absolute",
|
||||
"[&::-webkit-calendar-picker-indicator]:right-0",
|
||||
"[&::-webkit-calendar-picker-indicator]:w-10",
|
||||
"[&::-webkit-calendar-picker-indicator]:h-full",
|
||||
"[&::-webkit-calendar-picker-indicator]:cursor-pointer",
|
||||
].join(" ")}
|
||||
/>
|
||||
|
||||
{/* ✅ 클릭 가능한 아이콘 버튼 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openPicker}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-neutral-500 hover:text-neutral-700"
|
||||
aria-label="달력 열기"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M7.14286 7.99104H16.7812M6.48512 2.5625V4.19126M17.3125 2.5625V4.19106M20.5 7.19106L20.5 18.5625C20.5 20.2194 19.1569 21.5625 17.5 21.5625H6.5C4.84315 21.5625 3.5 20.2194 3.5 18.5625V7.19106C3.5 5.53421 4.84315 4.19106 6.5 4.19106H17.5C19.1569 4.19106 20.5 5.53421 20.5 7.19106Z" stroke="#1A1A1A" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
"use client";
|
||||
|
||||
import { useDmStore } from "@/app/store";
|
||||
import type { DemandCompBizType } from "@/app/store/sdmMng";
|
||||
import SubPageHeader from "@/components/SubPageHeader";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
type TopField = { label: string; key: string, value: string; required?: boolean };
|
||||
type PairField = { label: string; key: string, value: string };
|
||||
type SessionUser = { dmdInfoId?: string };
|
||||
|
||||
const formatDate = (value: string | Date | "" | undefined) => {
|
||||
if (!value) return "-";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value);
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const dd = String(date.getDate()).padStart(2, "0");
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
};
|
||||
|
||||
const formatValue = (value: string | number | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const formatBelow = (value: string | number | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
if (Number(value) === -1) return "불검출";
|
||||
return `${value} 이하`;
|
||||
};
|
||||
|
||||
function StatusBadge({ v }: { v: string }) {
|
||||
const cls = v === "신청" ? "text-red-600" : "text-lime-600";
|
||||
return <span className={["font-medium", cls].join(" ")}>{v || "-"}</span>;
|
||||
}
|
||||
|
||||
function TextInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
disabled,
|
||||
className,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
className={[
|
||||
"h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none",
|
||||
"focus:border-neutral-500",
|
||||
disabled ? "cursor-not-allowed" : "",
|
||||
className ?? "",
|
||||
].join(" ")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DateInput({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const openPicker = () => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
// Chrome/Edge 등 지원
|
||||
if (typeof (el as any).showPicker === "function") {
|
||||
(el as any).showPicker();
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback (일부 브라우저)
|
||||
el.focus();
|
||||
el.click();
|
||||
};
|
||||
return (
|
||||
<div className="relative w-full max-w-[200px]">
|
||||
<input
|
||||
ref={ref}
|
||||
type="date"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={[
|
||||
"h-11 w-full rounded-md border border-neutral-300 bg-white px-3 pr-10 text-sm outline-none focus:border-neutral-500",
|
||||
// ✅ 기본 달력 아이콘 숨김 (Tailwind arbitrary selector)
|
||||
"[&::-webkit-calendar-picker-indicator]:opacity-0",
|
||||
"[&::-webkit-calendar-picker-indicator]:absolute",
|
||||
"[&::-webkit-calendar-picker-indicator]:right-0",
|
||||
"[&::-webkit-calendar-picker-indicator]:w-10",
|
||||
"[&::-webkit-calendar-picker-indicator]:h-full",
|
||||
"[&::-webkit-calendar-picker-indicator]:cursor-pointer",
|
||||
].join(" ")}
|
||||
/>
|
||||
|
||||
{/* ✅ 클릭 가능한 아이콘 버튼 */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openPicker}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-neutral-500 hover:text-neutral-700"
|
||||
aria-label="달력 열기"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M7.14286 7.99104H16.7812M6.48512 2.5625V4.19126M17.3125 2.5625V4.19106M20.5 7.19106L20.5 18.5625C20.5 20.2194 19.1569 21.5625 17.5 21.5625H6.5C4.84315 21.5625 3.5 20.2194 3.5 18.5625V7.19106C3.5 5.53421 4.84315 4.19106 6.5 4.19106H17.5C19.1569 4.19106 20.5 5.53421 20.5 7.19106Z" stroke="#1A1A1A" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DemandStatusPage() {
|
||||
const { getSdmInfoStatusList, updateSdmInfoData, Sdm, Common } = useDmStore((state) => state);
|
||||
const { successLogin, sdmInfoStatusList } = Sdm;
|
||||
const { selectSidoData, selectSigunData } = Common;
|
||||
|
||||
const detailRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [isLoginStorageInfo, setIsLoginStorageInfo] = useState(false);
|
||||
const [selectedDmdInfoSn, setSelectedDmdInfoSn] = useState<number | "" | null>(null);
|
||||
const [transformedSdmInfoStatusList, setTransformedSdmInfoStatusList] = useState<DemandCompBizType[]>([]);
|
||||
|
||||
// 수정 유무
|
||||
const [ isEditMode, setIsEditMode ] = useState(false);
|
||||
// 수정용 (UI에서 바뀌는 값)
|
||||
const [editData, setEditData] = useState<any>(null);
|
||||
// 원본 백업 (취소용)
|
||||
const [originData, setOriginData] = useState<any>(null);
|
||||
// 사용용도
|
||||
const useTypesData = useDmStore((state) => state.Sdm.useTypesData);
|
||||
const getUseTypesData = useDmStore((state) => state.getUseTypesData);
|
||||
|
||||
useEffect(() => {
|
||||
const user = sessionStorage.getItem("user");
|
||||
if (user) {
|
||||
setIsLoginStorageInfo(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getUseTypesData();
|
||||
}, [getUseTypesData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!successLogin && !isLoginStorageInfo) return;
|
||||
|
||||
const user = sessionStorage.getItem("user");
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const userData = JSON.parse(user) as SessionUser;
|
||||
const dmdInfoId = userData.dmdInfoId?.trim();
|
||||
if (dmdInfoId) {
|
||||
getSdmInfoStatusList(dmdInfoId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing user data:", error);
|
||||
}
|
||||
}, [successLogin, isLoginStorageInfo, getSdmInfoStatusList]);
|
||||
|
||||
useEffect(() => {
|
||||
const mappedList = sdmInfoStatusList.map((item) => ({
|
||||
...item,
|
||||
ctpvCd: selectSidoData.find((sido) => sido.val === item.ctpvCd?.trim())?.text || item.ctpvCd,
|
||||
sggCd: selectSigunData.find((sigun) => sigun.val === item.sggCd?.trim())?.text || item.sggCd,
|
||||
}));
|
||||
|
||||
setTransformedSdmInfoStatusList(mappedList);
|
||||
}, [sdmInfoStatusList, selectSidoData, selectSigunData]);
|
||||
|
||||
useEffect(() => {
|
||||
const first = transformedSdmInfoStatusList[0];
|
||||
if (!first) {
|
||||
setSelectedDmdInfoSn(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDmdInfoSn == null) {
|
||||
setSelectedDmdInfoSn(first.dmdInfoSn);
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = transformedSdmInfoStatusList.some(
|
||||
(item) => item.dmdInfoSn === selectedDmdInfoSn,
|
||||
);
|
||||
if (!exists) {
|
||||
setSelectedDmdInfoSn(first.dmdInfoSn);
|
||||
}
|
||||
}, [transformedSdmInfoStatusList, selectedDmdInfoSn]);
|
||||
|
||||
const selectedSdmInfoStatus = useMemo(() => {
|
||||
if (!transformedSdmInfoStatusList.length) return null;
|
||||
return (
|
||||
transformedSdmInfoStatusList.find((item) => item.dmdInfoSn === selectedDmdInfoSn) ??
|
||||
transformedSdmInfoStatusList[0]
|
||||
);
|
||||
}, [transformedSdmInfoStatusList, selectedDmdInfoSn]);
|
||||
|
||||
const topFields: TopField[] = useMemo(() => {
|
||||
if (!selectedSdmInfoStatus) return [];
|
||||
|
||||
return [
|
||||
{ label: "요구시기", key: "dmndTmng", value: formatDate(selectedSdmInfoStatus.dmndTmng), required: true },
|
||||
{ label: "요구수량(㎥/d)", key: "dmndQty", value: formatValue(selectedSdmInfoStatus.dmndQty) },
|
||||
{ label: "사용용도", key: "useUsgCd", value: formatValue(selectedSdmInfoStatus.useUsgCd) },
|
||||
{ label: "희망단가(원/㎥)", key: "wntUntprc", value: formatValue(selectedSdmInfoStatus.wntUntprc) },
|
||||
];
|
||||
}, [selectedSdmInfoStatus]);
|
||||
|
||||
const waterFields: PairField[] = useMemo(() => {
|
||||
if (!selectedSdmInfoStatus) return [];
|
||||
|
||||
return [
|
||||
{ label: "총대장균군수(개/100mL)", key: "grssClfg", value: formatBelow(selectedSdmInfoStatus.grssClfg) },
|
||||
{ label: "결합잔류염소(mg/L)", key:"combResdch", value: formatBelow(selectedSdmInfoStatus.combResdch) },
|
||||
{ label: "탁도(NTU)", key:"trbt", value: formatBelow(selectedSdmInfoStatus.trbt) },
|
||||
|
||||
{ label: "생물학적산소요구량(BOD)(mg/L)", key:"blgcOxygDmndQnt", value: formatBelow(selectedSdmInfoStatus.blgcOxygDmndQnt) },
|
||||
{ label: "냄새", key:"smll", value: formatValue(selectedSdmInfoStatus.smll) },
|
||||
{ label: "색도(도)", key:"crmty", value: formatBelow(selectedSdmInfoStatus.crmty) },
|
||||
|
||||
{ label: "총질소(T-N)(mg/L)", key:"tn", value: formatBelow(selectedSdmInfoStatus.tn) },
|
||||
{ label: "총인(T-P)(mg/L)", key:"tph", value: formatBelow(selectedSdmInfoStatus.tph) },
|
||||
{ label: "수소이온농도(pH)", key:"ph", value: formatValue(selectedSdmInfoStatus.ph) },
|
||||
|
||||
{ label: "염화물(mgCL/L)", key:"clIonQnt", value: formatBelow(selectedSdmInfoStatus.clIonQnt) },
|
||||
{ label: "전기전도도(μS/cm)", key:"cdtnv", value: formatBelow(selectedSdmInfoStatus.cdtnv) },
|
||||
{ label: "비고", key:"rmrk", value: formatValue(selectedSdmInfoStatus.rmrk) },
|
||||
];
|
||||
}, [selectedSdmInfoStatus]);
|
||||
|
||||
const candidateNames = useMemo(() => {
|
||||
const list = isEditMode
|
||||
? editData?.splyCndtList ?? []
|
||||
: selectedSdmInfoStatus?.splyCndtList ?? [];
|
||||
return [
|
||||
list[0]?.ruseUtztnFcltNm ?? "",
|
||||
list[1]?.ruseUtztnFcltNm ?? "",
|
||||
list[2]?.ruseUtztnFcltNm ?? "",
|
||||
];
|
||||
}, [isEditMode, editData, selectedSdmInfoStatus]);
|
||||
|
||||
const onSelectSdmInfoStatus = (dmdInfoSn: number | "") => {
|
||||
if(isEditMode){
|
||||
if(confirm('저장되지 않은 데이터는 사라집니다.\n진행하시겠습니까?')){
|
||||
setIsEditMode(false);
|
||||
}
|
||||
}
|
||||
setSelectedDmdInfoSn(dmdInfoSn);
|
||||
requestAnimationFrame(() => {
|
||||
detailRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
};
|
||||
|
||||
// ph 전후로 나눔
|
||||
const getRange = (val: string) => {
|
||||
const a = val.split('~')[0]||''; const b = val.split('~')[1]||'';
|
||||
return { a, b };
|
||||
};
|
||||
|
||||
const searchCandidate = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const dmdInfoId = JSON.parse(
|
||||
sessionStorage.getItem("user") || "{}"
|
||||
)?.dmdInfoId?.toString();
|
||||
|
||||
const url = `/map/popup?dmdInfoId=${encodeURIComponent(dmdInfoId)}`;
|
||||
const width = window.screen.availWidth;
|
||||
const height = window.screen.availHeight;
|
||||
const left = 0;
|
||||
const top = 0;
|
||||
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
|
||||
window.open(url, "candidatePopup", popupOptions);
|
||||
};
|
||||
|
||||
const saveInfo = () => {
|
||||
if(confirm('수정된 데이터를 저장하시겠습니까?')){
|
||||
const payload = {
|
||||
...editData,
|
||||
splyCndtList : editData.splyCndtList ?? []
|
||||
};
|
||||
|
||||
updateSdmInfoData(payload).then((res) => {
|
||||
if (res) {
|
||||
const user = JSON.parse(sessionStorage.getItem("user") || "{}");
|
||||
if (user?.dmdInfoId) {
|
||||
getSdmInfoStatusList(user.dmdInfoId);
|
||||
}
|
||||
setIsEditMode(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (!event.data) return;
|
||||
|
||||
if (event.data.type === "SET_SUBS_INFO") {
|
||||
const list = event.data.payload || [];
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
splyCndtList: list,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
<main className="bg-white">
|
||||
<SubPageHeader
|
||||
title="수요신청현황"
|
||||
crumbs={[
|
||||
{ label: "Home", href: "/main" },
|
||||
{ label: "수요신청", href: "/apply/status" },
|
||||
{ label: "수요신청현황" },
|
||||
]}
|
||||
hideFooterLine
|
||||
hideGuideText
|
||||
/>
|
||||
|
||||
<div>
|
||||
<section className="mt-6">
|
||||
<div className="text-2xl font-bold text-slate-700">수요정보 신청현황</div>
|
||||
|
||||
<div className="mt-3 border border-neutral-200">
|
||||
<table className="w-full table-fixed">
|
||||
<thead className="bg-[#E7F4E2]">
|
||||
<tr className="text-sm text-slate-700">
|
||||
<th className="w-[60px] px-3 py-3 text-center">NO.</th>
|
||||
<th className="w-[120px] px-3 py-3 text-center">시도</th>
|
||||
<th className="w-[120px] px-3 py-3 text-center">시군구</th>
|
||||
<th className="w-[180px] px-3 py-3 text-center">회사명</th>
|
||||
<th className="px-3 py-3 text-center">공장주소</th>
|
||||
<th className="w-[140px] px-3 py-3 text-center">전화번호</th>
|
||||
<th className="w-[120px] px-3 py-3 text-center">요구시기</th>
|
||||
<th className="w-[120px] px-3 py-3 text-center">등록일자</th>
|
||||
<th className="w-[120px] px-3 py-3 text-center">수정일자</th>
|
||||
<th className="w-[110px] px-3 py-3 text-center">진행상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody className="bg-white text-base text-slate-700">
|
||||
{transformedSdmInfoStatusList.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="px-3 py-10 text-center text-slate-400">
|
||||
조회된 신청내역이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
transformedSdmInfoStatusList.map((sdmInfoStatus, index) => {
|
||||
const active = sdmInfoStatus.dmdInfoSn === selectedSdmInfoStatus?.dmdInfoSn;
|
||||
return (
|
||||
<tr
|
||||
key={`${sdmInfoStatus.dmdInfoSn}-${index}`}
|
||||
role="row"
|
||||
tabIndex={0}
|
||||
onClick={() => onSelectSdmInfoStatus(sdmInfoStatus.dmdInfoSn)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
onSelectSdmInfoStatus(sdmInfoStatus.dmdInfoSn);
|
||||
}
|
||||
}}
|
||||
className={[
|
||||
"border-t border-neutral-200 outline-none",
|
||||
"cursor-pointer",
|
||||
active ? "bg-slate-50" : "hover:bg-slate-50",
|
||||
"focus-visible:ring-2 focus-visible:ring-slate-300",
|
||||
].join(" ")}
|
||||
aria-selected={active}
|
||||
>
|
||||
<td className="px-3 py-3 text-center">{sdmInfoStatus.index || index + 1}</td>
|
||||
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.ctpvCd)}</td>
|
||||
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.sggCd)}</td>
|
||||
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.dmdCoNm)}</td>
|
||||
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.daddr)}</td>
|
||||
<td className="px-3 py-3 text-center">{formatValue(sdmInfoStatus.telno)}</td>
|
||||
<td className="px-3 py-3 text-center">{formatDate(sdmInfoStatus.dmndTmng)}</td>
|
||||
<td className="px-3 py-3 text-center">{formatDate(sdmInfoStatus.regDt)}</td>
|
||||
<td className="px-3 py-3 text-center">{formatDate(sdmInfoStatus.mdfcnDt)}</td>
|
||||
<td className="px-3 py-3 text-center">
|
||||
<StatusBadge v={formatValue(sdmInfoStatus.status)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 pb-16" ref={detailRef}>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-2xl font-bold text-slate-700">수요신청 상세현황</div>
|
||||
{
|
||||
!isEditMode ? (
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
|
||||
onClick={() => {
|
||||
setOriginData(selectedSdmInfoStatus);
|
||||
setEditData({...selectedSdmInfoStatus});
|
||||
setIsEditMode(true)}
|
||||
}
|
||||
>
|
||||
수정
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
|
||||
onClick={() => {
|
||||
saveInfo();
|
||||
}}
|
||||
>
|
||||
저장
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 rounded-md bg-[#888] px-4 text-base text-white hover:bg-neutral-700"
|
||||
onClick={() => {
|
||||
setEditData(originData);
|
||||
setIsEditMode(false)
|
||||
}}
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{!selectedSdmInfoStatus ? (
|
||||
<div className="mt-3 border border-neutral-200 px-6 py-10 text-center text-sm text-slate-400">
|
||||
상세 정보를 표시할 신청건이 없습니다.
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 border border-neutral-200">
|
||||
<div className="bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
|
||||
요구수량
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-12 text-sm">
|
||||
{topFields.map((field) => (
|
||||
<div
|
||||
key={field.label}
|
||||
className="col-span-12 md:col-span-6 grid grid-cols-12 border-b border-neutral-200"
|
||||
>
|
||||
<div className="col-span-4 bg-sky-50 px-4 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
|
||||
{field.label}
|
||||
{field.required ? <span className="ml-1 text-red-500">*</span> : null}
|
||||
</div>
|
||||
{
|
||||
!isEditMode ?
|
||||
<div className="col-span-8 px-4 py-4 flex items-center justify-center text-center">
|
||||
{field.key==='useUsgCd'? useTypesData.filter(item=>item.val===field.value)[0]?.text: field.value}
|
||||
</div>
|
||||
: <div className="col-span-8 px-4 py-4 flex items-center justify-center text-center">
|
||||
{field.key ==='dmndTmng'?
|
||||
<DateInput value={editData?.[field.key] ?? ""} onChange={(v) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
[field.key]: v,
|
||||
}))
|
||||
} />
|
||||
: field.key ==='useUsgCd'?
|
||||
<select
|
||||
value={editData?.[field.key] ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
[field.key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500"
|
||||
>
|
||||
<option value="">선택하세요</option>
|
||||
{useTypesData.map((item) => (
|
||||
<option key={item.val} value={item.val}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
: <TextInput value={editData?.[field.key] ?? ""} onChange={(v) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
[field.key]: v,
|
||||
}))
|
||||
}></TextInput>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
|
||||
요구수질
|
||||
</div>
|
||||
|
||||
{waterFields.map((field) => {
|
||||
const { a, b } = editData?.ph ? getRange(editData.ph) : { a: "", b: "" };
|
||||
return (
|
||||
<div
|
||||
key={field.label}
|
||||
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b border-neutral-200"
|
||||
>
|
||||
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
|
||||
{field.label}
|
||||
</div>
|
||||
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
|
||||
{
|
||||
!isEditMode ? field.value
|
||||
:
|
||||
field.key === "grssClfg" ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<TextInput
|
||||
value={
|
||||
editData?.grssClfg === -1
|
||||
? ""
|
||||
: editData?.grssClfg ?? ""
|
||||
}
|
||||
onChange={(v) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
grssClfg: v,
|
||||
}))
|
||||
}
|
||||
disabled={editData?.grssClfg === -1}
|
||||
className={
|
||||
editData?.grssClfg === -1
|
||||
? "bg-neutral-100 text-neutral-400"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
<label className="flex items-center gap-2 w-3/4 text-sm text-neutral-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editData?.grssClfg === -1}
|
||||
onChange={(e) => {
|
||||
const checked = e.target.checked;
|
||||
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
grssClfg: checked ? -1 : "",
|
||||
}));
|
||||
}}
|
||||
className="h-4 w-4 rounded border-neutral-300"
|
||||
/>
|
||||
불검출
|
||||
</label>
|
||||
</div>
|
||||
) : field.key === 'smll' ? (
|
||||
<select
|
||||
value={
|
||||
editData?.smll === -1
|
||||
? ""
|
||||
: editData?.smll ?? ""
|
||||
}
|
||||
onChange={(v) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
grssClfg: v,
|
||||
}))
|
||||
}
|
||||
className="h-11 w-full max-w-[200px] rounded-md border border-neutral-300 bg-white px-3 text-sm outline-none focus:border-neutral-500">
|
||||
<option>불쾌하지 않을 것</option>
|
||||
<option>약간 있음</option>
|
||||
<option>있음</option>
|
||||
</select>
|
||||
) : field.key === 'ph' ? (
|
||||
<>
|
||||
<TextInput
|
||||
value={a}
|
||||
onChange={(v) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
[field.key]: `${v}~${b}`,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
~
|
||||
<TextInput
|
||||
value={b}
|
||||
onChange={(v) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
[field.key]: `${a}~${v}`,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TextInput
|
||||
value={editData?.[field.key] ?? ""}
|
||||
onChange={(v) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
[field.key]: v,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{
|
||||
field.key==='tn'||field.key === 'tph'||field.key === 'trbt'||field.key==='blgcOxygDmndQnt'||field.key==='clIonQnt'||field.key==='cdtnv'|| field.key=='crmty'
|
||||
?<>이하</>:<></>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)})}
|
||||
|
||||
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-b border-neutral-200">
|
||||
공급처 후보지
|
||||
{
|
||||
isEditMode &&
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 w-25 ml-12 rounded-md bg-[#888] px-2 text-base text-white hover:bg-neutral-700"
|
||||
onClick={(e) => {
|
||||
searchCandidate(e);
|
||||
|
||||
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
|
||||
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
|
||||
}}>
|
||||
후보지 조회
|
||||
</button>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
{candidateNames.map((candidateName, index) => (
|
||||
isEditMode ? (
|
||||
<div
|
||||
key={`candidate-${index}`}
|
||||
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200"
|
||||
>
|
||||
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
|
||||
후보지{index + 1}
|
||||
</div>
|
||||
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
|
||||
<TextInput
|
||||
value={candidateName}
|
||||
disabled={true}
|
||||
onChange={(v) => {
|
||||
setEditData((prev) => {
|
||||
const newList = [...(prev.splyCndtList || [])];
|
||||
newList[index] = {
|
||||
...newList[index],
|
||||
ruseUtztnFcltNm: v,
|
||||
};
|
||||
return {
|
||||
...prev,
|
||||
splyCndtList: newList,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) :
|
||||
<div
|
||||
key={`candidate-${index}`}
|
||||
className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200"
|
||||
>
|
||||
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
|
||||
후보지{index + 1}
|
||||
</div>
|
||||
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
|
||||
{candidateName}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="col-span-12 bg-sky-50 px-4 py-3 text-sm font-semibold text-slate-700 border-t border-b border-neutral-200">
|
||||
수요신청 진행상태
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200">
|
||||
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
|
||||
진행상태
|
||||
</div>
|
||||
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
|
||||
{formatValue(selectedSdmInfoStatus.status)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-12 md:col-span-4 grid grid-cols-12 border-b md:border-b-0 border-neutral-200">
|
||||
<div className="col-span-5 bg-sky-50 px-3 py-4 flex items-center justify-center text-center font-semibold text-slate-700">
|
||||
공급처매칭결과
|
||||
</div>
|
||||
<div className="col-span-7 px-3 py-4 flex items-center justify-center text-center">
|
||||
{formatValue(selectedSdmInfoStatus.ruseUtztnFcltNm)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user