Merge branch 'iipbs/sn/dev'
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@ NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service
|
||||
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_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
|
||||
#NEXT_PUBLIC_SERVER_API_URL = http://localhost:8883/operation-service
|
||||
|
||||
|
||||
@@ -16,4 +16,4 @@ NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com/dc-service
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service
|
||||
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_CM_LOGIN_PAGE_URL = /
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
@@ -17,4 +17,3 @@ NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-s
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service
|
||||
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_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
@@ -0,0 +1,2 @@
|
||||
import { LoginPage } from "@/src/features/auth/login";
|
||||
export default LoginPage;
|
||||
@@ -1,4 +1,28 @@
|
||||
import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env";
|
||||
import { CLAIM_NAME, SITE_ID } from "@/src/shared/config/env";
|
||||
import { tokenManager } from "@/src/entities/auth/lib/token-manager";
|
||||
import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "./config/env";
|
||||
|
||||
const LOGIN_PAGE_URL =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
|
||||
const handleUnauthorizedResponse = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME);
|
||||
const isExpired = remainingMs === null || remainingMs <= 0;
|
||||
|
||||
if (!isExpired) return;
|
||||
|
||||
tokenManager.clearAuthToken();
|
||||
|
||||
const isLoginPage =
|
||||
window.location.pathname === "/auth/login" ||
|
||||
window.location.pathname.startsWith("/auth/login/");
|
||||
|
||||
if (!isLoginPage) {
|
||||
window.location.assign(LOGIN_PAGE_URL);
|
||||
}
|
||||
};
|
||||
|
||||
export const getFetch = async (part : string, url: string) => {
|
||||
let reqUrl = "";
|
||||
@@ -7,8 +31,12 @@ export const getFetch = async (part : string, url: string) => {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
}
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -21,8 +49,12 @@ export const getFetchAreaList = async (url: string) => {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
}
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -37,9 +69,13 @@ export const postFetch = async (part : string, url: string, params: unknown) =>
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -53,9 +89,13 @@ export const putFetch = async (part : string, url: string, params: unknown) => {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -69,11 +109,15 @@ export const deleteFetch = async (part : string, url: string, params: unknown) =
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if(!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { SITE_ID } from "@/src/shared/config/env";
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -59,7 +60,10 @@ const Map = () => {
|
||||
const reqUrl = `${API_URL}/lyrStngs/FO_NETWORK`;
|
||||
const response = await fetch(reqUrl, {
|
||||
method: "GET",
|
||||
headers: { accept: "application/json" },
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error(`HTTP 오류! 상태: ${response.status}`);
|
||||
const data = await response.json();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { SITE_ID } from "@/src/shared/config/env";
|
||||
import React from "react";
|
||||
|
||||
export default function TestPage() {
|
||||
@@ -6,6 +7,9 @@ export default function TestPage() {
|
||||
const handleFindUser = () => {
|
||||
fetch('/operation-service/api/v1/fclt/flow/1/10/2024', {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
}).then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
@@ -19,4 +23,4 @@ export default function TestPage() {
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -1,6 +1,13 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
|
||||
import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie";
|
||||
|
||||
type JwtPayload = {
|
||||
sub?: string;
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export class TokenManager {
|
||||
private static instance: TokenManager;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
@@ -18,6 +25,49 @@ export class TokenManager {
|
||||
return getCookie(key);
|
||||
}
|
||||
|
||||
private parseJwt(token: string): JwtPayload {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("유효한 JWT 형식이 아닙니다.");
|
||||
}
|
||||
|
||||
const base64Url = parts[1] ?? "";
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(padded)
|
||||
.split("")
|
||||
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`)
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload) as JwtPayload;
|
||||
}
|
||||
|
||||
public getRemainingMsFromToken(key: string = CLAIM_NAME): number | null {
|
||||
const token = this.getToken(key);
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.parseJwt(token);
|
||||
if (typeof payload.exp !== "number") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload.exp * 1000 - Date.now();
|
||||
} catch (error) {
|
||||
console.error("JWT 파싱 오류:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public isExpiredToken(key: string = CLAIM_NAME): boolean {
|
||||
const remainingMs = this.getRemainingMsFromToken(key);
|
||||
return remainingMs === null || remainingMs <= 0;
|
||||
}
|
||||
|
||||
public setToken(key: string, value: string): void {
|
||||
setCookie(key, value, {
|
||||
secure: location.protocol === "https:",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { LoginPage } from "./ui/login-page";
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect } from "react";
|
||||
import { images } from "@common/assets";
|
||||
import { LoginApi } from "@/src/entities/auth";
|
||||
import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
useEffect(() => {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes("hasuon.or.kr")) {
|
||||
window.location.href =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const userId = formData.get("id") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
await LoginApi.login({ userId, password });
|
||||
window.location.href = "/pages/dashboard";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login fx">
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
하수도정보시스템
|
||||
<br />
|
||||
<span>CMS 통합관리시스템</span>
|
||||
</h1>
|
||||
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
id="loginForm"
|
||||
name="loginForm"
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="login_box fx">
|
||||
<ul className="fx">
|
||||
<li>
|
||||
<h2>ID</h2>
|
||||
<div className="input_st st1">
|
||||
<i className="diveicon di-account-box" />
|
||||
<input
|
||||
type="text"
|
||||
className="id"
|
||||
title="아이디를 입력하세요."
|
||||
id="id"
|
||||
name="id"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h2>PASSWORD</h2>
|
||||
<div className="input_st st2">
|
||||
<i className="diveicon di-key" />
|
||||
<input
|
||||
type="password"
|
||||
className="input_style"
|
||||
maxLength={25}
|
||||
title="비밀번호를 입력하세요."
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bottom_box fx">
|
||||
<div className="checks fx">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="checkId"
|
||||
title="로그인ID 저장여부"
|
||||
id="checkId"
|
||||
/>
|
||||
<label htmlFor="checkId">아이디저장</label>
|
||||
</div>
|
||||
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
|
||||
</div>
|
||||
<button type="submit" className="login_btn fx">
|
||||
<span>LOGIN</span>
|
||||
</button>
|
||||
<div className="footer">
|
||||
<div className="copy fx">
|
||||
<p>copyright © 2023 KECO. All rights reserved.</p>
|
||||
</div>
|
||||
<div className="fot_btn fx">
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
개인정보처리방침
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
저작권정책안내
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
이메일무단수집거부
|
||||
</a>
|
||||
</div>
|
||||
<p className="addr fx">
|
||||
<span>
|
||||
주소 : (22689) 인천광역시 서구 환경로 42(경서동
|
||||
종합환경연구단지)
|
||||
</span>
|
||||
<span>
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
|
||||
3722
|
||||
</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* OLYM Cascading Style Sheets
|
||||
* File Name : login.css
|
||||
* Description : 로그인 스타일시트
|
||||
* Author : Web business Team / Choi Hyun Mi
|
||||
* Email : desingtj@olym.co.kr
|
||||
* Date : 2023.07.20
|
||||
* Update : 2023.07.26
|
||||
* Copyright(c) 2023 OLYM Communications. All Rights Reserved.
|
||||
*/
|
||||
html,
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
.fx {
|
||||
display: flex;
|
||||
-ms-display: flex;
|
||||
-webkit-display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.login .left_box {
|
||||
width: 100%;
|
||||
background: url(/images/bg_login.jpg) no-repeat center/cover;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 7% 5% 5% 5%;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 53px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .left_box .text h1 span {
|
||||
font-weight: 800;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 40px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .login_box {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 4% 12% 4% 6%;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #2640a5;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st {
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box > ul > li .input_st .diveicon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 28px;
|
||||
transform: translateY(-50%);
|
||||
font-family: "Xeicon";
|
||||
font-size: 25px;
|
||||
color: #003288;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
width: 100%;
|
||||
padding: 20px 20px 20px 70px;
|
||||
background: #f7f7f7;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
height: auto;
|
||||
}
|
||||
.login .login_box > ul > li input:focus {
|
||||
outline: 1px solid #3c5fe5;
|
||||
}
|
||||
.login .login_box > ul > li input::placeholder {
|
||||
color: #afabab;
|
||||
font-weight: 500;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 50px 0 45px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .checks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #636363;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message i {
|
||||
color: #327af5;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
padding: 24px 15px;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-family: "Montserrat";
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 22px;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.login .login_box .login_btn:before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.login .login_box .login_btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.login .footer .copy {
|
||||
margin-top: 36px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 3%;
|
||||
}
|
||||
.login .footer .copy p {
|
||||
font-size: 13px;
|
||||
color: #d4d4d4;
|
||||
width: 44%;
|
||||
text-align: center;
|
||||
}
|
||||
.login .footer .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
content: "";
|
||||
width: 20%;
|
||||
height: 1px;
|
||||
background: #d4d4d4;
|
||||
}
|
||||
.login .footer .fot_btn {
|
||||
gap: 20px;
|
||||
margin: 30px 0 10px 0;
|
||||
}
|
||||
.login .footer .fot_btn a {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.login .footer .fot_btn a:hover {
|
||||
color: #003288;
|
||||
}
|
||||
.login .footer .addr {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
gap: 3px 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* 비밀번호 변경 팝업창 */
|
||||
.login .modal .modal_container {
|
||||
width: 750px;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 10px 10px 30px 10px;
|
||||
color: #666;
|
||||
}
|
||||
.login .pw_desc p {
|
||||
font-size: 1.25rem;
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.login .pw_desc span {
|
||||
color: #f60;
|
||||
font-weight: 700;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 20px 50px;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.login {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
.login .left_box {
|
||||
padding: 30px;
|
||||
flex-direction: column-reverse;
|
||||
gap: 20px;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 30px;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .left_box .img img {
|
||||
height: 40px;
|
||||
}
|
||||
.login .login_box {
|
||||
padding: 30px;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
gap: 20px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st::after {
|
||||
left: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
padding: 15px 10px 15px 50px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 30px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .id_save label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
padding: 15px;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 18px;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.login .login_box .copy p {
|
||||
width: 100%;
|
||||
}
|
||||
.login .login_box .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
display: none;
|
||||
}
|
||||
.login .footer .addr {
|
||||
font-size: 11px;
|
||||
gap: 3px 10px;
|
||||
}
|
||||
.login .modal .modal_container {
|
||||
width: 90%;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
/* // login */
|
||||
+2
-2
@@ -14,5 +14,5 @@ NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&ver
|
||||
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://localhost:8883
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883
|
||||
|
||||
@@ -13,4 +13,4 @@ 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
|
||||
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
@@ -15,4 +15,3 @@ 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_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Button, TableHead, TableRow } from '@common/components/ui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
|
||||
@@ -65,22 +65,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`);
|
||||
|
||||
// helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM"
|
||||
const formatNoYearNoSec = (val: any) => {
|
||||
@@ -138,22 +123,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`);
|
||||
|
||||
// 각 아이템 전처리
|
||||
const processedDataArray = result.content.map((data: any) => {
|
||||
@@ -355,4 +325,4 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
)
|
||||
}
|
||||
|
||||
export default Ssc
|
||||
export default Ssc
|
||||
|
||||
@@ -15,40 +15,20 @@
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { getData } from '@/components/lib/api';
|
||||
|
||||
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
|
||||
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
|
||||
const BASE_URL = "/sewerage-service/api";
|
||||
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
let data: any = null;
|
||||
if (res.ok) {
|
||||
try {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get("content-length");
|
||||
if (contentLength === "0") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`);
|
||||
} catch (error) {
|
||||
console.error("JSON parsing error:", error);
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === "") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// JSON 파싱 시도
|
||||
const response = JSON.parse(text);
|
||||
data = response.data;
|
||||
} catch (error) {
|
||||
console.error("JSON parsing error:", error);
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!data) {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
@@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: ()
|
||||
)
|
||||
}
|
||||
|
||||
export default Smh;
|
||||
export default Smh;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import { getData } from "../../components/lib/api";
|
||||
import { getRawJson } from "../../components/lib/api";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import SearchModal from "./searchModal/searchModal";
|
||||
@@ -43,22 +43,7 @@ const Map = () => {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let data;
|
||||
const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') data = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') data = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
data = JSON.parse(text);
|
||||
}
|
||||
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
setTreeData(data);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
// FileUpload.js
|
||||
import React, { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { SITE_ID } from '@/shared/config';
|
||||
const SEWERAGE_URL = "/sewerage-service/api";
|
||||
|
||||
function FileUpload() {
|
||||
@@ -49,6 +50,7 @@ function FileUpload() {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify({authToken: auth})
|
||||
}).then((data) => {
|
||||
@@ -58,7 +60,11 @@ function FileUpload() {
|
||||
const handleFindUserOna = () => {
|
||||
const auth = getCookie("Authorization");
|
||||
if (auth) console.log("쿠키 auth : " + auth);
|
||||
fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b").then((data) => {
|
||||
fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b", {
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
}).then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
};
|
||||
@@ -86,7 +92,11 @@ function FileUpload() {
|
||||
formData.append('inpFile', file);
|
||||
|
||||
try {
|
||||
const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData);
|
||||
const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData, {
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
});
|
||||
|
||||
alert('업로드 성공: ' + response.data.message);
|
||||
} catch (err) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useState, useEffect } from 'react'
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar'
|
||||
import { UITab, UiTable } from '@common/business_components/ui'
|
||||
import * as XLSX from 'xlsx';
|
||||
import { getBlobData } from '@/components/lib/api';
|
||||
// import { Table, TableBody, TableHeader } from "@common/components/ui";
|
||||
// import { HeadMakeFloodPre } from './headMakeFloodPre';
|
||||
// import { InputTableBody } from './inputTableBody';
|
||||
@@ -54,12 +55,7 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
return false
|
||||
}
|
||||
|
||||
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
|
||||
|
||||
const file = new File([blob], fileNm, {
|
||||
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
@@ -267,4 +263,4 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
)
|
||||
};
|
||||
|
||||
export default TableModal;
|
||||
export default TableModal;
|
||||
|
||||
@@ -19,6 +19,7 @@ import { UITab, UiTable } from '@common/business_components/ui'
|
||||
import * as XLSX from 'xlsx';
|
||||
import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan';
|
||||
import { oldPpTable2HeadList, oldPpTable2NodeList, oldPpTable6HeadList, oldPpTable6NodeList } from './headList';
|
||||
import { getBlobData } from '@/components/lib/api';
|
||||
|
||||
const TABS = [
|
||||
{ val: "1", text: "raw data" },
|
||||
@@ -63,12 +64,7 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
|
||||
|
||||
const file = new File([blob], fileNm, {
|
||||
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
@@ -476,4 +472,4 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
)
|
||||
};
|
||||
|
||||
export default TableModal;
|
||||
export default TableModal;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import { LoginPage } from "@/features/auth/login";
|
||||
export default LoginPage;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const hostname = request.nextUrl.hostname;
|
||||
const redirectUrl = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "https://portal.hasuon.or.kr";
|
||||
|
||||
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "con.mind-one.co.kr") {
|
||||
tokenManager.clearAuthToken();
|
||||
return NextResponse.redirect(new URL("/", request.url)); // 로컬 및 개발 환경
|
||||
} else {
|
||||
return NextResponse.redirect(new URL(redirectUrl)); // 공단 환경
|
||||
@@ -11,13 +11,51 @@
|
||||
*/
|
||||
import { useEffect, useState } from "react";
|
||||
import axios from 'axios';
|
||||
import { CLAIM_NAME, SITE_ID } from "@/shared/config";
|
||||
import { tokenManager } from "@/entities/auth/lib/token-manager";
|
||||
|
||||
const BASE_URL = '/sewerage-service/api';
|
||||
const DEFAULT_HEADERS = {
|
||||
"X-Site-Id": SITE_ID,
|
||||
};
|
||||
const LOGIN_PAGE_URL =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
|
||||
const withSiteIdHeaders = (headers: HeadersInit = {}) => ({
|
||||
...headers,
|
||||
...DEFAULT_HEADERS,
|
||||
});
|
||||
|
||||
const resolveApiUrl = (url: string) =>
|
||||
url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`;
|
||||
|
||||
const handleUnauthorizedResponse = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME);
|
||||
const isExpired = remainingMs === null || remainingMs <= 0;
|
||||
|
||||
if (!isExpired) return;
|
||||
|
||||
tokenManager.clearAuthToken();
|
||||
|
||||
const isLoginPage =
|
||||
window.location.pathname === "/auth/login" ||
|
||||
window.location.pathname.startsWith("/auth/login/");
|
||||
|
||||
if (!isLoginPage) {
|
||||
window.location.assign(LOGIN_PAGE_URL);
|
||||
}
|
||||
};
|
||||
|
||||
export const getData = async (url: string): Promise<any> => {
|
||||
const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, {
|
||||
const res = await fetch(resolveApiUrl(url), {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
try {
|
||||
// 응답이 비어있는지 확인
|
||||
@@ -47,12 +85,15 @@ export const getData = async (url: string): Promise<any> => {
|
||||
export const postData = async (url: string, data: object): Promise<any> => {
|
||||
const options: RequestInit = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
headers: withSiteIdHeaders({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
const res = await fetch(`${BASE_URL}/${url}`, options);
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
const resData = await res.json();
|
||||
console.log('postData response:', resData);
|
||||
@@ -62,6 +103,7 @@ export const postData = async (url: string, data: object): Promise<any> => {
|
||||
|
||||
export const postMutiPart = async (url: string, data: FormData): Promise<any> => {
|
||||
const res = await axios.post(`${BASE_URL}/${url}`, data, {
|
||||
headers: DEFAULT_HEADERS,
|
||||
timeout: 600000, // 10분
|
||||
});
|
||||
console.log('postMultiPart response:', res);
|
||||
@@ -71,12 +113,15 @@ export const postMutiPart = async (url: string, data: FormData): Promise<any> =>
|
||||
export const putData = async (url: string, data: object): Promise<any> => {
|
||||
const options: RequestInit = {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
headers: withSiteIdHeaders({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify(data),
|
||||
};
|
||||
const res = await fetch(`${BASE_URL}/${url}`, options);
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
const resData = await res.json();
|
||||
console.log('putData response:', resData);
|
||||
return resData.data;
|
||||
@@ -85,21 +130,75 @@ export const putData = async (url: string, data: object): Promise<any> => {
|
||||
export const deleteData = async (url: string, data?: object): Promise<any> => {
|
||||
const options: RequestInit = {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
headers: withSiteIdHeaders({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
};
|
||||
const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, options);
|
||||
const res = await fetch(resolveApiUrl(url), options);
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.status === 204) return true
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getRawJson = async (url: string): Promise<any> => {
|
||||
const res = await fetch(resolveApiUrl(url), {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data');
|
||||
}
|
||||
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
throw new Error('Invalid JSON response from server');
|
||||
}
|
||||
};
|
||||
|
||||
export const getBlobData = async (url: string): Promise<Blob> => {
|
||||
const res = await fetch(resolveApiUrl(url), {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to fetch data');
|
||||
}
|
||||
|
||||
return res.blob();
|
||||
};
|
||||
|
||||
// DC API 호출
|
||||
export const getDcData = async (url: string, resType: string = "json"): Promise<any> => {
|
||||
const res = await fetch('/dc-service' + url, {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
let data;
|
||||
if (resType === "json") data = await res.json();
|
||||
@@ -114,7 +213,11 @@ export const getDcData = async (url: string, resType: string = "json"): Promise<
|
||||
export const getCmsData = async (url: string, data?: object): Promise<any> => {
|
||||
const res = await fetch('/cms-service' + url, {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
return data;
|
||||
@@ -126,7 +229,11 @@ export const getCmsData = async (url: string, data?: object): Promise<any> => {
|
||||
export const getGeoData = async (url: string, data?: object): Promise<any> => {
|
||||
const res = await fetch('/geoserver' + url, {
|
||||
method: "GET",
|
||||
headers: withSiteIdHeaders(),
|
||||
});
|
||||
if (res.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
return data;
|
||||
@@ -157,4 +264,4 @@ export const useCookieAuth = () => {
|
||||
}, []);
|
||||
|
||||
return data;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* - 첨부파일 삭제및 Soft Delete 삭제(첨부파일코드로 삭제): DELETE /api/v1/atfl/attachments/atflCd/{atflCd} (attach-service)
|
||||
*/
|
||||
import { getCookie } from "@/shared/lib";
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
import { ENDPOINT } from "../api/attachment/attachment-service";
|
||||
|
||||
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
|
||||
@@ -43,6 +44,7 @@ export const handleDelete = async (
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
|
||||
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
|
||||
},
|
||||
@@ -118,4 +120,4 @@ export const handleDelete = async (
|
||||
// options?.handleDeleteError?.(deleteError);
|
||||
// throw deleteError;
|
||||
// }
|
||||
// };
|
||||
// };
|
||||
|
||||
@@ -10,10 +10,15 @@
|
||||
* - Fetch 기반 HTTP 요청 처리
|
||||
*/
|
||||
|
||||
import { SITE_ID } from "@/shared/config";
|
||||
|
||||
export const handleDownload = async (api: string, fileNm: string) => {
|
||||
try {
|
||||
const response = await fetch(api, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error('파일 다운로드 실패');
|
||||
|
||||
@@ -34,4 +39,4 @@ export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터
|
||||
// 메모리 해제
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
import useStore from "@/store/useStore";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import AgedPipeFilterPanel from "./AgedPipeFilterPanel";
|
||||
import AgedPipeDetailPanel from "./AgedPipeDetailPanel";
|
||||
import AgedPipeListModal from "./AgedPipeListModal";
|
||||
@@ -102,19 +102,7 @@ const Map = () => {
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let data;
|
||||
const response = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const contentLength = response.headers.get("content-length");
|
||||
if (contentLength === "0") data = null;
|
||||
|
||||
const text = await response.text();
|
||||
if (!text || text.trim() === "") data = null;
|
||||
else data = JSON.parse(text);
|
||||
}
|
||||
|
||||
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
setTreeData(data);
|
||||
} catch (error) {
|
||||
console.error("노후관로 레이어 설정 조회 중 오류:", error);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { Button, TableHead, TableRow } from '@common/components/ui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getRawJson } from "@/components/lib/api";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
|
||||
@@ -65,22 +65,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`);
|
||||
|
||||
// helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM"
|
||||
const formatNoYearNoSec = (val: any) => {
|
||||
@@ -138,22 +123,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`);
|
||||
|
||||
// 각 아이템 전처리
|
||||
const processedDataArray = result.content.map((data: any) => {
|
||||
@@ -355,4 +325,4 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
)
|
||||
}
|
||||
|
||||
export default Ssc
|
||||
export default Ssc
|
||||
|
||||
@@ -15,40 +15,20 @@
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { getData } from '@/components/lib/api';
|
||||
|
||||
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
|
||||
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
|
||||
const BASE_URL = "/sewerage-service/api";
|
||||
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
let data: any = null;
|
||||
if (res.ok) {
|
||||
try {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get("content-length");
|
||||
if (contentLength === "0") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`);
|
||||
} catch (error) {
|
||||
console.error("JSON parsing error:", error);
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === "") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// JSON 파싱 시도
|
||||
const response = JSON.parse(text);
|
||||
data = response.data;
|
||||
} catch (error) {
|
||||
console.error("JSON parsing error:", error);
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!data) {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
@@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: ()
|
||||
)
|
||||
}
|
||||
|
||||
export default Smh;
|
||||
export default Smh;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import { getData } from "../../../components/lib/api";
|
||||
import { getRawJson } from "../../../components/lib/api";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import SearchModal from "./searchModal/searchModal";
|
||||
@@ -43,22 +43,7 @@ const Map = () => {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let data;
|
||||
const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') data = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') data = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
data = JSON.parse(text);
|
||||
}
|
||||
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
setTreeData(data);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -5,6 +5,7 @@ import {
|
||||
ACCESS_TOKEN,
|
||||
AUTH_USER_ID,
|
||||
CLAIM_NAME,
|
||||
SITE_ID,
|
||||
} from "@/shared/config";
|
||||
import type { LoginService, ILogin } from "../model/login-interface";
|
||||
import { tokenManager } from "../lib/token-manager";
|
||||
@@ -23,8 +24,13 @@ export class LoginApiService implements LoginService {
|
||||
|
||||
public async login(payload: ILogin): Promise<void> {
|
||||
try {
|
||||
tokenManager.clearAuthToken();
|
||||
const response = await fetch(`/api/proxy${ENDPOINT.USER_SERVICE.LOGIN}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: payload.userId,
|
||||
password: payload.password,
|
||||
@@ -57,7 +63,12 @@ export class LoginApiService implements LoginService {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{ method: "PUT" },
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useUserStore } from "@/shared/lib";
|
||||
import { ENDPOINT, AUTH_USER_ID } from "@/shared/config";
|
||||
import { isAtLoginRedirectUrl } from "@common/network";
|
||||
import { getCookie } from "@/shared/lib";
|
||||
import { tokenManager } from "./token-manager";
|
||||
|
||||
const LOGIN_PAGE_URL =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
@@ -38,6 +39,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
await authCheck();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
setUserInfo(null);
|
||||
|
||||
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return;
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/shared/config";
|
||||
import { getCookie, setCookie, removeCookie } from "@/shared/lib";;
|
||||
|
||||
type JwtPayload = {
|
||||
sub?: string;
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export class TokenManager {
|
||||
private static instance: TokenManager;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
@@ -18,6 +25,49 @@ export class TokenManager {
|
||||
return getCookie(key);
|
||||
}
|
||||
|
||||
private parseJwt(token: string): JwtPayload {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("유효한 JWT 형식이 아닙니다.");
|
||||
}
|
||||
|
||||
const base64Url = parts[1] ?? "";
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(padded)
|
||||
.split("")
|
||||
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`)
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload) as JwtPayload;
|
||||
}
|
||||
|
||||
public getRemainingMsFromToken(key: string = CLAIM_NAME): number | null {
|
||||
const token = this.getToken(key);
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.parseJwt(token);
|
||||
if (typeof payload.exp !== "number") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload.exp * 1000 - Date.now();
|
||||
} catch (error) {
|
||||
console.error("JWT 파싱 오류:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public isExpiredToken(key: string = CLAIM_NAME): boolean {
|
||||
const remainingMs = this.getRemainingMsFromToken(key);
|
||||
return remainingMs === null || remainingMs <= 0;
|
||||
}
|
||||
|
||||
public setToken(key: string, value: string): void {
|
||||
// 환경에 따른 도메인 설정
|
||||
//const domain = this.getCookieDomain();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { LoginPage } from "./ui/login-page";
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect } from "react";
|
||||
import { images } from "@common/assets";
|
||||
import { LoginApi } from "@/entities/auth";
|
||||
import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
useEffect(() => {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes("hasuon.or.kr")) {
|
||||
window.location.href =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const userId = formData.get("id") as string;
|
||||
const password = formData.get("password") as string;
|
||||
|
||||
await LoginApi.login({ userId, password });
|
||||
window.location.href = "/map";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login fx">
|
||||
<div className="left_box fx">
|
||||
<div className="text">
|
||||
<h1>
|
||||
하수도정보시스템
|
||||
<br />
|
||||
<span>CMS 통합관리시스템</span>
|
||||
</h1>
|
||||
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
|
||||
</div>
|
||||
<div className="img">
|
||||
<Image src={images.logo} width={229} height={46} alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
id="loginForm"
|
||||
name="loginForm"
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="login_box fx">
|
||||
<ul className="fx">
|
||||
<li>
|
||||
<h2>ID</h2>
|
||||
<div className="input_st st1">
|
||||
<i className="diveicon di-account-box" />
|
||||
<input
|
||||
type="text"
|
||||
className="id"
|
||||
title="아이디를 입력하세요."
|
||||
id="id"
|
||||
name="id"
|
||||
maxLength={12}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<h2>PASSWORD</h2>
|
||||
<div className="input_st st2">
|
||||
<i className="diveicon di-key" />
|
||||
<input
|
||||
type="password"
|
||||
className="input_style"
|
||||
maxLength={25}
|
||||
title="비밀번호를 입력하세요."
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="bottom_box fx">
|
||||
<div className="checks fx">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="checkId"
|
||||
title="로그인ID 저장여부"
|
||||
id="checkId"
|
||||
/>
|
||||
<label htmlFor="checkId">아이디저장</label>
|
||||
</div>
|
||||
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
|
||||
</div>
|
||||
<button type="submit" className="login_btn fx">
|
||||
<span>LOGIN</span>
|
||||
</button>
|
||||
<div className="footer">
|
||||
<div className="copy fx">
|
||||
<p>copyright © 2023 KECO. All rights reserved.</p>
|
||||
</div>
|
||||
<div className="fot_btn fx">
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
개인정보처리방침
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
저작권정책안내
|
||||
</a>
|
||||
<a
|
||||
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
|
||||
target="_blank"
|
||||
>
|
||||
이메일무단수집거부
|
||||
</a>
|
||||
</div>
|
||||
<p className="addr fx">
|
||||
<span>
|
||||
주소 : (22689) 인천광역시 서구 환경로 42(경서동
|
||||
종합환경연구단지)
|
||||
</span>
|
||||
<span>
|
||||
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
|
||||
3722
|
||||
</span>
|
||||
<span>팩스 :032-590-3729</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* OLYM Cascading Style Sheets
|
||||
* File Name : login.css
|
||||
* Description : 로그인 스타일시트
|
||||
* Author : Web business Team / Choi Hyun Mi
|
||||
* Email : desingtj@olym.co.kr
|
||||
* Date : 2023.07.20
|
||||
* Update : 2023.07.26
|
||||
* Copyright(c) 2023 OLYM Communications. All Rights Reserved.
|
||||
*/
|
||||
html,
|
||||
body {
|
||||
background: #fff;
|
||||
}
|
||||
.fx {
|
||||
display: flex;
|
||||
-ms-display: flex;
|
||||
-webkit-display: flex;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.login {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.login .left_box {
|
||||
width: 100%;
|
||||
background: url(/images/bg_login.jpg) no-repeat center/cover;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 7% 5% 5% 5%;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 53px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .left_box .text h1 span {
|
||||
font-weight: 800;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 40px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
.login .login_box {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 4% 12% 4% 6%;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #2640a5;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st {
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box > ul > li .input_st .diveicon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 28px;
|
||||
transform: translateY(-50%);
|
||||
font-family: "Xeicon";
|
||||
font-size: 25px;
|
||||
color: #003288;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
width: 100%;
|
||||
padding: 20px 20px 20px 70px;
|
||||
background: #f7f7f7;
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
height: auto;
|
||||
}
|
||||
.login .login_box > ul > li input:focus {
|
||||
outline: 1px solid #3c5fe5;
|
||||
}
|
||||
.login .login_box > ul > li input::placeholder {
|
||||
color: #afabab;
|
||||
font-weight: 500;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 50px 0 45px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .checks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.login .login_box .bottom_box .checks label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #636363;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.login .login_box .bottom_box .pw_message i {
|
||||
color: #327af5;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
padding: 24px 15px;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
overflow: hidden;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-family: "Montserrat";
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 22px;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.login .login_box .login_btn:before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#1d3969 0%,
|
||||
#264078 10%,
|
||||
#172558 40%,
|
||||
#162254 50%,
|
||||
#111c43 60%,
|
||||
#111c42 100%
|
||||
);
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.login .login_box .login_btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.login .footer .copy {
|
||||
margin-top: 36px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 3%;
|
||||
}
|
||||
.login .footer .copy p {
|
||||
font-size: 13px;
|
||||
color: #d4d4d4;
|
||||
width: 44%;
|
||||
text-align: center;
|
||||
}
|
||||
.login .footer .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
content: "";
|
||||
width: 20%;
|
||||
height: 1px;
|
||||
background: #d4d4d4;
|
||||
}
|
||||
.login .footer .fot_btn {
|
||||
gap: 20px;
|
||||
margin: 30px 0 10px 0;
|
||||
}
|
||||
.login .footer .fot_btn a {
|
||||
color: #333;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.login .footer .fot_btn a:hover {
|
||||
color: #003288;
|
||||
}
|
||||
.login .footer .addr {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
gap: 3px 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* 비밀번호 변경 팝업창 */
|
||||
.login .modal .modal_container {
|
||||
width: 750px;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 10px 10px 30px 10px;
|
||||
color: #666;
|
||||
}
|
||||
.login .pw_desc p {
|
||||
font-size: 1.25rem;
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.login .pw_desc span {
|
||||
color: #f60;
|
||||
font-weight: 700;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 20px 50px;
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.login {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
.login .left_box {
|
||||
padding: 30px;
|
||||
flex-direction: column-reverse;
|
||||
gap: 20px;
|
||||
}
|
||||
.login .left_box .text h1 {
|
||||
font-size: 30px;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
.login .left_box .text p {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .left_box .img img {
|
||||
height: 40px;
|
||||
}
|
||||
.login .login_box {
|
||||
padding: 30px;
|
||||
}
|
||||
.login .login_box > ul {
|
||||
gap: 20px;
|
||||
}
|
||||
.login .login_box > ul > li h2 {
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
.login .login_box > ul > li .input_st::after {
|
||||
left: 15px;
|
||||
font-size: 20px;
|
||||
}
|
||||
.login .login_box > ul > li input {
|
||||
padding: 15px 10px 15px 50px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .bottom_box {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 30px 0;
|
||||
}
|
||||
.login .login_box .bottom_box .id_save label {
|
||||
font-size: 14px;
|
||||
}
|
||||
.login .login_box .login_btn {
|
||||
padding: 15px;
|
||||
}
|
||||
.login .login_box .login_btn span {
|
||||
font-size: 16px;
|
||||
}
|
||||
.login .login_box .login_btn i {
|
||||
font-size: 18px;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.login .login_box .copy p {
|
||||
width: 100%;
|
||||
}
|
||||
.login .login_box .copy::before,
|
||||
.login .login_box .copy::after {
|
||||
display: none;
|
||||
}
|
||||
.login .footer .addr {
|
||||
font-size: 11px;
|
||||
gap: 3px 10px;
|
||||
}
|
||||
.login .modal .modal_container {
|
||||
width: 90%;
|
||||
}
|
||||
.login .pw_desc {
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
.login .modal .form_write {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
/* // login */
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
interface PromptContext {
|
||||
|
||||
@@ -20,5 +20,4 @@ NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=G
|
||||
NEXT_PUBLIC_HASUDOINFO = https://www.hasudoinfo.or.kr
|
||||
NEXT_PUBLIC_HASUDOINFO_IN = http://10.101.163.102:8889
|
||||
NEXT_PUBLIC_FINDADRESS_URL=https://business.juso.go.kr/addrlink/addrLinkApi.do
|
||||
NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA=
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PRIVATE_WR_ADDRESS_KEY = U01TXOFVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA=
|
||||
@@ -21,7 +21,8 @@ import WrOverLay from "@/components/ui/map/wrOverLay/wrOverLay";
|
||||
|
||||
const Map = () => {
|
||||
|
||||
const { code, comMap, rainMapDetile, rswtMapDetile, getRuseMaplListReturn, pbnMapDetaile, ldrmntMapDetile, htdfMapDetile, coSttnMapDetail } = useWrStore((state) => state);
|
||||
const { code, comMap, rainMapDetile, rswtMapDetile, getRuseMaplListReturn
|
||||
, pbnMapDetaile, ldrmntMapDetile, htdfMapDetile, coSttnMapDetail } = useWrStore((state) => state);
|
||||
const { isFeatured } = comMap;
|
||||
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
/**
|
||||
* 관망도 API
|
||||
*/
|
||||
import { SITE_ID } from "@/src/shared/config";
|
||||
|
||||
const API_URL = `/sewerage-service/api/v1/map`
|
||||
//const API_URL = "http://con.mind-one.co.kr:11083/sewerage-service/api/v1/map";
|
||||
|
||||
@@ -35,6 +37,7 @@ export const mapService = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Site-Id': SITE_ID,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 관망도 API
|
||||
*/
|
||||
import { DEV_URL_WR, DEV_URL_SN } from "@/components/lib/config";
|
||||
import { SITE_ID } from "@/src/shared/config";
|
||||
|
||||
export type LyrStngProps = {
|
||||
lyr_sn: string
|
||||
@@ -34,6 +35,7 @@ export const mapService = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Site-Id': SITE_ID,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -48,6 +50,7 @@ export const mapService = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Site-Id': SITE_ID,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* JWT를 decoding하면 만료시간을 읽을 수 있다.
|
||||
* JWT는 3부분으로 구성 : Header.Payload.Signature
|
||||
* Payload 부분에 만료시간(exp)이 있다.
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { CLAIM_NAME } from "@/src/shared/config";
|
||||
import { getCookie } from "@/src/shared/lib";
|
||||
|
||||
interface JwtPayload {
|
||||
sub?: string; // 토큰 subject. 일반적으로 사용자 식별자나 주체 정보
|
||||
exp?: number; // 만료 시각(Unix timestamp, 초 단위)
|
||||
iat?: number; // 토큰 발급 시각(Unix timestamp, 초 단위)
|
||||
[key: string]: unknown; // 그 외 JWT payload에 포함될 수 있는 추가 클레임
|
||||
}
|
||||
|
||||
interface JwtState {
|
||||
token: string; // 쿠키에서 읽어온 JWT 원본 문자열
|
||||
payload: JwtPayload | null; // JWT를 파싱한 payload 객체
|
||||
exp: number | null; // payload에서 추출한 exp 값
|
||||
expDateLocal: string | null; // exp를 로컬 시간 문자열로 변환한 값
|
||||
remainingMs: number | null; // 현재 시각 기준 남은 시간(ms)
|
||||
error: string | null; // 쿠키 없음, 파싱 실패 등 오류 메시지
|
||||
}
|
||||
|
||||
|
||||
function parseJwt(token: string): JwtPayload {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("유효한 JWT 형식이 아닙니다.");
|
||||
}
|
||||
|
||||
const base64Url = parts[1] ?? ""; // Payload를 가져온다.
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(padded)
|
||||
.split("")
|
||||
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`)
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload) as JwtPayload;
|
||||
}
|
||||
|
||||
function formatRemainingMs(remainingMs: number | null): string {
|
||||
if (remainingMs === null) return "-";
|
||||
if (remainingMs <= 0) return "만료됨";
|
||||
|
||||
const totalSeconds = Math.floor(remainingMs / 1000);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
return `${hours}시간 ${minutes}분 ${seconds}초`;
|
||||
}
|
||||
|
||||
function readJwtState(): JwtState {
|
||||
const token = getCookie(CLAIM_NAME) ?? "";
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
token: "",
|
||||
payload: null,
|
||||
exp: null,
|
||||
expDateLocal: null,
|
||||
remainingMs: null,
|
||||
error: `${CLAIM_NAME} 쿠키가 없습니다.`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = parseJwt(token);
|
||||
|
||||
if (typeof payload.exp !== "number") {
|
||||
return {
|
||||
token,
|
||||
payload,
|
||||
exp: null,
|
||||
expDateLocal: null,
|
||||
remainingMs: null,
|
||||
error: "JWT payload에 exp 값이 없습니다.",
|
||||
};
|
||||
}
|
||||
|
||||
const expMs = payload.exp * 1000;
|
||||
|
||||
return {
|
||||
token,
|
||||
payload,
|
||||
exp: payload.exp,
|
||||
expDateLocal: new Date(expMs).toLocaleString(),
|
||||
remainingMs: Math.max(expMs - Date.now(), 0),
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
token,
|
||||
payload: null,
|
||||
exp: null,
|
||||
expDateLocal: null,
|
||||
remainingMs: null,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "JWT 파싱 중 오류가 발생했습니다.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const [jwtState, setJwtState] = useState<JwtState>(() => readJwtState());
|
||||
|
||||
useEffect(() => {
|
||||
const syncJwtState = () => {
|
||||
setJwtState(readJwtState());
|
||||
};
|
||||
|
||||
syncJwtState();
|
||||
const timer = window.setInterval(syncJwtState, 1000);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-8 text-base">
|
||||
<h1 className="text-2xl font-bold">JWT Exp Test</h1>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">쿠키명</div>
|
||||
<div>{CLAIM_NAME}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">exp</div>
|
||||
<div>{jwtState.exp ?? "-"}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">만료시각</div>
|
||||
<div>{jwtState.expDateLocal ?? "-"}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">남은시간</div>
|
||||
<div>{formatRemainingMs(jwtState.remainingMs)}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">상태</div>
|
||||
<div>{jwtState.error ?? "정상"}</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border p-4">
|
||||
<div className="font-semibold">payload</div>
|
||||
<pre className="overflow-auto whitespace-pre-wrap break-all text-sm">
|
||||
{jwtState.payload ? JSON.stringify(jwtState.payload, null, 2) : "-"}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -287,7 +287,7 @@ export default function PublicReuseFaciltyInsert() {
|
||||
}, [pbnstRes, append, selectRow]);
|
||||
|
||||
useEffect(() => {
|
||||
pubHasuInit(String(pageNation), laeSeCd);
|
||||
pubHasuInit(String(pageNation), laeSeCd); // 해당 지자체의 하수처리시설 조회
|
||||
isResetPage(false);
|
||||
setSelectRow(undefined);
|
||||
setSelectedRowIndex(-1);
|
||||
|
||||
@@ -37,17 +37,20 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useAlertStore } from "@/src/shared/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }];
|
||||
|
||||
export default function HtdflSttn() {
|
||||
const { auth, code, htdflSttn, htdflSttnSearch, initHtdfl } = useWrStore((state) => state);
|
||||
const { auth, code, htdflSttn, htdflSttnSearch, initHtdfl, getLaeSeCdCheck } = useWrStore((state) => state);
|
||||
const { laeSeCd } = auth; // 지자체구분코드
|
||||
const { htdflSttnRes, htdflSttnResCount } = htdflSttn;
|
||||
const isDownloadTriggered = useRef(false);
|
||||
const skipNextSearchRef = useRef(true);
|
||||
const { t } = useTranslation('base');
|
||||
const [pageNation, setPageNation] = useState(1);
|
||||
const [isChHead, setIsChHead] = useState(false);
|
||||
const [resetPage, isResetPage] = useState(false);
|
||||
const [bdstTable, setBdstTable] = useState<customRainBdst[]>();
|
||||
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>()
|
||||
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(defaultSigunOptions)
|
||||
const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" });
|
||||
const [detailModal, setDetailModal] = useState<{
|
||||
isOpen: boolean;
|
||||
@@ -59,22 +62,23 @@ export default function HtdflSttn() {
|
||||
// }>({ isOpen: false, chBuilding: [] });
|
||||
const [fcltNm, setFcltNm] = useState<string>("");
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
console.log("laeSeCd : ", laeSeCd);
|
||||
useEffect(() => {
|
||||
htdflSttnSearch(false, pageNation, htdflSttnSize, String(getConValue()), fcltNm, code.cityCd, code.sggCd);
|
||||
isResetPage(false)
|
||||
}, [pageNation]);
|
||||
const isAllAreaUser = laeSeCd === "0";
|
||||
const isLaeSeCdReady =
|
||||
laeSeCd === "0" || laeSeCd.length === 2 || laeSeCd.length === 5;
|
||||
const fixedSidoValue =
|
||||
!isAllAreaUser && laeSeCd?.length ? laeSeCd.slice(0, 2) : "";
|
||||
const fixedSigunValue =
|
||||
!isAllAreaUser && laeSeCd?.length === 5 ? laeSeCd : "0";
|
||||
|
||||
const getSelCity = (val: string | number) => {
|
||||
const cityValue = String(val);
|
||||
|
||||
if (cityValue === "0") {
|
||||
setSelectSgg([]);
|
||||
setSelectSgg(defaultSigunOptions);
|
||||
setObjSearch({ city: cityValue, sgg: "" });
|
||||
} else {
|
||||
const data = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(data);
|
||||
setSelectSgg(data.length > 0 ? data : defaultSigunOptions);
|
||||
|
||||
const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? "");
|
||||
setObjSearch({ city: cityValue, sgg: firstSggVal });
|
||||
@@ -85,16 +89,24 @@ export default function HtdflSttn() {
|
||||
setObjSearch({ ...objSearch, sgg: String(val) })
|
||||
};
|
||||
|
||||
const getConValue = () => {
|
||||
const getConValue = (): string => {
|
||||
if (!objSearch.city || objSearch.city === "0") {
|
||||
return "0";
|
||||
}
|
||||
if (objSearch.sgg && objSearch.sgg !== "0") {
|
||||
return objSearch.sgg;
|
||||
return String(objSearch.sgg);
|
||||
}
|
||||
return objSearch.city;
|
||||
return String(objSearch.city);
|
||||
};
|
||||
|
||||
const getFixedConValue = useCallback((): string => {
|
||||
if (laeSeCd === undefined || laeSeCd === "0") {
|
||||
return getConValue();
|
||||
}
|
||||
|
||||
return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2);
|
||||
}, [laeSeCd, objSearch]);
|
||||
|
||||
const cellClick = (index: number | undefined, headId: string) => {
|
||||
if (
|
||||
headId !== "chBuildingYn" &&
|
||||
@@ -106,9 +118,37 @@ export default function HtdflSttn() {
|
||||
};
|
||||
|
||||
const excelClick = useCallback(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDownloadTriggered.current = true;
|
||||
htdflSttnSearch(true, 1, htdflSttnResCount, String(getConValue()), fcltNm, code.cityCd, code.sggCd);
|
||||
}, [htdflSttnSearch, objSearch, code.cityCd, code.sggCd, fcltNm, htdflSttnResCount]);
|
||||
htdflSttnSearch(true, 1, htdflSttnResCount, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd);
|
||||
}, [htdflSttnSearch, fcltNm, htdflSttnResCount, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
getLaeSeCdCheck();
|
||||
}, [getLaeSeCdCheck]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (laeSeCd === "0") {
|
||||
setSelectSgg(defaultSigunOptions);
|
||||
setObjSearch({ city: "0", sgg: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
const cityValue = laeSeCd.slice(0, 2);
|
||||
const sigunOptions = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(sigunOptions.length > 0 ? sigunOptions : defaultSigunOptions);
|
||||
setObjSearch({
|
||||
city: cityValue,
|
||||
sgg: laeSeCd.length === 5 ? laeSeCd : "0",
|
||||
});
|
||||
}, [laeSeCd, code.sggCd, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDownloadTriggered.current) {
|
||||
@@ -135,10 +175,27 @@ export default function HtdflSttn() {
|
||||
}, [htdflSttn.excelList, code]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!htdflSttn.isInit) {
|
||||
initHtdfl(false, 1, htdflSttnSize, "0", fcltNm, code.cityCd, code.sggCd);
|
||||
if (!isLaeSeCdReady || htdflSttn.isInit) {
|
||||
return;
|
||||
}
|
||||
}, [htdflSttn.isInit, initHtdfl]);
|
||||
|
||||
skipNextSearchRef.current = true;
|
||||
initHtdfl(false, 1, htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd);
|
||||
}, [htdflSttn.isInit, initHtdfl, fcltNm, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipNextSearchRef.current) {
|
||||
skipNextSearchRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
htdflSttnSearch(false, pageNation, htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd);
|
||||
isResetPage(false)
|
||||
}, [pageNation, htdflSttnSearch, fcltNm, code.cityCd, code.sggCd, getFixedConValue, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
const data = tableData(htdflSttnRes, { code });
|
||||
@@ -171,18 +228,27 @@ export default function HtdflSttn() {
|
||||
className="col-span-2 w-1/6 h-full text-2xl"
|
||||
label="시도"
|
||||
selectClass={""}
|
||||
selectArray={code.cityCd}
|
||||
selectArray={
|
||||
!isAllAreaUser && fixedSidoValue
|
||||
? code.cityCd.filter((item) => item.val === fixedSidoValue)
|
||||
: code.cityCd
|
||||
}
|
||||
value={objSearch.city || "0"}
|
||||
onSelectValue={getSelCity}
|
||||
/>
|
||||
<SelectBox
|
||||
className="col-span-2 w-1/6 h-full text-2xl ml-4"
|
||||
label="시군구"
|
||||
selectClass={""}
|
||||
selectArray={selectSgg || []}
|
||||
value={objSearch.sgg} // value props 추가
|
||||
selectArray={
|
||||
!isAllAreaUser && laeSeCd?.length === 5
|
||||
? (selectSgg || defaultSigunOptions).filter((item) => item.val === "0" || item.val === fixedSigunValue)
|
||||
: selectSgg || defaultSigunOptions
|
||||
}
|
||||
value={objSearch.sgg || "0"}
|
||||
onSelectValue={getSgg}
|
||||
/>
|
||||
<div className="col-span-2 flex flex-row space-x-2 ml-3 mt-2">
|
||||
<div className="col-span-2 flex flex-row space-x-2 ml-3">
|
||||
<Label className="w-28 mt-2 text-xl">
|
||||
{"시설명"}
|
||||
</Label>
|
||||
@@ -201,7 +267,10 @@ export default function HtdflSttn() {
|
||||
<Button
|
||||
className={btnClass}
|
||||
onClick={() => {
|
||||
htdflSttnSearch(false, 1,htdflSttnSize, String(getConValue()), fcltNm, code.cityCd, code.sggCd);
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
htdflSttnSearch(false, 1,htdflSttnSize, String(getFixedConValue()), fcltNm, code.cityCd, code.sggCd);
|
||||
isResetPage(true)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -36,16 +36,19 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function PbnstSttn() {
|
||||
const {
|
||||
auth,
|
||||
code,
|
||||
pbnstSttn,
|
||||
pbnstSttnSearch,
|
||||
pbnSttnInit,
|
||||
getPbnStnDetail,
|
||||
getFlcts,
|
||||
getFlcts, // 하수재이용시설 조회
|
||||
resetPbnstDetail,
|
||||
getLaeSeCdCheck,
|
||||
} = useWrStore((state) => state);
|
||||
|
||||
const { pbnstSttnRes, fclts, pbnstSttnResCount } = pbnstSttn;
|
||||
const { laeSeCd } = auth;
|
||||
|
||||
const [isReset, setIsReset] = useState(false);
|
||||
const ref = useRef<ReturnType>({} as ReturnType);
|
||||
@@ -59,21 +62,53 @@ export default function PbnstSttn() {
|
||||
data: customPbnst;
|
||||
}>({ isModal: false, data: {} as customPbnst });
|
||||
const isDownloadTriggered = useRef(false);
|
||||
const useInitRef = useRef(false);
|
||||
const [pbnRuseNm, setPbnRuseNm] = useState<string>("");
|
||||
const { t } = useTranslation("base");
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const isAllAreaUser = laeSeCd === "0";
|
||||
const fixedSidoValue =
|
||||
!isAllAreaUser && laeSeCd?.length ? laeSeCd.slice(0, 2) : "";
|
||||
const fixedSigunValue =
|
||||
!isAllAreaUser && laeSeCd?.length === 5 ? laeSeCd : "0";
|
||||
const getFixedSearchValue = useCallback(
|
||||
(baseValue: ReturnType): ReturnType => {
|
||||
if (isAllAreaUser) {
|
||||
return baseValue;
|
||||
}
|
||||
|
||||
const fixedSidoText =
|
||||
code.cityCd.find((item) => item.val === fixedSidoValue)?.text ?? "";
|
||||
const fixedSigunText =
|
||||
fixedSigunValue !== "0"
|
||||
? code.sggCd.find((item) => item.val === fixedSigunValue)?.text ?? ""
|
||||
: "";
|
||||
|
||||
return {
|
||||
...baseValue,
|
||||
partValue: "A",
|
||||
sidoValue: fixedSidoValue,
|
||||
sigunValue: fixedSigunValue,
|
||||
sidoText: fixedSidoText,
|
||||
sigunText: fixedSigunText,
|
||||
};
|
||||
},
|
||||
[isAllAreaUser, fixedSidoValue, fixedSigunValue, code.cityCd, code.sggCd]
|
||||
);
|
||||
|
||||
const excelClick = useCallback(() => {
|
||||
isDownloadTriggered.current = true;
|
||||
const fixedSearchValue = getFixedSearchValue(ref.current);
|
||||
pbnstSttnSearch(
|
||||
true,
|
||||
String(1),
|
||||
"100000",
|
||||
ref.current,
|
||||
fixedSearchValue,
|
||||
pbnRuseNm,
|
||||
searchYear,
|
||||
fclts
|
||||
);
|
||||
}, [pbnstSttnSearch, fclts, pbnRuseNm]);
|
||||
}, [pbnstSttnSearch, fclts, pbnRuseNm, searchYear, getFixedSearchValue]);
|
||||
|
||||
const cellCLick = useCallback(
|
||||
(index: number | undefined) => {
|
||||
@@ -84,14 +119,23 @@ export default function PbnstSttn() {
|
||||
},
|
||||
[getPbnStnDetail, pbnstSttnRes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getLaeSeCdCheck();
|
||||
useInitRef.current = true;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pbnstSttn.isInit) {
|
||||
getFlcts();
|
||||
getFlcts(); // 하수재이용시설 조회
|
||||
}
|
||||
}, [pbnstSttn.isInit]);
|
||||
|
||||
useEffect(() => {
|
||||
pbnSttnInit("1", "15", ref.current, searchYear, fclts);
|
||||
}, [fclts]);
|
||||
if (useInitRef.current === true && laeSeCd !== undefined && fclts.length > 0) {
|
||||
pbnSttnInit("1", "15", getFixedSearchValue(ref.current), searchYear, fclts);
|
||||
}
|
||||
}, [fclts, laeSeCd, searchYear, pbnSttnInit, getFixedSearchValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pbnstSttnRes && pbnstSttnRes.length > 0) {
|
||||
@@ -139,8 +183,18 @@ export default function PbnstSttn() {
|
||||
<div className="grid grid-cols-2 gap-4 ">
|
||||
<div className="col-span-2">
|
||||
<SearchBaseForm
|
||||
selectSidoData={code.cityCd}
|
||||
selectSigunData={code.sggCd}
|
||||
selectSidoData={
|
||||
!isAllAreaUser && fixedSidoValue
|
||||
? code.cityCd.filter((item) => item.val === fixedSidoValue)
|
||||
: code.cityCd
|
||||
}
|
||||
selectSigunData={
|
||||
!isAllAreaUser && laeSeCd?.length === 5
|
||||
? code.sggCd.filter((item) => item.val === laeSeCd)
|
||||
: !isAllAreaUser && laeSeCd?.length === 2
|
||||
? code.sggCd.filter((item) => item.val.slice(0, 2) === laeSeCd)
|
||||
: code.sggCd
|
||||
}
|
||||
selectRegionData={code.rvbsnCd}
|
||||
selectUnitWaterShedData={code.lgbsnCd}
|
||||
selectMiddleRegionData={code.mdmbsnCd}
|
||||
@@ -171,7 +225,7 @@ export default function PbnstSttn() {
|
||||
setSearchYear(String(val))
|
||||
}
|
||||
/>
|
||||
<Label className="w-full mt-2 ml-4 text-xl text-gray-600">
|
||||
<Label className="w-full ml-4 text-xl text-gray-600">
|
||||
{"하수처리시설명"}
|
||||
</Label>
|
||||
<SearchInput
|
||||
@@ -185,9 +239,16 @@ export default function PbnstSttn() {
|
||||
</div>
|
||||
}
|
||||
queryClick={() => {
|
||||
// pbnSearch(returnType);
|
||||
|
||||
//setTableIndex(0)
|
||||
const fixedSearchValue = getFixedSearchValue(ref.current);
|
||||
pbnstSttnSearch(
|
||||
false,
|
||||
"1",
|
||||
String(prfRegPageSize),
|
||||
fixedSearchValue,
|
||||
pbnRuseNm,
|
||||
searchYear,
|
||||
fclts
|
||||
);
|
||||
setIsReset(true);
|
||||
}}
|
||||
excelClick={excelClick}
|
||||
@@ -196,7 +257,9 @@ export default function PbnstSttn() {
|
||||
}}
|
||||
btnClass={btnClass}
|
||||
ref={ref}
|
||||
isAdmin={false}
|
||||
isAdmin={!isAllAreaUser}
|
||||
initSidoValue={fixedSidoValue}
|
||||
initSigunValue={fixedSigunValue !== "0" ? fixedSigunValue : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,11 +278,12 @@ export default function PbnstSttn() {
|
||||
cellCLick(index);
|
||||
}}
|
||||
onPageChange={(page: number) => {
|
||||
const fixedSearchValue = getFixedSearchValue(ref.current);
|
||||
pbnstSttnSearch(
|
||||
false,
|
||||
String(page),
|
||||
String(prfRegPageSize),
|
||||
ref.current,
|
||||
fixedSearchValue,
|
||||
pbnRuseNm,
|
||||
searchYear,
|
||||
fclts
|
||||
|
||||
@@ -35,18 +35,21 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useAlertStore } from "@/src/shared/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }];
|
||||
|
||||
export default function RainSttn() {
|
||||
const { code, rainSttn, rainSttnSearch, rainSttnInit } = useWrStore(
|
||||
const { auth, code, rainSttn, rainSttnSearch, rainSttnInit, getLaeSeCdCheck } = useWrStore(
|
||||
(state) => state
|
||||
);
|
||||
const { rainSttnRes, rainSttnResCount } = rainSttn;
|
||||
const { laeSeCd } = auth;
|
||||
|
||||
const [pageNation, setPageNation] = useState(1);
|
||||
const [isChHead, setIsChHead] = useState(false);
|
||||
const [bdstTable, setBdstTable] = useState<customRainBdst[]>();
|
||||
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>();
|
||||
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(defaultSigunOptions);
|
||||
const [resetPage, isResetPage] = useState(false);
|
||||
const isDownloadTriggered = useRef(false);
|
||||
const [isDownloadTriggered, setIsDownloadTriggered] = useState(false);
|
||||
const [objSearch, setObjSearch] = useState<objSearchType>({
|
||||
sgg: "",
|
||||
city: "",
|
||||
@@ -59,6 +62,14 @@ export default function RainSttn() {
|
||||
const [fcltNm, setFcltNm] = useState<string>("");
|
||||
const { t } = useTranslation("base");
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const skipNextSearchRef = useRef(true);
|
||||
const isAllAreaUser = laeSeCd === "0";
|
||||
const isLaeSeCdReady =
|
||||
laeSeCd === "0" || laeSeCd.length === 2 || laeSeCd.length === 5;
|
||||
const fixedSidoValue =
|
||||
!isAllAreaUser && laeSeCd?.length ? laeSeCd.slice(0, 2) : "";
|
||||
const fixedSigunValue =
|
||||
!isAllAreaUser && laeSeCd?.length === 5 ? laeSeCd : "0";
|
||||
|
||||
const cellClick = (index: number | undefined) => {
|
||||
if (index !== undefined && bdstTable !== undefined) {
|
||||
@@ -69,11 +80,11 @@ export default function RainSttn() {
|
||||
const getSelCity = (val: string | number) => {
|
||||
const cityValue = String(val);
|
||||
if (cityValue === "0") {
|
||||
setSelectSgg([]);
|
||||
setSelectSgg(defaultSigunOptions);
|
||||
setObjSearch({ city: cityValue, sgg: "" });
|
||||
} else {
|
||||
const data = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(data);
|
||||
setSelectSgg(data.length > 0 ? data : defaultSigunOptions);
|
||||
const firstSggVal =
|
||||
data.find((item) => item.val === "0")?.val || (data[0]?.val ?? "");
|
||||
setObjSearch({ city: cityValue, sgg: firstSggVal });
|
||||
@@ -84,21 +95,135 @@ export default function RainSttn() {
|
||||
setObjSearch({ ...objSearch, sgg: String(val) });
|
||||
};
|
||||
|
||||
const getConValue = () => {
|
||||
const getConValue = (): string => {
|
||||
if (!objSearch.city || objSearch.city === "0") {
|
||||
return "0";
|
||||
}
|
||||
if (objSearch.sgg && objSearch.sgg !== "0") {
|
||||
return objSearch.sgg;
|
||||
return String(objSearch.sgg);
|
||||
}
|
||||
return objSearch.city;
|
||||
return String(objSearch.city);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!rainSttn.isInit) {
|
||||
rainSttnInit(false, 1, rainSttnSize, "0", fcltNm, code as unknown as Record<string, SelectDataType[]>);
|
||||
const getFixedConValue = useCallback((): string => {
|
||||
if (laeSeCd === undefined || laeSeCd === "0") {
|
||||
return getConValue();
|
||||
}
|
||||
}, [rainSttn.isInit, rainSttnInit]);
|
||||
|
||||
return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2);
|
||||
}, [laeSeCd, objSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
getLaeSeCdCheck();
|
||||
}, [getLaeSeCdCheck]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (laeSeCd === "0") {
|
||||
setSelectSgg(defaultSigunOptions);
|
||||
setObjSearch({ city: "0", sgg: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
const cityValue = laeSeCd.slice(0, 2);
|
||||
const sigunOptions = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(sigunOptions.length > 0 ? sigunOptions : defaultSigunOptions);
|
||||
setObjSearch({
|
||||
city: cityValue,
|
||||
sgg: laeSeCd.length === 5 ? laeSeCd : "0",
|
||||
});
|
||||
}, [laeSeCd, code.sggCd, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady || rainSttn.isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
skipNextSearchRef.current = true;
|
||||
rainSttnInit(
|
||||
false,
|
||||
1,
|
||||
rainSttnSize,
|
||||
getFixedConValue(),
|
||||
fcltNm,
|
||||
code as unknown as Record<string, SelectDataType[]>
|
||||
);
|
||||
}, [rainSttn.isInit, rainSttnInit, fcltNm, code, getFixedConValue, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipNextSearchRef.current) {
|
||||
skipNextSearchRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
rainSttnSearch(
|
||||
false,
|
||||
pageNation,
|
||||
rainSttnSize,
|
||||
String(getFixedConValue()),
|
||||
fcltNm,
|
||||
code as unknown as Record<string, SelectDataType[]>
|
||||
);
|
||||
isResetPage(false);
|
||||
}, [pageNation, rainSttnSearch, fcltNm, code, getFixedConValue, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDownloadTriggered) {
|
||||
return;
|
||||
}
|
||||
if (rainSttn.excelList.length > 0) {
|
||||
const sheetArray: SheetParameter[] = [{
|
||||
data: rainSttn.excelList,
|
||||
headerNum: 3,
|
||||
headerRange: "A1:W3",
|
||||
sheetName: 'sheet1',
|
||||
startCol: 'A',
|
||||
decimalArray0: [6,10,12,14,16] // 0부터 시작
|
||||
}];
|
||||
excelDownloadNumStyle(
|
||||
"빗물이용시설 현황",
|
||||
"sample_rainSttn.xlsx", // public 폴더에 있는 template 파일 경로
|
||||
sheetArray
|
||||
);
|
||||
} else {
|
||||
alert(t("excelDownErrMsg"));
|
||||
}
|
||||
setIsDownloadTriggered(false);
|
||||
}, [rainSttn.excelList, isDownloadTriggered, alert, t, isLaeSeCdReady]);
|
||||
|
||||
const excelClick = useCallback(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDownloadTriggered(true);
|
||||
rainSttnSearch(
|
||||
true,
|
||||
1,
|
||||
rainSttnResCount,
|
||||
String(getFixedConValue()),
|
||||
fcltNm,
|
||||
code as unknown as Record<string, SelectDataType[]>
|
||||
);
|
||||
}, [
|
||||
rainSttnSearch,
|
||||
fcltNm,
|
||||
rainSttnResCount,
|
||||
code,
|
||||
getFixedConValue,
|
||||
isLaeSeCdReady,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const data = tableData(rainSttnRes, { code });
|
||||
@@ -143,60 +268,6 @@ export default function RainSttn() {
|
||||
setBdstTable(chData);
|
||||
}, [code, rainSttnRes]);
|
||||
|
||||
useEffect(() => {
|
||||
rainSttnSearch(
|
||||
false,
|
||||
pageNation,
|
||||
rainSttnSize,
|
||||
String(getConValue()),
|
||||
fcltNm,
|
||||
code as unknown as Record<string, SelectDataType[]>
|
||||
);
|
||||
isResetPage(false);
|
||||
}, [pageNation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDownloadTriggered.current) {
|
||||
return;
|
||||
}
|
||||
if (rainSttn.excelList.length > 0) {
|
||||
const sheetArray: SheetParameter[] = [{
|
||||
data: rainSttn.excelList,
|
||||
headerNum: 3,
|
||||
headerRange: "A1:W3",
|
||||
sheetName: 'sheet1',
|
||||
startCol: 'A',
|
||||
decimalArray0: [6,10,12,14,16] // 0부터 시작
|
||||
}];
|
||||
excelDownloadNumStyle(
|
||||
"빗물이용시설 현황",
|
||||
"sample_rainSttn.xlsx", // public 폴더에 있는 template 파일 경로
|
||||
sheetArray
|
||||
);
|
||||
} else {
|
||||
alert(t("excelDownErrMsg"));
|
||||
}
|
||||
isDownloadTriggered.current = false;
|
||||
}, [rainSttn.excelList]);
|
||||
|
||||
const excelClick = useCallback(() => {
|
||||
isDownloadTriggered.current = true;
|
||||
rainSttnSearch(
|
||||
true,
|
||||
1,
|
||||
rainSttnResCount,
|
||||
String(getConValue()),
|
||||
fcltNm,
|
||||
code as unknown as Record<string, SelectDataType[]>
|
||||
);
|
||||
}, [
|
||||
rainSttnSearch,
|
||||
objSearch,
|
||||
code.cityCd,
|
||||
code.sggCd,
|
||||
fcltNm,
|
||||
rainSttnResCount,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -210,18 +281,27 @@ export default function RainSttn() {
|
||||
className="col-span-2 w-1/6 h-full text-2xl"
|
||||
label="시도"
|
||||
selectClass={""}
|
||||
selectArray={code.cityCd}
|
||||
selectArray={
|
||||
!isAllAreaUser && fixedSidoValue
|
||||
? code.cityCd.filter((item) => item.val === fixedSidoValue)
|
||||
: code.cityCd
|
||||
}
|
||||
value={objSearch.city || "0"}
|
||||
onSelectValue={getSelCity}
|
||||
/>
|
||||
<SelectBox
|
||||
className="col-span-2 w-1/6 h-full text-2xl ml-4"
|
||||
label="시군구"
|
||||
selectClass={""}
|
||||
selectArray={selectSgg || []}
|
||||
value={objSearch.sgg} // value props 추가
|
||||
selectArray={
|
||||
!isAllAreaUser && laeSeCd?.length === 5
|
||||
? (selectSgg || defaultSigunOptions).filter((item) => item.val === "0" || item.val === fixedSigunValue)
|
||||
: selectSgg || defaultSigunOptions
|
||||
}
|
||||
value={objSearch.sgg || "0"}
|
||||
onSelectValue={getSgg}
|
||||
/>
|
||||
<div className="col-span-2 flex flex-row space-x-2 ml-3 mt-2">
|
||||
<div className="col-span-2 flex flex-row space-x-2 ml-3">
|
||||
<Label className="w-28 mt-2 text-xl">
|
||||
{"시설명"}
|
||||
</Label>
|
||||
@@ -243,7 +323,7 @@ export default function RainSttn() {
|
||||
false,
|
||||
1,
|
||||
rainSttnSize,
|
||||
String(getConValue()),
|
||||
String(getFixedConValue()),
|
||||
fcltNm,
|
||||
code as unknown as Record<string, SelectDataType[]>
|
||||
);
|
||||
|
||||
@@ -35,24 +35,35 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useAlertStore } from "@/src/shared/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }];
|
||||
|
||||
export default function RswtSttn() {
|
||||
const { code, rswtSttn, rswtSttnSearch, rswtSttnInit } = useWrStore(
|
||||
const { auth, code, rswtSttn, rswtSttnSearch, rswtSttnInit, getLaeSeCdCheck } = useWrStore(
|
||||
(state) => state
|
||||
);
|
||||
|
||||
const [pageNation, setPageNation] = useState(1);
|
||||
|
||||
const { rswtSttnRes, rswtSttnResCount } = rswtSttn;
|
||||
const { laeSeCd } = auth;
|
||||
const [isChHead, setIsChHead] = useState(false);
|
||||
const [resetPage, isResetPage] = useState(false);
|
||||
const [bdstTable, setBdstTable] = useState<customRswtBdst[]>();
|
||||
const isDownloadTriggered = useRef(false);
|
||||
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>()
|
||||
const skipNextSearchRef = useRef(true);
|
||||
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(defaultSigunOptions)
|
||||
const [objSearch, setObjSearch] = useState<objSearchType>({ sgg: "", city: "" });
|
||||
const [detailModal, setDetailModal] = useState<{ isOpen: boolean, data: customRswtBdst | undefined }>({ data: undefined, isOpen: false })
|
||||
const [fcltNm, setFcltNm] = useState<string>("");
|
||||
const { t } = useTranslation('base');
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
const isAllAreaUser = laeSeCd === "0";
|
||||
const isLaeSeCdReady =
|
||||
laeSeCd === "0" || laeSeCd.length === 2 || laeSeCd.length === 5;
|
||||
const fixedSidoValue =
|
||||
!isAllAreaUser && laeSeCd?.length ? laeSeCd.slice(0, 2) : "";
|
||||
const fixedSigunValue =
|
||||
!isAllAreaUser && laeSeCd?.length === 5 ? laeSeCd : "0";
|
||||
const cellClick = (index: number | undefined) => {
|
||||
|
||||
if ( index !== undefined && bdstTable !== undefined) {
|
||||
@@ -64,11 +75,11 @@ export default function RswtSttn() {
|
||||
const cityValue = String(val);
|
||||
|
||||
if (cityValue === "0") {
|
||||
setSelectSgg([]);
|
||||
setSelectSgg(defaultSigunOptions);
|
||||
setObjSearch({ city: cityValue, sgg: "" });
|
||||
} else {
|
||||
const data = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(data);
|
||||
setSelectSgg(data.length > 0 ? data : defaultSigunOptions);
|
||||
|
||||
const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? "");
|
||||
setObjSearch({ city: cityValue, sgg: firstSggVal });
|
||||
@@ -79,21 +90,63 @@ export default function RswtSttn() {
|
||||
setObjSearch({ ...objSearch, sgg: String(val) })
|
||||
};
|
||||
|
||||
const getConValue = () => {
|
||||
const getConValue = (): string => {
|
||||
if (!objSearch.city || objSearch.city === "0") {
|
||||
return "0";
|
||||
}
|
||||
if (objSearch.sgg && objSearch.sgg !== "0") {
|
||||
return objSearch.sgg;
|
||||
return String(objSearch.sgg);
|
||||
}
|
||||
return objSearch.city;
|
||||
return String(objSearch.city);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!rswtSttn.isInit&&code) {
|
||||
rswtSttnInit(false, 1, rswtSttnSize, code.userSiSggCd, fcltNm, code as unknown as Record<string, SelectDataType[]>);
|
||||
const getFixedConValue = useCallback((): string => {
|
||||
if (laeSeCd === undefined || laeSeCd === "0") {
|
||||
return getConValue();
|
||||
}
|
||||
}, [rswtSttn.isInit, rswtSttnInit]);
|
||||
|
||||
return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2);
|
||||
}, [laeSeCd, objSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
getLaeSeCdCheck();
|
||||
}, [getLaeSeCdCheck]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (laeSeCd === "0") {
|
||||
setSelectSgg(defaultSigunOptions);
|
||||
setObjSearch({ city: "0", sgg: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
const cityValue = laeSeCd.slice(0, 2);
|
||||
const sigunOptions = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(sigunOptions.length > 0 ? sigunOptions : defaultSigunOptions);
|
||||
setObjSearch({
|
||||
city: cityValue,
|
||||
sgg: laeSeCd.length === 5 ? laeSeCd : "0",
|
||||
});
|
||||
}, [laeSeCd, code.sggCd, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady || rswtSttn.isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
skipNextSearchRef.current = true;
|
||||
rswtSttnInit(
|
||||
false,
|
||||
1,
|
||||
rswtSttnSize,
|
||||
getFixedConValue(),
|
||||
fcltNm,
|
||||
code as unknown as Record<string, SelectDataType[]>
|
||||
);
|
||||
}, [rswtSttn.isInit, rswtSttnInit, fcltNm, code, getFixedConValue, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
const data = tableData(rswtSttnRes, { code });
|
||||
@@ -150,14 +203,27 @@ export default function RswtSttn() {
|
||||
}, [rswtSttn.excelList]);
|
||||
|
||||
const excelClick = useCallback(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDownloadTriggered.current = true;
|
||||
rswtSttnSearch(true, 1, rswtSttnResCount, String(getConValue()), fcltNm, code as unknown as Record<string, SelectDataType[]>);
|
||||
}, [rswtSttnSearch, objSearch, code.cityCd, code.sggCd, fcltNm, rswtSttnResCount]);
|
||||
rswtSttnSearch(true, 1, rswtSttnResCount, String(getFixedConValue()), fcltNm, code as unknown as Record<string, SelectDataType[]>);
|
||||
}, [rswtSttnSearch, fcltNm, rswtSttnResCount, code, getFixedConValue, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
rswtSttnSearch(false, pageNation, rswtSttnSize, String(getConValue()), fcltNm, code as unknown as Record<string, SelectDataType[]>);
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipNextSearchRef.current) {
|
||||
skipNextSearchRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
rswtSttnSearch(false, pageNation, rswtSttnSize, String(getFixedConValue()), fcltNm, code as unknown as Record<string, SelectDataType[]>);
|
||||
isResetPage(false)
|
||||
}, [pageNation]);
|
||||
}, [pageNation, rswtSttnSearch, fcltNm, code, getFixedConValue, isLaeSeCdReady]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -171,7 +237,12 @@ export default function RswtSttn() {
|
||||
className="col-span-2 w-1/6 h-full text-2xl"
|
||||
label="시도"
|
||||
selectClass={""}
|
||||
selectArray={code.cityCd}
|
||||
selectArray={
|
||||
!isAllAreaUser && fixedSidoValue
|
||||
? code.cityCd.filter((item) => item.val === fixedSidoValue)
|
||||
: code.cityCd
|
||||
}
|
||||
value={objSearch.city || "0"}
|
||||
onSelectValue={
|
||||
getSelCity
|
||||
}
|
||||
@@ -180,13 +251,17 @@ export default function RswtSttn() {
|
||||
className="col-span-2 w-1/6 h-full text-2xl ml-4"
|
||||
label="시군구"
|
||||
selectClass={""}
|
||||
selectArray={selectSgg || []}
|
||||
value={objSearch.sgg} // value props 추가
|
||||
selectArray={
|
||||
!isAllAreaUser && laeSeCd?.length === 5
|
||||
? (selectSgg || defaultSigunOptions).filter((item) => item.val === "0" || item.val === fixedSigunValue)
|
||||
: selectSgg || defaultSigunOptions
|
||||
}
|
||||
value={objSearch.sgg || "0"}
|
||||
onSelectValue={
|
||||
getSgg
|
||||
}
|
||||
/>
|
||||
<div className="col-span-2 flex flex-row space-x-2 ml-3 mt-2">
|
||||
<div className="col-span-2 flex flex-row space-x-2 ml-3">
|
||||
<Label className="w-28 mt-2 text-xl">
|
||||
{"시설명"}
|
||||
</Label>
|
||||
@@ -205,7 +280,10 @@ export default function RswtSttn() {
|
||||
<Button
|
||||
className={btnClass}
|
||||
onClick={() => {
|
||||
rswtSttnSearch(false, 1, rswtSttnSize, String(getConValue()), fcltNm, code as unknown as Record<string, SelectDataType[]>);
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
rswtSttnSearch(false, 1, rswtSttnSize, String(getFixedConValue()), fcltNm, code as unknown as Record<string, SelectDataType[]>);
|
||||
isResetPage(true)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -65,7 +65,8 @@ export default function Page() {
|
||||
rswtChkDelete, // 중수도시설 지도점검 선택 건 삭제
|
||||
rswtrChkExcelDownload, // 중수도시설 지도점검 엑셀 양식/데이터 조회
|
||||
rswtChkHeadChange, // 중수도시설 테이블 헤더 선택 상태 변경
|
||||
getLaeSeCdCheck // 사용자 관할 기관 코드 조회
|
||||
getLaeSeCdCheck, // 사용자 관할 기관 코드 조회
|
||||
getCommonCodesByKeys, // 지도점검 화면에서 필요한 공통코드 보강 조회
|
||||
} = useWrStore((state) => {
|
||||
return state;
|
||||
});
|
||||
@@ -93,8 +94,15 @@ export default function Page() {
|
||||
|
||||
useEffect(() => {
|
||||
getLaeSeCdCheck();
|
||||
const hasRequiredCodes =
|
||||
code.rainRswtMng.length > 0 &&
|
||||
code.rainFcltFlowRate.length > 0 &&
|
||||
code.dmdUszCd.length > 0;
|
||||
if (!hasRequiredCodes) {
|
||||
void getCommonCodesByKeys(["WR014", "WR015", "WR016"]);
|
||||
}
|
||||
useInitRef.current = true; // getLaeSeCdCheck()를 호출한 뒤 true로 바뀜
|
||||
}, []);
|
||||
}, [getLaeSeCdCheck, getCommonCodesByKeys, code.rainRswtMng, code.rainFcltFlowRate, code.dmdUszCd]);
|
||||
|
||||
|
||||
|
||||
@@ -102,75 +110,46 @@ export default function Page() {
|
||||
if (searchYear)
|
||||
if (selFcltDiv === "RNB") { // 빗물
|
||||
if (fcltNm) {
|
||||
rainChkNmSearch(
|
||||
rainPageNation,
|
||||
rainPageSize,
|
||||
searchYear,
|
||||
laeSeCd,
|
||||
fcltNm
|
||||
);
|
||||
rainChkNmSearch( rainPageNation, rainPageSize, searchYear, laeSeCd, fcltNm );
|
||||
setRainPageNation(1);
|
||||
} else {
|
||||
rainChkInit(rainPageNation, rainPageSize, searchYear, laeSeCd);
|
||||
}
|
||||
} else { // 중수도
|
||||
if (fcltNm) {
|
||||
rswtChkNmSearch(
|
||||
rswtPageNation,
|
||||
rswtPageSize,
|
||||
searchYear,
|
||||
laeSeCd,
|
||||
fcltNm
|
||||
);
|
||||
rswtChkNmSearch( rswtPageNation, rswtPageSize, searchYear, laeSeCd, fcltNm );
|
||||
setRswtPageNation(1);
|
||||
} else {
|
||||
rswtChkgdnInit(rswtPageNation, rswtPageSize, searchYear, laeSeCd);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rainPageChange = (pageNo: number) => {
|
||||
if (pageNo > 0) {
|
||||
setRainPageNation(pageNo);
|
||||
if (fcltNm) {
|
||||
rainChkNmSearch(
|
||||
pageNo,
|
||||
rainPageSize,
|
||||
searchYear,
|
||||
laeSeCd,
|
||||
fcltNm
|
||||
);
|
||||
rainChkNmSearch( pageNo, rainPageSize, searchYear, laeSeCd, fcltNm );
|
||||
} else {
|
||||
rainChkInit(pageNo, rainPageSize, searchYear, laeSeCd);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rswtPageChange = (pageNo: number) => {
|
||||
if (pageNo > 0) {
|
||||
setRswtPageNation(pageNo);
|
||||
if (fcltNm) {
|
||||
rswtChkNmSearch(
|
||||
pageNo,
|
||||
rswtPageSize,
|
||||
searchYear,
|
||||
laeSeCd,
|
||||
fcltNm
|
||||
);
|
||||
rswtChkNmSearch( pageNo, rswtPageSize, searchYear, laeSeCd, fcltNm );
|
||||
} else {
|
||||
rswtChkgdnInit(pageNo, rswtPageSize, searchYear, laeSeCd);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (useInitRef.current == true && laeSeCd !== undefined&& !rainChkgdn.isInit) {
|
||||
rainChkInit(
|
||||
1,
|
||||
rainPageSize,
|
||||
new Date().getFullYear().toString(),
|
||||
laeSeCd
|
||||
);
|
||||
rainChkInit( 1, rainPageSize, new Date().getFullYear().toString(), laeSeCd );
|
||||
isRainSttn(false);
|
||||
setIsModal({ data: {}, open: false });
|
||||
setIsRegExcel(false);
|
||||
@@ -179,12 +158,7 @@ export default function Page() {
|
||||
|
||||
useEffect(() => {
|
||||
if (useInitRef.current == true && laeSeCd !== undefined&&!rswtChkgdn.isInit) {
|
||||
rswtChkgdnInit(
|
||||
1,
|
||||
rswtPageSize,
|
||||
new Date().getFullYear().toString(),
|
||||
laeSeCd
|
||||
);
|
||||
rswtChkgdnInit( 1, rswtPageSize, new Date().getFullYear().toString(), laeSeCd );
|
||||
setIsModal({ data: {}, open: false });
|
||||
setIsRegExcel(false);
|
||||
}
|
||||
@@ -192,38 +166,24 @@ export default function Page() {
|
||||
|
||||
useEffect(() => {
|
||||
if (rswtChkgdn.excelList[0] && rswtChkgdn.excelList[0]?.data.length > 1) {
|
||||
excelArrayDownload(
|
||||
"중수도시설 지도점검",
|
||||
"sample_rs_chk.xlsx",
|
||||
rswtChkgdn.excelList
|
||||
);
|
||||
excelArrayDownload( "중수도시설 지도점검", "sample_rs_chk.xlsx", rswtChkgdn.excelList );
|
||||
}
|
||||
}, [rswtChkgdn.excelList]);
|
||||
|
||||
useEffect(() => {
|
||||
excelDownload(
|
||||
rainChkgdn.excelList,
|
||||
"빗물이용시설 지도점검",
|
||||
"sample_rainy.xlsx",
|
||||
"A1:AN1",
|
||||
2
|
||||
);
|
||||
rainChkgdn.excelList, "빗물이용시설 지도점검", "sample_rainy.xlsx", "A1:AN1", 2 );
|
||||
}, [rainChkgdn.excelList]);
|
||||
|
||||
const excelClick = useCallback(() => {
|
||||
if (selFcltDiv === "RNB") {
|
||||
rainChkExcelDownload(code.cityCd, code.sggCd,laeSeCd);
|
||||
} else {
|
||||
rswtrChkExcelDownload(code.cityCd, code.sggCd,laeSeCd);
|
||||
}
|
||||
}, [
|
||||
rainChkExcelDownload,
|
||||
rswtrChkExcelDownload,
|
||||
code.cityCd,
|
||||
code.sggCd,
|
||||
selFcltDiv,
|
||||
laeSeCd
|
||||
]);
|
||||
if (selFcltDiv === "RNB") {
|
||||
rainChkExcelDownload(code.cityCd, code.sggCd,laeSeCd);
|
||||
} else {
|
||||
rswtrChkExcelDownload(code.cityCd, code.sggCd,laeSeCd);
|
||||
}
|
||||
},
|
||||
[ rainChkExcelDownload, rswtrChkExcelDownload, code.cityCd, code.sggCd, selFcltDiv, laeSeCd ]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(data: any) => {
|
||||
@@ -264,6 +224,7 @@ export default function Page() {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chkUpdateTable = async (selFcltDiv: string) => {
|
||||
if (selFcltDiv === "RNB") {
|
||||
if (updateRainDetail && updateRainDetail.length > 0) {
|
||||
@@ -417,6 +378,9 @@ export default function Page() {
|
||||
readonly={mdfcnAuthrtYn === 'N'}
|
||||
/>
|
||||
)}
|
||||
<span className="text-2xl font-bold text-gray-500">
|
||||
☞ 연도별 점검일 최종 데이터를 기준으로 통계 작성합니다.
|
||||
</span>
|
||||
</ListDiv>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useWrStore } from "@/app/store";
|
||||
import { btnClass } from "@/app/store/common/code";
|
||||
import { rainPageSize } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn";
|
||||
import { rswtPageSize } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn";
|
||||
import { excelDownloadNumStyle, yearOptions } from "@/components/lib/func/common";
|
||||
import { excelDownloadNumStyle, getCity, yearOptions } from "@/components/lib/func/common";
|
||||
import { type SheetParameter } from "@common/business_components/ui";
|
||||
import ChkgdModal from "@/components/ui/modal/chkgd/chkgdModal/chkgdModal";
|
||||
import RainChkgdTable from "@/components/ui/table/chkgdTable/rainChkgd/rainChkgdTable";
|
||||
@@ -17,16 +17,20 @@ import {
|
||||
SearchInput,
|
||||
SelectBox,
|
||||
UiModal,
|
||||
type SelectDataType,
|
||||
} from "@common/business_components/ui";
|
||||
import ListDiv from "@common/business_components/ui/list_div";
|
||||
import { Button, Label } from "@common/components/ui";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useAlertStore } from "@/src/shared/lib";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const defaultSigunOptions: SelectDataType[] = [{ text: "전체", val: "0" }];
|
||||
|
||||
export default function Page() {
|
||||
const {
|
||||
auth,
|
||||
code,
|
||||
rainChkSttn,
|
||||
rswtChkSttn,
|
||||
rainChkSttnInit,
|
||||
@@ -36,15 +40,28 @@ export default function Page() {
|
||||
isRainSttn,
|
||||
isRswtSttn,
|
||||
rswtChkPolInit,
|
||||
getLaeSeCdCheck,
|
||||
} = useWrStore((state) => {
|
||||
return state;
|
||||
});
|
||||
|
||||
const { laeSeCd } = auth;
|
||||
const isAllAreaUser = laeSeCd === "0";
|
||||
const isLaeSeCdReady =
|
||||
laeSeCd === "0" || laeSeCd.length === 2 || laeSeCd.length === 5;
|
||||
const fixedSidoValue =
|
||||
!isAllAreaUser && laeSeCd?.length ? laeSeCd.slice(0, 2) : "";
|
||||
const fixedSigunValue =
|
||||
!isAllAreaUser && laeSeCd?.length === 5 ? laeSeCd : "0";
|
||||
const [selFcltDiv, setSelFcltDiv] = useState("RNB");
|
||||
const [rainPageNation, setRainPageNation] = useState(1);
|
||||
const [rswtPageNation, setRswtPageNation] = useState(1);
|
||||
const [fcltNm, setFcltNm] = useState<string>("");
|
||||
const [selectSgg, setSelectSgg] = useState<SelectDataType[] | []>(defaultSigunOptions);
|
||||
const [objSearch, setObjSearch] = useState<{ city: string; sgg: string }>({
|
||||
city: "",
|
||||
sgg: "",
|
||||
});
|
||||
const [isModal, setIsModal] = useState({
|
||||
open: false,
|
||||
data: {},
|
||||
@@ -52,7 +69,51 @@ export default function Page() {
|
||||
const [searchYear, setSearchYear] = useState<string>(
|
||||
String(new Date().getFullYear())
|
||||
);
|
||||
const useInitRef = useRef(false);
|
||||
const skipNextSearchRef = useRef(true);
|
||||
|
||||
const getSelCity = (val: string | number) => {
|
||||
const cityValue = String(val);
|
||||
if (cityValue === "0") {
|
||||
setSelectSgg(defaultSigunOptions);
|
||||
setObjSearch({ city: cityValue, sgg: "" });
|
||||
} else {
|
||||
const data = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(data.length > 0 ? data : defaultSigunOptions);
|
||||
const firstSggVal =
|
||||
data.find((item) => item.val === "0")?.val || (data[0]?.val ?? "");
|
||||
setObjSearch({ city: cityValue, sgg: String(firstSggVal) });
|
||||
}
|
||||
};
|
||||
|
||||
const getSgg = (val: string | number) => {
|
||||
setObjSearch((prev) => ({ ...prev, sgg: String(val) }));
|
||||
};
|
||||
|
||||
const getConValue = (): string => {
|
||||
if (!objSearch.city || objSearch.city === "0") {
|
||||
return "0";
|
||||
}
|
||||
if (objSearch.sgg && objSearch.sgg !== "0") {
|
||||
return objSearch.sgg;
|
||||
}
|
||||
return objSearch.city;
|
||||
};
|
||||
|
||||
const getFixedConValue = useCallback((): string => {
|
||||
if (laeSeCd === undefined || laeSeCd === "0") {
|
||||
return getConValue();
|
||||
}
|
||||
|
||||
return laeSeCd.length === 5 ? laeSeCd : laeSeCd.slice(0, 2);
|
||||
}, [laeSeCd, objSearch]);
|
||||
|
||||
const cellClick = () => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conValue = getFixedConValue();
|
||||
if (searchYear) {
|
||||
if (selFcltDiv === "RNB") {
|
||||
if (fcltNm) {
|
||||
@@ -60,11 +121,11 @@ export default function Page() {
|
||||
rainPageNation,
|
||||
rainPageSize,
|
||||
searchYear,
|
||||
laeSeCd,
|
||||
conValue,
|
||||
fcltNm
|
||||
);
|
||||
} else {
|
||||
rainChkSttnInit(rainPageNation, rainPageSize, searchYear, laeSeCd);
|
||||
rainChkSttnInit(rainPageNation, rainPageSize, searchYear, conValue);
|
||||
}
|
||||
} else {
|
||||
if (fcltNm) {
|
||||
@@ -72,63 +133,108 @@ export default function Page() {
|
||||
rswtPageNation,
|
||||
rswtPageSize,
|
||||
searchYear,
|
||||
laeSeCd,
|
||||
conValue,
|
||||
fcltNm
|
||||
);
|
||||
} else {
|
||||
rswtChkSttnInit(rswtPageNation, rswtPageSize, searchYear, laeSeCd);
|
||||
rswtChkSttnInit(rswtPageNation, rswtPageSize, searchYear, conValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rainPageChange = (pageNo: number) => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conValue = getFixedConValue();
|
||||
if (pageNo > 0) {
|
||||
setRainPageNation(pageNo);
|
||||
if (fcltNm) {
|
||||
rainChkSttnNmSearch(pageNo, rainPageSize, searchYear, laeSeCd, fcltNm);
|
||||
rainChkSttnNmSearch(pageNo, rainPageSize, searchYear, conValue, fcltNm);
|
||||
} else {
|
||||
rainChkSttnInit(pageNo, rainPageSize, searchYear, laeSeCd);
|
||||
rainChkSttnInit(pageNo, rainPageSize, searchYear, conValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rswtPageChange = (pageNo: number) => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conValue = getFixedConValue();
|
||||
if (pageNo > 0) {
|
||||
setRswtPageNation(pageNo);
|
||||
if (fcltNm) {
|
||||
rswtChkNmSttnSearch(pageNo, rainPageSize, searchYear, laeSeCd, fcltNm);
|
||||
rswtChkNmSttnSearch(pageNo, rainPageSize, searchYear, conValue, fcltNm);
|
||||
} else {
|
||||
rswtChkSttnInit(pageNo, rswtPageSize, searchYear, laeSeCd);
|
||||
rswtChkSttnInit(pageNo, rswtPageSize, searchYear, conValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getLaeSeCdCheck();
|
||||
useInitRef.current = true;
|
||||
}, [getLaeSeCdCheck]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (laeSeCd === "0") {
|
||||
setSelectSgg(defaultSigunOptions);
|
||||
setObjSearch({ city: "0", sgg: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
const cityValue = laeSeCd.slice(0, 2);
|
||||
const sigunOptions = getCity(cityValue, code.sggCd);
|
||||
setSelectSgg(sigunOptions.length > 0 ? sigunOptions : defaultSigunOptions);
|
||||
setObjSearch({
|
||||
city: cityValue,
|
||||
sgg: laeSeCd.length === 5 ? laeSeCd : "0",
|
||||
});
|
||||
}, [laeSeCd, code.sggCd, isLaeSeCdReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useInitRef.current || !isLaeSeCdReady || rainChkSttn.isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
skipNextSearchRef.current = true;
|
||||
if (!rainChkSttn.isInit) {
|
||||
isRainSttn(true);
|
||||
rainChkSttnInit(
|
||||
1,
|
||||
rainPageSize,
|
||||
new Date().getFullYear().toString(),
|
||||
laeSeCd
|
||||
getFixedConValue()
|
||||
);
|
||||
setIsModal({ data: {}, open: false });
|
||||
}
|
||||
}, [rainChkSttn.isInit, rainChkSttnInit, rainPageSize]);
|
||||
}, [rainChkSttn.isInit, rainChkSttnInit, rainPageSize, isLaeSeCdReady, getFixedConValue, isRainSttn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useInitRef.current || !isLaeSeCdReady || rswtChkSttn.isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
skipNextSearchRef.current = true;
|
||||
if (!rswtChkSttn.isInit) {
|
||||
setIsModal({ data: {}, open: false });
|
||||
rswtChkSttnInit(
|
||||
1,
|
||||
rswtPageSize,
|
||||
new Date().getFullYear().toString(),
|
||||
laeSeCd
|
||||
getFixedConValue()
|
||||
);
|
||||
isRswtSttn(true);
|
||||
}
|
||||
}, [rswtChkSttn.isInit, rswtChkSttnInit, rswtPageSize]);
|
||||
}, [rswtChkSttn.isInit, rswtChkSttnInit, rswtPageSize, isLaeSeCdReady, getFixedConValue, isRswtSttn]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(data: any) => {
|
||||
@@ -191,6 +297,42 @@ export default function Page() {
|
||||
<div className="col-span-4">
|
||||
<SearchDiv>
|
||||
<SearchFormLeft>
|
||||
<div className="col-span-2 flex flex-row space-x-2">
|
||||
<Label className="w-28 mt-2 text-xl text-gray-600">
|
||||
{"시도"}
|
||||
</Label>
|
||||
<SelectBox
|
||||
className="w-full h-full text-2xl"
|
||||
label=""
|
||||
selectClass=""
|
||||
selectArray={
|
||||
!isAllAreaUser && fixedSidoValue
|
||||
? code.cityCd.filter((item) => item.val === fixedSidoValue)
|
||||
: code.cityCd
|
||||
}
|
||||
value={objSearch.city || "0"}
|
||||
onSelectValue={getSelCity}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-row space-x-2">
|
||||
<Label className="w-28 mt-2 text-xl text-gray-600">
|
||||
{"시군구"}
|
||||
</Label>
|
||||
<SelectBox
|
||||
className="w-full h-full text-2xl"
|
||||
label=""
|
||||
selectClass=""
|
||||
selectArray={
|
||||
!isAllAreaUser && laeSeCd?.length === 5
|
||||
? (selectSgg || defaultSigunOptions).filter(
|
||||
(item) => item.val === "0" || item.val === fixedSigunValue
|
||||
)
|
||||
: selectSgg || defaultSigunOptions
|
||||
}
|
||||
value={objSearch.sgg || "0"}
|
||||
onSelectValue={getSgg}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2 flex flex-row space-x-2">
|
||||
<Label className="w-48 mt-2 text-xl text-gray-600">
|
||||
{"지도점검년도"}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { headMakeprfmnc } from "../../prfmncReg/chkgdnReg/TableHead";
|
||||
|
||||
export default function PbnstPrfmncSttn() {
|
||||
const {
|
||||
auth,
|
||||
code,
|
||||
tableHeadMaker,
|
||||
pbnstPrfmncSttn,
|
||||
@@ -24,8 +25,10 @@ export default function PbnstPrfmncSttn() {
|
||||
pbnstPrfmncSearch,
|
||||
getFlctsSttn,
|
||||
prfmnc,
|
||||
pbnNmSearch
|
||||
pbnNmSearch,
|
||||
getLaeSeCdCheck
|
||||
} = useWrStore((state) => state);
|
||||
const { laeSeCd } = auth;
|
||||
const { monthHeadList } = prfmnc
|
||||
const { pbnstPrfHeadList, prfmncResCount, prfmncRes, rawData, selectYearList, fclts } = pbnstPrfmncSttn;
|
||||
// const [isChHead, setIsChHead] = useState(false);
|
||||
@@ -42,6 +45,39 @@ export default function PbnstPrfmncSttn() {
|
||||
const [excelData, setExcelData] = useState<any[]>([]);
|
||||
const [pageNation, setPageNation] = useState(1);
|
||||
const [resetPage, isResetPage] = useState(false);
|
||||
const skipNextSearchRef = useRef(true);
|
||||
const isAllAreaUser = laeSeCd === "0";
|
||||
const isLaeSeCdReady =
|
||||
laeSeCd === "0" || laeSeCd.length === 2 || laeSeCd.length === 5;
|
||||
const fixedSidoValue =
|
||||
!isAllAreaUser && laeSeCd?.length ? laeSeCd.slice(0, 2) : "";
|
||||
const fixedSigunValue =
|
||||
!isAllAreaUser && laeSeCd?.length === 5 ? laeSeCd : "0";
|
||||
|
||||
const getFixedSearchValue = useCallback(
|
||||
(baseValue: ReturnType): ReturnType => {
|
||||
if (isAllAreaUser) {
|
||||
return baseValue;
|
||||
}
|
||||
|
||||
const fixedSidoText =
|
||||
code.cityCd.find((item) => item.val === fixedSidoValue)?.text ?? "";
|
||||
const fixedSigunText =
|
||||
fixedSigunValue !== "0"
|
||||
? code.sggCd.find((item) => item.val === fixedSigunValue)?.text ?? ""
|
||||
: "";
|
||||
|
||||
return {
|
||||
...baseValue,
|
||||
partValue: "A",
|
||||
sidoValue: fixedSidoValue,
|
||||
sigunValue: fixedSigunValue,
|
||||
sidoText: fixedSidoText,
|
||||
sigunText: fixedSigunText,
|
||||
};
|
||||
},
|
||||
[isAllAreaUser, fixedSidoValue, fixedSigunValue, code.cityCd, code.sggCd]
|
||||
);
|
||||
|
||||
const excelClick = useCallback(() => {
|
||||
if (!excelData || excelData.length === 0) {
|
||||
@@ -489,7 +525,18 @@ export default function PbnstPrfmncSttn() {
|
||||
}, [outLineData]);
|
||||
|
||||
useEffect(() => {
|
||||
const returnType: ReturnType = {
|
||||
getLaeSeCdCheck();
|
||||
tableHeadMaker(Number(searchYear), Number(searchYear));
|
||||
getFlctsSttn();
|
||||
}, [getLaeSeCdCheck, tableHeadMaker, searchYear, getFlctsSttn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLaeSeCdReady || fclts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
skipNextSearchRef.current = true;
|
||||
const initialValue = getFixedSearchValue({
|
||||
sidoValue: "0",
|
||||
sigunValue: "0",
|
||||
partValue: "A",
|
||||
@@ -498,17 +545,31 @@ export default function PbnstPrfmncSttn() {
|
||||
middleRegionValue: "",
|
||||
envPartValue: "",
|
||||
sidoText: "",
|
||||
sigunText: ""
|
||||
}
|
||||
tableHeadMaker(Number(searchYear), Number(searchYear));
|
||||
getFlctsSttn();
|
||||
pbnstPrfmncSearch(1, returnType, "", searchYear, searchYear, fclts)
|
||||
}, []);
|
||||
sigunText: "",
|
||||
});
|
||||
pbnstPrfmncSearch(1, initialValue, "", searchYear, searchYear, fclts);
|
||||
}, [fclts, isLaeSeCdReady, getFixedSearchValue, pbnstPrfmncSearch, searchYear]);
|
||||
|
||||
useEffect(() => {
|
||||
pbnstPrfmncSearch(pageNation, ref.current, pbnRuseNm, fromDate, toDate, pbnstPrfmncSttn.fclts);
|
||||
if (!isLaeSeCdReady || fclts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipNextSearchRef.current) {
|
||||
skipNextSearchRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
pbnstPrfmncSearch(
|
||||
pageNation,
|
||||
getFixedSearchValue(ref.current),
|
||||
pbnRuseNm,
|
||||
fromDate,
|
||||
toDate,
|
||||
pbnstPrfmncSttn.fclts
|
||||
);
|
||||
isResetPage(false)
|
||||
}, [fclts, pageNation]);
|
||||
}, [fclts, pageNation, pbnRuseNm, fromDate, toDate, pbnstPrfmncSttn.fclts, isLaeSeCdReady, getFixedSearchValue, pbnstPrfmncSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const tableData = prfmncRes.map((e, idx) => {
|
||||
@@ -525,8 +586,8 @@ export default function PbnstPrfmncSttn() {
|
||||
|
||||
useEffect(() => {
|
||||
if (pbnRuseNm.length > 1)
|
||||
pbnNmSearch("1", "23", ref.current, pbnRuseNm, searchYear, []);
|
||||
}, [pbnRuseNm]);
|
||||
pbnNmSearch("1", "23", getFixedSearchValue(ref.current), pbnRuseNm, searchYear, []);
|
||||
}, [pbnRuseNm, getFixedSearchValue, pbnNmSearch, searchYear]);
|
||||
|
||||
const cellClick = (index: number | undefined, headId: string) => {
|
||||
if (index !== undefined && headId !== undefined) {
|
||||
@@ -539,8 +600,18 @@ export default function PbnstPrfmncSttn() {
|
||||
<div className="grid grid-cols-4 gap-4 ">
|
||||
<div className="col-span-4">
|
||||
<SearchBaseForm
|
||||
selectSidoData={code.cityCd}
|
||||
selectSigunData={code.sggCd}
|
||||
selectSidoData={
|
||||
!isAllAreaUser && fixedSidoValue
|
||||
? code.cityCd.filter((item) => item.val === fixedSidoValue)
|
||||
: code.cityCd
|
||||
}
|
||||
selectSigunData={
|
||||
!isAllAreaUser && laeSeCd?.length === 5
|
||||
? code.sggCd.filter((item) => item.val === laeSeCd)
|
||||
: !isAllAreaUser && laeSeCd?.length
|
||||
? code.sggCd.filter((item) => item.val.slice(0, 2) === fixedSidoValue)
|
||||
: code.sggCd
|
||||
}
|
||||
selectRegionData={code.rvbsnCd}
|
||||
selectUnitWaterShedData={code.lgbsnCd}
|
||||
selectMiddleRegionData={code.mdmbsnCd}
|
||||
@@ -574,7 +645,7 @@ export default function PbnstPrfmncSttn() {
|
||||
selectArray={yearOptions}
|
||||
value={toDate}
|
||||
onSelectValue={(val: string | number) => setToDate(String(val))} />
|
||||
<Label className="w-28 mt-2 text-xl">
|
||||
<Label className="w-28 text-xl">
|
||||
{"시설명"}
|
||||
</Label>
|
||||
<SearchInput
|
||||
@@ -594,7 +665,11 @@ export default function PbnstPrfmncSttn() {
|
||||
</div>
|
||||
</div>}
|
||||
queryClick={function (returnType: ReturnType): void {
|
||||
pbnstPrfmncSearch(1, returnType, pbnRuseNm ?? "", fromDate, toDate, fclts);
|
||||
if (!isLaeSeCdReady || fclts.length === 0) {
|
||||
return;
|
||||
}
|
||||
const fixedSearchValue = getFixedSearchValue(returnType);
|
||||
pbnstPrfmncSearch(1, fixedSearchValue, pbnRuseNm ?? "", fromDate, toDate, fclts);
|
||||
if (fromDate && toDate) {
|
||||
const fromNum = Number(fromDate);
|
||||
const toNum = Number(toDate);
|
||||
@@ -618,7 +693,11 @@ export default function PbnstPrfmncSttn() {
|
||||
setToDate(String(new Date().getFullYear()))
|
||||
}}
|
||||
btnClass={btnClass}
|
||||
ref={ref} isAdmin={false} />
|
||||
ref={ref}
|
||||
isAdmin={!isAllAreaUser}
|
||||
initSidoValue={fixedSidoValue}
|
||||
initSigunValue={fixedSigunValue !== "0" ? fixedSigunValue : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-6 gap-4 ">
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function RswtStat() {
|
||||
"개소": item.ctpvCnt,
|
||||
// "건축연면적(m³)": item.archArea,
|
||||
"처리용량(m³)": item.filtrTrtmfctCpct,
|
||||
"물사용량(m³/년)": item.wtrUsqty,
|
||||
// "물사용량(m³/년)": item.wtrUsqty,
|
||||
"중수도+대체수이용량(m³/년)": item.rswtAltSum,
|
||||
"중수도이용량(m³/년)": item.rswtrUsqty,
|
||||
"대체수이용량(m³/년)": item.altUswtrUtztnQnt,
|
||||
@@ -59,8 +59,8 @@ export default function RswtStat() {
|
||||
data: (data as unknown as JSON[]) ?? [],
|
||||
fileName: "중수도재이용통계",
|
||||
sheetNameArray: ["Sheet1"],
|
||||
headerRange: "A1:N1",
|
||||
decimalArray0: [1,2,3,4,5,6,7,8,9,10,11,12,13], // 0부터 시작
|
||||
headerRange: "A1:M1",
|
||||
decimalArray0: [1,2,3,4,5,6,7,8,9,10,11,12], // 0부터 시작
|
||||
})
|
||||
} else {
|
||||
alert(t("excelDownErrMsg"))
|
||||
@@ -174,15 +174,9 @@ export default function RswtStat() {
|
||||
} else if (i == 12) {
|
||||
const value = Number(item[12]) || 0;
|
||||
sidoData[i] = (Number(sidoData[i]) + value) as string | number;
|
||||
} else if (i == 13) {
|
||||
const value = Number(item[13]) || 0;
|
||||
sidoData[i] = (Number(sidoData[i]) + value) as string | number;
|
||||
} else if (i == 14) {
|
||||
const value = Number(item[14]) || 0;
|
||||
sidoData[i] = (Number(sidoData[i]) + value) as string | number;
|
||||
} else if (i == 16) {
|
||||
const value = Number(item[16]) || 0;
|
||||
sidoData[i] = (Number(sidoData[i]) + value) as string | number;
|
||||
} else {
|
||||
sidoData[i] = '';
|
||||
}
|
||||
@@ -209,7 +203,7 @@ export default function RswtStat() {
|
||||
});
|
||||
|
||||
const sidoSummaryList = Array.from(sidoDataMap.values());
|
||||
const sidoTotalSummary = [['전국', '', '합계', 0, '', '', 0, '', '', 0, 0, 0, 0, 0, 0, '']];
|
||||
const sidoTotalSummary = [['전국', '', '합계', 0, '', '', 0, '', '', 0, 0, 0, 0, '', 0]];
|
||||
sidoSummaryList.forEach((item: (string | number)[]) => {
|
||||
item.forEach((value: string | number, index: number) => {
|
||||
if (typeof value === 'number') {
|
||||
@@ -226,7 +220,7 @@ export default function RswtStat() {
|
||||
const sheetArray: SheetParameter[] = [{
|
||||
data: excelData,
|
||||
headerNum: 3,
|
||||
headerRange: "A1:P3",
|
||||
headerRange: "A1:O3",
|
||||
sheetName: 'sheet1',
|
||||
startCol: 'A',
|
||||
decimalArray0: [1,2,3,4,5,6,7,8,9,10,11,12], // 0부터 시작
|
||||
|
||||
@@ -12,7 +12,7 @@ export type authState = {
|
||||
mdfcnAuthrtYn: string;
|
||||
delAuthrtYn: string;
|
||||
laeSeCd: string; // 지자체구분코드
|
||||
myUUID?: string;
|
||||
myUUID?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -67,17 +67,18 @@ export const authActionsExport: (set: any, get: any) => authActions = (
|
||||
redirect("/");
|
||||
}
|
||||
},
|
||||
// 로그인 사용자 지역코드 laeSeCd를 조회 후 상태 update
|
||||
getLaeSeCdCheck: async () => {
|
||||
try {
|
||||
const current = get().auth.laeSeCd;
|
||||
if (current && current !== "") {
|
||||
return;
|
||||
}
|
||||
const infoMe = await getUserInfo();
|
||||
//console.log(infoMe);
|
||||
const infoMe = await getUserInfo(); // 로그인 사용자의 정보 조회
|
||||
// console.log(infoMe);
|
||||
const isLaeSeCd =
|
||||
infoMe.laeSeCd.slice(0, 2) === "G3" ? infoMe.laeSeCd.slice(2) : "0";
|
||||
|
||||
infoMe.laeSeCd.slice(0, 2) === "G3" ? infoMe.laeSeCd.slice(2) : "0"; //G341830
|
||||
// console.log("isLaeSeCd",isLaeSeCd);
|
||||
set((state: authState) => ({
|
||||
...state,
|
||||
auth: {
|
||||
|
||||
@@ -40,7 +40,6 @@ const codeMApping = {
|
||||
};
|
||||
export const getCommonUpCd = async (set: any) => {
|
||||
try {
|
||||
let lwCdCode: Record<string, SelectDataType[]> = {};
|
||||
const upCd = await getFetchWithCount(
|
||||
fetchType.CS,
|
||||
"/cms-service/api/v1/up-codes?keyword=&upCd=&cdSe=&useYn=Y&siteId=WR&page=0&size=100&sort=string"
|
||||
@@ -50,24 +49,9 @@ export const getCommonUpCd = async (set: any) => {
|
||||
.filter((item: any) => item.upCd.slice(0, 2) === "WR")
|
||||
.map((cd: any) => cd.upCd);
|
||||
|
||||
await Promise.all(
|
||||
upWrCode.map(async (cd) => {
|
||||
const mapping = codeMApping[cd as keyof typeof codeMApping];
|
||||
if (!mapping) return;
|
||||
await fetchCommonCodesByKeys(upWrCode);
|
||||
|
||||
const lwCd = await getFetchWithCount(
|
||||
fetchType.CS,
|
||||
`/cms-service/api/v1/lwpo-cds/up-cd/${cd}`
|
||||
);
|
||||
|
||||
const list = Array.isArray(lwCd) ? lwCd : (lwCd.content ?? []);
|
||||
lwCds[mapping] = list.map((it: any) => ({
|
||||
text: it.lwpoCdNm,
|
||||
val: it.lwpoCd,
|
||||
}));
|
||||
})
|
||||
);
|
||||
lwCdCode = lwCds;
|
||||
const lwCdCode = lwCds;
|
||||
set((state: comCodeState) => ({
|
||||
code: {
|
||||
...state.code,
|
||||
@@ -78,6 +62,8 @@ export const getCommonUpCd = async (set: any) => {
|
||||
pplLenght: lwCdCode["pplLenght"] ?? [],
|
||||
dmdUszCd: lwCdCode["dmdUszCd"] ?? [],
|
||||
sewageOutFclttUse: lwCdCode["sewageOutFclttUse"] ?? [],
|
||||
rainRswtMng: lwCdCode["rainRswtMng"] ?? [],
|
||||
rainFcltFlowRate: lwCdCode["rainFcltFlowRate"] ?? [],
|
||||
ruseMngMtd: lwCdCode["ruseMngMtd"] ?? [],
|
||||
rusePrcsMthdCd: lwCdCode["rusePrcsMthdCd"] ?? [],
|
||||
erctUswtrPrcsMeth: lwCdCode["erctUswtrPrcsMeth"] ?? [],
|
||||
@@ -93,6 +79,30 @@ export const getCommonUpCd = async (set: any) => {
|
||||
console.log("com code error ", e);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCommonCodesByKeys = async (keys: string[]) => {
|
||||
const targets = [...new Set(keys)].filter(
|
||||
(cd) => Boolean(codeMApping[cd as keyof typeof codeMApping])
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
targets.map(async (cd) => {
|
||||
const mapping = codeMApping[cd as keyof typeof codeMApping];
|
||||
if (!mapping) return;
|
||||
|
||||
const lwCd = await getFetchWithCount(
|
||||
fetchType.CS,
|
||||
`/cms-service/api/v1/lwpo-cds/up-cd/${cd}`
|
||||
);
|
||||
|
||||
const list = Array.isArray(lwCd) ? lwCd : (lwCd.content ?? []);
|
||||
lwCds[mapping] = list.map((it: any) => ({
|
||||
text: it.lwpoCdNm,
|
||||
val: it.lwpoCd,
|
||||
}));
|
||||
})
|
||||
);
|
||||
};
|
||||
//물재이용 주소 코드: devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM=
|
||||
export const buildingType = {
|
||||
RST: "RST",
|
||||
@@ -219,41 +229,42 @@ export const getEnvPartCd = async (set: any) => {
|
||||
|
||||
export type comCodeState = {
|
||||
code: {
|
||||
isInit: boolean;
|
||||
chBuCd: SelectDataType[];
|
||||
rainFcltCd: SelectDataType[];
|
||||
bdstUsgCd: SelectDataType[];
|
||||
cityCd: SelectDataType[];
|
||||
sggCd: SelectDataType[];
|
||||
rvbsnCd: SelectDataType[];
|
||||
mdmbsnCd: SelectDataType[];
|
||||
lgbsnCd: SelectDataType[];
|
||||
lclgvrnmntCd: EnvPartSelectDataType[];
|
||||
orgwtKndCd: SelectDataType[];
|
||||
yn: SelectDataType[];
|
||||
pplQulity: SelectDataType[];
|
||||
pplLenght: SelectDataType[];
|
||||
dmdUszCd: SelectDataType[];
|
||||
sewageOutFclttUse: SelectDataType[];
|
||||
isInit: boolean; // 초기화 여부
|
||||
chBuCd: SelectDataType[]; // 변경사업코드
|
||||
rainFcltCd: SelectDataType[]; // 빗물시설코드
|
||||
bdstUsgCd: SelectDataType[]; // 건물용도코드
|
||||
cityCd: SelectDataType[]; // 시도코드
|
||||
sggCd: SelectDataType[]; // 시군구코드
|
||||
rvbsnCd: SelectDataType[]; // 대권역코드
|
||||
mdmbsnCd: SelectDataType[]; // 중권역코드
|
||||
lgbsnCd: SelectDataType[]; // 단위유역(대영역)코드
|
||||
lclgvrnmntCd: EnvPartSelectDataType[]; // 지방환경청코드
|
||||
orgwtKndCd: SelectDataType[]; // 원수종류코드
|
||||
yn: SelectDataType[]; // 여부코드
|
||||
pplQulity: SelectDataType[]; // 관저질
|
||||
pplLenght: SelectDataType[]; // 관연장
|
||||
dmdUszCd: SelectDataType[]; // 용도별코드
|
||||
sewageOutFclttUse: SelectDataType[]; // 하수재이용 장외코드
|
||||
rainRswtMng: SelectDataType[]; // 운영관리형태코드
|
||||
rainFcltFlowRate: SelectDataType[]; // 유량측정방법코드
|
||||
buildingType: {
|
||||
RST: string;
|
||||
RNB: string;
|
||||
HTB: string;
|
||||
};
|
||||
ruseMngMtd: SelectDataType[];
|
||||
ruseSeCd: SelectDataType[];
|
||||
ruseBizMthdCd: SelectDataType[];
|
||||
rusePrcsMthdCd: SelectDataType[];
|
||||
erctUswtrPrcsMeth: SelectDataType[];
|
||||
bdstFilterYn: SelectDataType[];
|
||||
bdstIncenYn: SelectDataType[];
|
||||
bdstGrntchClass: SelectDataType[];
|
||||
// rswtPrscCdMthdCd: SelectDataType[];
|
||||
htdPowerMthdCd: SelectDataType[];
|
||||
htdUtztnUsgCd: SelectDataType[];
|
||||
rswtBdstType: SelectDataType[];
|
||||
bdstTotalUsgType: SelectDataType[];
|
||||
userSiSggCd: string;
|
||||
RST: string; // 중수도시설 구분
|
||||
RNB: string; // 빗물이용시설 구분
|
||||
HTB: string; // 온배수시설 구분
|
||||
}; // 시설구분
|
||||
ruseMngMtd: SelectDataType[]; // 재이용관리방식코드
|
||||
ruseSeCd: SelectDataType[]; // 재이용구분코드
|
||||
ruseBizMthdCd: SelectDataType[]; // 재이용사업방식코드
|
||||
rusePrcsMthdCd: SelectDataType[]; // 재이용처리공법코드
|
||||
erctUswtrPrcsMeth: SelectDataType[]; // 하수처리공법코드
|
||||
bdstFilterYn: SelectDataType[]; // 여과시설여부코드
|
||||
bdstIncenYn: SelectDataType[]; // 인센티브여부코드
|
||||
bdstGrntchClass: SelectDataType[]; // 보조금지원대상분류코드
|
||||
htdPowerMthdCd: SelectDataType[]; // 온배수발전방식코드
|
||||
htdUtztnUsgCd: SelectDataType[]; // 온배수이용용도코드
|
||||
rswtBdstType: SelectDataType[]; // 중수도건물유형코드
|
||||
bdstTotalUsgType: SelectDataType[]; // 전체건물용도유형코드
|
||||
userSiSggCd: string; // 사용자 시군구코드
|
||||
};
|
||||
};
|
||||
|
||||
@@ -277,6 +288,8 @@ export const comCodeStateExport: comCodeState = {
|
||||
yn: yn,
|
||||
dmdUszCd: lwCds["dmdUszCd"] ?? [],
|
||||
sewageOutFclttUse: lwCds["sewageOutFclttUse"] ?? [],
|
||||
rainRswtMng: lwCds["rainRswtMng"] ?? [],
|
||||
rainFcltFlowRate: lwCds["rainFcltFlowRate"] ?? [],
|
||||
buildingType: buildingType,
|
||||
ruseMngMtd: lwCds["ruseMngMtd"] ?? [],
|
||||
ruseSeCd: ruseSeCd,
|
||||
@@ -298,10 +311,51 @@ export const comCodeStateExport: comCodeState = {
|
||||
export type comCodeActions = {
|
||||
returnCodeText: (val: string, codeName: string) => string;
|
||||
returnTextCode: (text: string, codeName: string) => string;
|
||||
getCommonCodesByKeys: (keys: string[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export const codeActionExport: (set: any, get:any) => comCodeActions = (set: any, get:any) => {
|
||||
return {
|
||||
getCommonCodesByKeys: async (keys: string[]) => {
|
||||
try {
|
||||
await fetchCommonCodesByKeys(keys);
|
||||
const lwCdCode = lwCds;
|
||||
|
||||
set((state: comCodeState) => ({
|
||||
code: {
|
||||
...state.code,
|
||||
rainFcltCd: lwCdCode["rainFcltCd"] ?? state.code.rainFcltCd,
|
||||
bdstUsgCd: lwCdCode["bdstUsgCd"] ?? state.code.bdstUsgCd,
|
||||
orgwtKndCd: lwCdCode["orgwtKndCd"] ?? state.code.orgwtKndCd,
|
||||
pplQulity: lwCdCode["pplQulity"] ?? state.code.pplQulity,
|
||||
pplLenght: lwCdCode["pplLenght"] ?? state.code.pplLenght,
|
||||
dmdUszCd: lwCdCode["dmdUszCd"] ?? state.code.dmdUszCd,
|
||||
sewageOutFclttUse:
|
||||
lwCdCode["sewageOutFclttUse"] ?? state.code.sewageOutFclttUse,
|
||||
rainRswtMng: lwCdCode["rainRswtMng"] ?? state.code.rainRswtMng,
|
||||
rainFcltFlowRate:
|
||||
lwCdCode["rainFcltFlowRate"] ?? state.code.rainFcltFlowRate,
|
||||
ruseMngMtd: lwCdCode["ruseMngMtd"] ?? state.code.ruseMngMtd,
|
||||
rusePrcsMthdCd:
|
||||
lwCdCode["rusePrcsMthdCd"] ?? state.code.rusePrcsMthdCd,
|
||||
erctUswtrPrcsMeth:
|
||||
lwCdCode["erctUswtrPrcsMeth"] ?? state.code.erctUswtrPrcsMeth,
|
||||
bdstGrntchClass:
|
||||
lwCdCode["bdstGrntchClass"] ?? state.code.bdstGrntchClass,
|
||||
htdPowerMthdCd:
|
||||
lwCdCode["htdPowerMthdCd"] ?? state.code.htdPowerMthdCd,
|
||||
htdUtztnUsgCd:
|
||||
lwCdCode["htdUtztnUsgCd"] ?? state.code.htdUtztnUsgCd,
|
||||
rswtBdstType: lwCdCode["rswtBdstType"] ?? state.code.rswtBdstType,
|
||||
bdstTotalUsgType:
|
||||
lwCdCode["bdstUsgCd"]?.concat(lwCdCode["rswtBdstType"] || []) ??
|
||||
state.code.bdstTotalUsgType,
|
||||
},
|
||||
}));
|
||||
} catch (e) {
|
||||
console.log("selected common code error ", e);
|
||||
}
|
||||
},
|
||||
returnCodeText: (val: string, codeName: string) => {
|
||||
|
||||
const codeList = get().code[codeName] || [];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getFetch } from "@/components/lib/api";
|
||||
import type { FacilityInfo, SubsInfo } from "./type";
|
||||
import { DEV_URL_FO } from "@/components/lib/config";
|
||||
import { SITE_ID } from "@/src/shared/config";
|
||||
|
||||
const checkWrRegList = async () => {
|
||||
const wrRegList = await getFetch("wr", `/reuseFc/pubFc`);
|
||||
@@ -26,6 +27,7 @@ const getRuseFclt = async (
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify(ruseList),
|
||||
});
|
||||
|
||||
@@ -270,35 +270,25 @@ export const rswtExcelDataCheck = (
|
||||
issues: string[]
|
||||
) => {
|
||||
// const chkMngType = { "01": "위탁", "02": "자체" };
|
||||
|
||||
//rainRswtMng: SelectDataType[]; // 운영관리형태코드
|
||||
// console.log(rswtChkgdData);
|
||||
rswtChkgdData.forEach((rswt: any) => {
|
||||
const defaultMessage = `*중수도 수질 [NO: ${rswt.bdstRuseFcltSn}, 점검일: ${rswt.chckYmd}, 이용용도: ${codeToText(rswt.utztnUsgCd ,"dmdUszCd")??rswt.utztnUsgCd}] 항목에서 `;
|
||||
|
||||
if (rswt.chckYmd && isInvalidDate(rswt.chckYmd)) {
|
||||
issues.push(defaultMessage + `점검일: 날짜 형식 아님 (${rswt.chckYmd})`);
|
||||
}
|
||||
// if (rswt.operMngCd?.length > 1 && !chkMngType[rswt.operMngCd]) {
|
||||
// issues.push(
|
||||
// defaultMessage +
|
||||
// `운영관리형태: 위탁 혹은 자체만 허용 (${rswt.operMngCd})`
|
||||
// );
|
||||
// }
|
||||
// if (rswt.utztnUsgCd?.length > 2 && !codeToText(rswt.utztnUsgCd, "dmdUszCd")) {
|
||||
// issues.push(
|
||||
// defaultMessage + `이용용도: 허용되지 않은 값 (${rswt.utztnUsgCd})`
|
||||
// );
|
||||
// }
|
||||
if (rswt.operMngCd?.length > 1 && !codeToText(rswt.operMngCd, "rainRswtMng")) {
|
||||
issues.push( defaultMessage + `운영관리형태: 위탁 혹은 자체만 허용 (${rswt.operMngCd})` );
|
||||
}
|
||||
if (rswt.utztnUsgCd?.length > 2 && !codeToText(rswt.utztnUsgCd, "dmdUszCd")) {
|
||||
issues.push( defaultMessage + `이용용도: 허용되지 않은 값 (${rswt.utztnUsgCd})` );
|
||||
}
|
||||
if (rswt.anlOperCst.length > 2 && isInvalidNumber(rswt.anlOperCst)) {
|
||||
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
` 연간운영비(원/년): 숫자 형식 아님 (${rswt.anlOperCst})`
|
||||
);
|
||||
issues.push( defaultMessage + ` 연간운영비(원/년): 숫자 형식 아님 (${rswt.anlOperCst})` );
|
||||
}
|
||||
if (rswt.prcsCpct.length > 2 && isInvalidNumber(rswt.prcsCpct)) {
|
||||
issues.push(
|
||||
defaultMessage + ` 처리용량(m³): 숫자 형식 아님 (${rswt.prcsCpct})`
|
||||
);
|
||||
issues.push( defaultMessage + ` 처리용량(m³): 숫자 형식 아님 (${rswt.prcsCpct})` );
|
||||
}
|
||||
// if (
|
||||
// rswt.spwtPositn.length > 1 &&
|
||||
@@ -313,55 +303,31 @@ export const rswtExcelDataCheck = (
|
||||
if (rswt?.rswtrPollutions && rswt?.rswtrPollutions.length > 0) {
|
||||
rswt.rswtrPollutions.forEach((pol) => {
|
||||
if (isInvalidNumber(pol.grssClfgUswtr)) {
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
`${pol.node}분기_총대장균군수: 숫자 형식 아님 (${pol.grssClfgUswtr})`
|
||||
);
|
||||
issues.push( defaultMessage + `${pol.node}분기_총대장균군수: 숫자 형식 아님 (${pol.grssClfgUswtr})` );
|
||||
}
|
||||
if (isInvalidNumber(pol.combResdch)) {
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
`${pol.node}분기_결합잔류염소: 숫자 형식 아님 (${pol.combResdch})`
|
||||
);
|
||||
issues.push( defaultMessage + `${pol.node}분기_결합잔류염소: 숫자 형식 아님 (${pol.combResdch})` );
|
||||
}
|
||||
if (isInvalidNumber(pol.trbt)) {
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
`${pol.node}분기_탁도: 숫자 형식 아님 (${pol.trbt})`
|
||||
);
|
||||
issues.push( defaultMessage + `${pol.node}분기_탁도: 숫자 형식 아님 (${pol.trbt})` );
|
||||
}
|
||||
if (isInvalidNumber(pol.bod)) {
|
||||
issues.push(
|
||||
defaultMessage + `${pol.node}분기_BOD: 숫자 형식 아님 (${pol.bod})`
|
||||
);
|
||||
issues.push( defaultMessage + `${pol.node}분기_BOD: 숫자 형식 아님 (${pol.bod})` );
|
||||
}
|
||||
if (isInvalidNumber(pol.crmty)) {
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
`${pol.node}분기_색도: 숫자 형식 아님 (${pol.crmty})`
|
||||
);
|
||||
issues.push( defaultMessage + `${pol.node}분기_색도: 숫자 형식 아님 (${pol.crmty})` );
|
||||
}
|
||||
if (isInvalidNumber(pol.tph)) {
|
||||
issues.push(
|
||||
defaultMessage + `${pol.node}분기_T-P: 숫자 형식 아님 (${pol.tph})`
|
||||
);
|
||||
issues.push( defaultMessage + `${pol.node}분기_T-P: 숫자 형식 아님 (${pol.tph})` );
|
||||
}
|
||||
if (isInvalidNumber(pol.tn)) {
|
||||
issues.push(
|
||||
defaultMessage + `${pol.node}분기_T-N: 숫자 형식 아님 (${pol.tn})`
|
||||
);
|
||||
issues.push( defaultMessage + `${pol.node}분기_T-N: 숫자 형식 아님 (${pol.tn})` );
|
||||
}
|
||||
if (pol.ph?.length > 1 && isInvalidNumber(pol.ph)) {
|
||||
issues.push(
|
||||
defaultMessage + `${pol.node}분기_PH: 숫자 형식 아님 (${pol.ph})`
|
||||
);
|
||||
if (isInvalidNumber(pol.ph)) {
|
||||
issues.push( defaultMessage + `${pol.node}분기_PH: 숫자 형식 아님 (${pol.ph})` );
|
||||
}
|
||||
|
||||
if (isInvalidNumber(pol.clIonQnt)) {
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
`${pol.node}분기_염소이온량: 숫자 형식 아님 (${pol.clIonQnt})`
|
||||
);
|
||||
if (isInvalidNumber(pol.clIonQnt)) {
|
||||
issues.push( defaultMessage + `${pol.node}분기_염소이온량: 숫자 형식 아님 (${pol.clIonQnt})` );
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -369,19 +335,43 @@ export const rswtExcelDataCheck = (
|
||||
if (rswt?.rswtrDetails && rswt?.rswtrDetails.length > 0) {
|
||||
rswt?.rswtrDetails.forEach((key) => {
|
||||
if (key.levyAmt?.length > 1 && isInvalidNumber(key.levyAmt)) {
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
`과태료부과금액(천원): 숫자 형식 아님 (${key.levyAmt})`
|
||||
);
|
||||
issues.push( defaultMessage + `과태료부과금액(천원): 숫자 형식 아님 (${key.levyAmt})` );
|
||||
}
|
||||
if (key.vltnDt?.length > 2 && isInvalidDate(key.vltnDt)) {
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
`과태료 부과일(위반일): 날짜 형식 아님 (${key.vltnDt})`
|
||||
);
|
||||
issues.push( defaultMessage + `과태료 부과일(위반일): 날짜 형식 아님 (${key.vltnDt})` );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const polutionUseCodes = new Set<string>(
|
||||
(rswt?.rswtrPollutions ?? [])
|
||||
.map((pol) => pol.utztnUsgCd)
|
||||
.filter((code): code is string => Boolean(code))
|
||||
);
|
||||
const guideDetailCodes = new Set<string>(
|
||||
(rswt?.guideDetails ?? [])
|
||||
.map((detail) => detail.utztnUsgCd)
|
||||
.filter((code): code is string => Boolean(code))
|
||||
);
|
||||
|
||||
polutionUseCodes.forEach((code) => {
|
||||
if (!guideDetailCodes.has(code)) {
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
`중수도_점검결과_수질 sheet의 이용용도에 대응하는 중수도_점검결과_이용량현황sheet의 이용용도가 없습니다.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
guideDetailCodes.forEach((code) => {
|
||||
if (!polutionUseCodes.has(code)) {
|
||||
issues.push(
|
||||
defaultMessage +
|
||||
`중수도_점검결과_이용량현황sheet의 이용용도에 대응하는 중수도_점검결과_수질sheet의 이용용도가 없습니다.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { buildingType } from "@/app/store/common/code";
|
||||
/**
|
||||
* 중수도 지도점검 store
|
||||
*/
|
||||
import { buildingType } from "@/app/store/common/code";
|
||||
import type { TableUpperProps } from "@/app/store/common/type/header";
|
||||
import {
|
||||
TEST_UUID,
|
||||
@@ -53,29 +56,14 @@ export type rswtChkgdnState = {
|
||||
isInit: boolean; // 초기 목록 조회 완료 여부
|
||||
isSttn: boolean; // 테이블 선택/상세 상태 제어 플래그
|
||||
isLoading:boolean; // 등록/업로드 등 비동기 처리 진행 여부
|
||||
rswtChkCount: number; // 메인 목록 전체 건수
|
||||
rswtChkCount: number; // 메인 목록 전체 건수 : 시설의 건수임
|
||||
};
|
||||
};
|
||||
|
||||
type rswtChkgdnActions = {
|
||||
rswtChkgdnInit: (
|
||||
pageNo: number,
|
||||
cnt: number,
|
||||
year: string,
|
||||
sggCd: string
|
||||
) => void;
|
||||
rswtChkNmSearch: (
|
||||
pageNo: number,
|
||||
cnt: number,
|
||||
year: string,
|
||||
sggCd: string,
|
||||
txt: string
|
||||
) => void;
|
||||
rswtrChkExcelDownload(
|
||||
sidoCode: SelectDataType[],
|
||||
sggCode: SelectDataType[],
|
||||
sggCd: string,
|
||||
): Promise<void>;
|
||||
rswtChkgdnInit: ( pageNo: number, cnt: number, year: string, sggCd: string ) => void;
|
||||
rswtChkNmSearch: ( pageNo: number, cnt: number, year: string, sggCd: string, txt: string ) => void;
|
||||
rswtrChkExcelDownload( sidoCode: SelectDataType[], sggCode: SelectDataType[], sggCd: string, ): Promise<void>;
|
||||
rswtChkExceDataUpload: (excelData: rswtChkZodType[],laeSeCd:string) => Promise<string>;
|
||||
rswtChkDelete: (chkgdnSns: number[] ,year:string) => Promise<string>;
|
||||
rswtChkDetilsUpdate: (rswtChkDetails: rswtrChkDetailsZodType[] ,year:string) => Promise<string>;
|
||||
@@ -189,8 +177,7 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = (
|
||||
// set((state: rswtChkgdnType) => ({ rswtChkgdn: { ...state.rswtChkgdn, isLoading: true } }));
|
||||
try {
|
||||
const rswtChkRes = await initFetch(pageNo, cnt, year, sggCd);
|
||||
|
||||
|
||||
// console.log(rswtChkRes);
|
||||
set((state: rswtChkgdnState) => ({
|
||||
...state,
|
||||
rswtChkgdn: {
|
||||
@@ -246,7 +233,7 @@ export const rswtChkgdnActionsExport: (set: any) => rswtChkgdnActions = (
|
||||
rswtChkExceDataUpload: async (excelData: rswtChkZodType[],laeSeCd:string) => {
|
||||
const successMessage = "지도점검 등록이 완료되었습니다.";
|
||||
const failMessage = "지도점검 등록이 실패되었습니다.";
|
||||
console.log(excelData);
|
||||
// console.log(excelData);
|
||||
try {
|
||||
set((state: rswtChkgdnType) => (
|
||||
{ rswtChkgdn: { ...state.rswtChkgdn, isLoading: true } }));
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
postFectch,
|
||||
putFectch,
|
||||
} from "@/components/lib/api";
|
||||
import { SITE_ID } from "@/src/shared/config";
|
||||
import type { chHeadType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn/headList";
|
||||
import {
|
||||
type customPbnst,
|
||||
@@ -99,18 +100,18 @@ export const pubFacStateExport: pubFacState = {
|
||||
pbnstHasuResCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
// 재사용시설에 대한 정보 조회
|
||||
export const checkWrRegList = async () => {
|
||||
const wrRegList = await getFetch("wr", `/reuseFc/pubFc`);
|
||||
return wrRegList;
|
||||
};
|
||||
|
||||
// 하수처리시설 정보 조회
|
||||
const hasuListFetch = async (
|
||||
page: string,
|
||||
sggOrCity: string,
|
||||
sggOrCity: string, // 해당 지자체의 하수처리시설 조회
|
||||
searchTxt?: string
|
||||
) => {
|
||||
const checkWrRegListRes = await checkWrRegList();
|
||||
const checkWrRegListRes = await checkWrRegList(); // 재사용시설에 대한 정보 조회
|
||||
|
||||
let url = `/prcsFclt/ruseInfo/${page ? page : 1}/11/A/${sggOrCity ? sggOrCity : "11"}/0/1/SW`;
|
||||
|
||||
@@ -307,6 +308,7 @@ export const pubFacActionsExport: (set: any) => pubFacActions = (set: any) => {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/octet-stream",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -112,6 +112,12 @@ export const hasuListFetch = async (
|
||||
year: string,
|
||||
flcts: string[]
|
||||
) => {
|
||||
|
||||
// 시설 코드가 없으면 FO 조회를 호출하지 않고 빈 결과 반환
|
||||
if (flcts.length === 0) {
|
||||
return { data: [], count: 0 };
|
||||
}
|
||||
|
||||
let url = ``;
|
||||
let filteredFclt = flcts;
|
||||
const filterData =
|
||||
@@ -163,6 +169,11 @@ export const hasuListFetch = async (
|
||||
url += "/" + searchNm;
|
||||
}
|
||||
|
||||
// 시설 코드가 없으면 FO 조회를 호출하지 않고 빈 결과 반환
|
||||
if (filteredFclt.length === 0) {
|
||||
return { data: [], count: 0 };
|
||||
}
|
||||
|
||||
const pbnList = await postFectch(
|
||||
`/prcsFclt/ruseInfo/1/${filteredFclt.length}/` + url,
|
||||
filteredFclt,
|
||||
|
||||
@@ -1,41 +1,51 @@
|
||||
import type { RainStatProps } from ".";
|
||||
import { codeToText } from "@/components/lib/func/common";
|
||||
import type { RainStatProps } from ".";
|
||||
|
||||
export const excelRainStat = (excelList:RainStatProps[]) : (string | number)[][] => {
|
||||
const toUseTypeText = (val: string) =>
|
||||
val
|
||||
.split(",")
|
||||
.map((code) => code.trim())
|
||||
.filter(Boolean)
|
||||
.map((code) => codeToText(code, "dmdUszCd") ?? code)
|
||||
.join(", ");
|
||||
|
||||
export const excelRainStat = (
|
||||
excelList: RainStatProps[]
|
||||
): (string | number)[][] => {
|
||||
const excelData: (string | number)[][] = [];
|
||||
excelList.forEach((item) => {
|
||||
excelList.forEach((item) => {
|
||||
excelData.push([
|
||||
//시도
|
||||
item.ctpvCd ?? "",
|
||||
//시군구
|
||||
item.sggCd ?? "",
|
||||
//행정구역명
|
||||
String(item.areaNm) ?? "",
|
||||
//행정구역명
|
||||
String(item.areaNm) ?? "",
|
||||
//시설명
|
||||
String(item.bdstNm) ?? "",
|
||||
//건축물용도코드 (값 없음)
|
||||
//String(item.bdstUsgCd) ?? "",
|
||||
"",
|
||||
codeToText(String(item.bdstUsgCd ?? ""), "bdstUsgCd") ?? String(item.bdstUsgCd ?? ""),
|
||||
//주소
|
||||
String(item.daddr) ?? "",
|
||||
//법적의무대상여부
|
||||
String(item.lawTrgtYn)==="Y" ? "대상" : "미대상",
|
||||
String(item.lawTrgtYn) === "Y" ? "대상" : "미대상",
|
||||
//설치완료일자
|
||||
String(item.instlCmptnDt).substring(0, 10) ?? "",
|
||||
//설치비용
|
||||
item.instlCst ?? "",
|
||||
//접수면종류
|
||||
String(item.pntfColKnd) ?? "",
|
||||
//집수면종류
|
||||
codeToText(String(item.pntfColKnd ?? ""), "rainFcltCd") ?? String(item.pntfColKnd ?? ""),
|
||||
//여과등처리시설유무
|
||||
String(item.filtrTrtmfctYn) ?? "",
|
||||
//처리용량(저류조용량)
|
||||
item.dtfcltCpct ?? "",
|
||||
//연간사용량
|
||||
item.wtrUsqty ?? "",
|
||||
//이용용도코드
|
||||
item.utztnUsgCd ?? "",
|
||||
//연간운영비
|
||||
item.anlOperCst ?? "",
|
||||
]);
|
||||
});
|
||||
//처리용량(저류조용량)
|
||||
item.dtfcltCpct ?? "",
|
||||
//연간사용량
|
||||
item.wtrUsqty ?? "",
|
||||
//이용용도코드
|
||||
toUseTypeText(item.utztnUsgCd ?? ""),
|
||||
//연간운영비
|
||||
item.anlOperCst ?? "",
|
||||
]);
|
||||
});
|
||||
return excelData;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,158 +1,34 @@
|
||||
import type { TableUpperProps } from "../../common/type/header";
|
||||
|
||||
export const rainStatHeadListExport: TableUpperProps[] = [
|
||||
{
|
||||
id: "ctpvCd",
|
||||
title: "시도",
|
||||
row:0,
|
||||
rowSpan:2
|
||||
},
|
||||
{
|
||||
id: "tmp",
|
||||
title: "개소",
|
||||
row:0,
|
||||
rowSpan:2
|
||||
},
|
||||
{
|
||||
id: "instlCst",
|
||||
title: "설치비(백만원)",
|
||||
row:0,
|
||||
rowSpan:2
|
||||
},
|
||||
{
|
||||
id: "dtfcltCpct",
|
||||
title: "저류조용량(m³/년)",
|
||||
row:0,
|
||||
rowSpan:2
|
||||
},
|
||||
{
|
||||
id: "wtrUsqty",
|
||||
title: "연간사용량(㎥/년)",
|
||||
row:0,
|
||||
rowSpan:2
|
||||
},
|
||||
{
|
||||
id: "a",
|
||||
title: "이용용도",
|
||||
row:0,
|
||||
colSpan:7
|
||||
},
|
||||
{
|
||||
id: "lndwtr",
|
||||
title: "조경용수",
|
||||
commonCode : "01",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "cleanToilUswtr",
|
||||
title: "청소화장실용수",
|
||||
commonCode : "02",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id:"resn",
|
||||
title:"세척살수용수",
|
||||
commonCode : "03",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "farmUswtr",
|
||||
title: "농업용수",
|
||||
commonCode : "04",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "indstUswtr",
|
||||
title: "공업용수",
|
||||
commonCode : "05",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "hydUswtr",
|
||||
title: "친수용수",
|
||||
commonCode : "06",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "ithlEtcUswtr",
|
||||
title: "기타",
|
||||
commonCode : "07",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "anlOperCst",
|
||||
title: "연간운영비(백만원/년)",
|
||||
row:0,
|
||||
rowSpan:2
|
||||
},
|
||||
{ id: "ctpvCd", title: "시도", row:0, rowSpan:2 },
|
||||
{ id: "tmp", title: "개소", row:0, rowSpan:2 },
|
||||
{ id: "instlCst", title: "설치비(백만원)", row:0, rowSpan:2 },
|
||||
{ id: "dtfcltCpct", title: "저류조용량(m³/년)", row:0, rowSpan:2 },
|
||||
{ id: "wtrUsqty", title: "연간사용량(㎥/년)", row:0, rowSpan:2 },
|
||||
{ id: "a", title: "이용용도", row:0, colSpan:7 },
|
||||
{ id: "lndwtr", title: "조경용수", commonCode : "01", row:1 },
|
||||
{ id: "cleanToilUswtr", title: "청소화장실용수", commonCode : "02", row:1 },
|
||||
{ id: "resn", title: "세척살수용수", commonCode : "03", row:1 },
|
||||
{ id: "farmUswtr", title: "농업용수", commonCode : "04", row:1 },
|
||||
{ id: "indstUswtr", title: "공업용수", commonCode : "05", row:1 },
|
||||
{ id: "hydUswtr", title: "친수용수", commonCode : "06", row:1 },
|
||||
{ id: "ithlEtcUswtr", title: "기타", commonCode : "07", row:1 },
|
||||
{ id: "anlOperCst", title: "연간운영비(백만원/년)", row:0, rowSpan:2 },
|
||||
];
|
||||
|
||||
export const rainStatDetailHeadListExport: TableUpperProps[] = [
|
||||
{
|
||||
id: "ctpvCd",
|
||||
title: "시도",
|
||||
row:0,
|
||||
},
|
||||
{
|
||||
id: "sggCd",
|
||||
title: "시군구",
|
||||
row:0,
|
||||
},
|
||||
{
|
||||
id: "areaNm",
|
||||
title: "행정구역명",
|
||||
row:0,
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
title: "시설명",
|
||||
row:0,
|
||||
},
|
||||
{
|
||||
id: "이용용도",
|
||||
title: "이용용도",
|
||||
row:0,
|
||||
colSpan:7
|
||||
},
|
||||
{
|
||||
id: "조경용수",
|
||||
title: "조경용수",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "청소화장실용수",
|
||||
title: "청소화장실용수",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id:"세척살수용수",
|
||||
title:"세척살수용수",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "농업용수",
|
||||
title: "농업용수",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "공업용수",
|
||||
title: "공업용수",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "친수용수",
|
||||
title: "친수용수",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "기타",
|
||||
title: "기타",
|
||||
row:1,
|
||||
},
|
||||
{
|
||||
id: "연간운영비",
|
||||
title: "연간운영비(백만원/년)",
|
||||
row:0,
|
||||
rowSpan:2
|
||||
},
|
||||
{ id: "ctpvCd", title: "시도", row:0 },
|
||||
{ id: "sggCd", title: "시군구", row:0 },
|
||||
{ id: "areaNm", title: "행정구역명", row:0 },
|
||||
{ id: "", title: "시설명", row:0 },
|
||||
{ id: "이용용도", title: "이용용도", row:0, colSpan:7 },
|
||||
{ id: "조경용수", title: "조경용수", row:1 },
|
||||
{ id: "청소화장실용수", title: "청소화장실용수", row:1 },
|
||||
{ id: "세척살수용수", title: "세척살수용수", row:1 },
|
||||
{ id: "농업용수", title: "농업용수", row:1 },
|
||||
{ id: "공업용수", title: "공업용수", row:1 },
|
||||
{ id: "친수용수", title: "친수용수", row:1 },
|
||||
{ id: "기타", title: "기타", row:1 },
|
||||
{ id: "연간운영비", title: "연간운영비(백만원/년)", row:0, rowSpan:2 },
|
||||
];
|
||||
@@ -89,6 +89,7 @@ export const rainStatActionsExport = (
|
||||
searchRainStatList: async (year: string, city: SelectDataType[]) => {
|
||||
const res = await fetchRainStatList(year);
|
||||
const cityArr: any[] = [];
|
||||
excelDownList.length = 0;
|
||||
|
||||
res.filter((resFilter: RainStatProps) =>
|
||||
city.some((cityCd) =>
|
||||
@@ -104,7 +105,6 @@ export const rainStatActionsExport = (
|
||||
if (obj) {
|
||||
obj.instlCst += item.instlCst;
|
||||
obj.dtfcltCpct += item.dtfcltCpct;
|
||||
obj.wtrUsqty += item.wtrUsqty;
|
||||
obj.anlOperCst += item.anlOperCst;
|
||||
obj.tmp += 1;
|
||||
} else {
|
||||
@@ -113,7 +113,7 @@ export const rainStatActionsExport = (
|
||||
obj.instlCst = item.instlCst;
|
||||
obj.tmp = 1;
|
||||
obj.dtfcltCpct = item.dtfcltCpct;
|
||||
obj.wtrUsqty = item.wtrUsqty;
|
||||
obj.wtrUsqty = 0;
|
||||
obj.anlOperCst = item.anlOperCst;
|
||||
obj.lndwtr = 0; //조경용수
|
||||
obj.cleanToilUswtr = 0; //청소화장실용수
|
||||
@@ -138,6 +138,15 @@ export const rainStatActionsExport = (
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
obj.wtrUsqty =
|
||||
obj.lndwtr +
|
||||
obj.cleanToilUswtr +
|
||||
obj.resn +
|
||||
obj.farmUswtr +
|
||||
obj.indstUswtr +
|
||||
obj.hydUswtr +
|
||||
obj.ithlEtcUswtr;
|
||||
});
|
||||
|
||||
const totalObj = {
|
||||
@@ -145,7 +154,7 @@ export const rainStatActionsExport = (
|
||||
tmp: 0,
|
||||
instlCst: 0, //설치비
|
||||
dtfcltCpct: 0, //저류조용량
|
||||
bdstUsgCd: 0, //연간사용량
|
||||
wtrUsqty: 0, //연간사용량
|
||||
lndwtr: 0, //조경용수
|
||||
cleanToilUswtr: 0, //청소화장실용수
|
||||
resn: 0, //세척살수용수
|
||||
@@ -165,8 +174,8 @@ export const rainStatActionsExport = (
|
||||
totalObj.dtfcltCpct += !isNaN(Number(item.dtfcltCpct))
|
||||
? Number(item.dtfcltCpct)
|
||||
: 0;
|
||||
totalObj.bdstUsgCd += !isNaN(Number(item.bdstUsgCd))
|
||||
? Number(item.bdstUsgCd)
|
||||
totalObj.wtrUsqty += !isNaN(Number(item.wtrUsqty))
|
||||
? Number(item.wtrUsqty)
|
||||
: 0;
|
||||
totalObj.lndwtr += !isNaN(Number(item.lndwtr))
|
||||
? Number(item.lndwtr)
|
||||
@@ -196,7 +205,7 @@ export const rainStatActionsExport = (
|
||||
tmp: item.tmp,
|
||||
instlCst: item.instlCst, //설치비
|
||||
dtfcltCpct: item.dtfcltCpct, //저류조용량
|
||||
bdstUsgCd: item.bdstUsgCd, //연간사용량
|
||||
wtrUsqty: item.wtrUsqty, //연간사용량
|
||||
lndwtr: item.lndwtr, //조경용수
|
||||
cleanToilUswtr: item.cleanToilUswtr, //청소화장실용수
|
||||
resn: item.resn, //세척살수용수
|
||||
@@ -208,17 +217,30 @@ export const rainStatActionsExport = (
|
||||
});
|
||||
});
|
||||
|
||||
cityArr.forEach((item) => {
|
||||
item.instlCst = Math.round(Number(item.instlCst) / 1000000);
|
||||
item.anlOperCst = Math.round(Number(item.anlOperCst) / 1000000);
|
||||
});
|
||||
|
||||
totalObj.instlCst = Math.round(Number(totalObj.instlCst) / 1000000);
|
||||
totalObj.anlOperCst = Math.round(Number(totalObj.anlOperCst) / 1000000);
|
||||
|
||||
|
||||
//상세
|
||||
const rainDetailList: RainStatProps[] = [];
|
||||
res.forEach((item: RainStatProps) => {
|
||||
const utztnUsgArray = new Array<string>();
|
||||
let wtrUsqty = 0;
|
||||
item.guideStats.forEach((guideStat) => {
|
||||
utztnUsgArray.push(guideStat.utztnUsgCd);
|
||||
wtrUsqty += Number(guideStat.ruseFcltUtztnQnt) || 0;
|
||||
});
|
||||
rainDetailList.push({
|
||||
...item,
|
||||
instlCst: Math.round(Number(item.instlCst) / 1000000),
|
||||
wtrUsqty,
|
||||
utztnUsgCd: utztnUsgArray.join(", "),
|
||||
anlOperCst: Math.round(Number(item.anlOperCst) / 1000000),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,50 +1,53 @@
|
||||
import type { SelectDataType } from "@common/business_components/ui";
|
||||
import { codeToText } from "@/components/lib/func/common";
|
||||
import type { RswtStatProps } from ".";
|
||||
|
||||
export const excelRswtStat = (excelList: RswtStatProps[], rswtBdstType : SelectDataType[] ) : (string | number)[][] => {
|
||||
const toUseTypeText = (val: string) =>
|
||||
val
|
||||
.split(",")
|
||||
.map((code) => code.trim())
|
||||
.filter(Boolean)
|
||||
.map((code) => codeToText(code, "dmdUszCd") ?? code)
|
||||
.join(", ");
|
||||
|
||||
|
||||
console.log('bdstUsgCd ', rswtBdstType)
|
||||
export const excelRswtStat = (
|
||||
excelList: RswtStatProps[]
|
||||
): (string | number)[][] => {
|
||||
const excelData: (string | number)[][] = [];
|
||||
excelList.forEach((item) => {
|
||||
excelData.push([
|
||||
//시도
|
||||
String(item.ctpvCd) ?? "",
|
||||
//시군구
|
||||
String(item.sggCd) ?? "",
|
||||
//행정구역명
|
||||
String(item.areaNm) ?? "",
|
||||
//건축물명
|
||||
String(item.bdstNm) ?? "",
|
||||
//건축물용도코드(빈칸)
|
||||
//String(item.bdstUsgCd) ?? "",
|
||||
rswtBdstType?.find((cd)=> cd.val === item.bdstUsgCd.toString())?.text ?? "", //건축물용도
|
||||
//주소
|
||||
String(item.daddr) ?? "",
|
||||
//법적의무대상여부
|
||||
String(item.lawTrgtYn)==="Y" ? "대상" : "미대상",
|
||||
//설치완료일자
|
||||
item.instlCmptnDt ? String(item.instlCmptnDt).substring(0, 10) : "",
|
||||
//가동개시일(빈칸)
|
||||
//String(item.instlCst) ?? "",
|
||||
"",
|
||||
//건축물면적
|
||||
// item.archArea ?? "",
|
||||
//처리용량
|
||||
item.filtrTrtmfctCpct ?? "",
|
||||
//물사용량
|
||||
item.wtrUsqty ?? "",
|
||||
//중수도+대체수이용량
|
||||
item.rswtAltSum ?? "",
|
||||
//중수도이용량
|
||||
item.rswtrUsqty ?? "",
|
||||
//대체수이용량
|
||||
item.altUswtrUtztnQnt ?? "",
|
||||
//중수도용도
|
||||
String(item.utztnUsgCd) ?? "",
|
||||
//연간운영비
|
||||
item.anlOperCst ?? "",
|
||||
]);
|
||||
});
|
||||
excelList.forEach((item) => {
|
||||
excelData.push([
|
||||
//시도
|
||||
String(item.ctpvCd) ?? "",
|
||||
//시군구
|
||||
String(item.sggCd) ?? "",
|
||||
//행정구역명
|
||||
String(item.areaNm) ?? "",
|
||||
//건축물명
|
||||
String(item.bdstNm) ?? "",
|
||||
//건축물용도코드
|
||||
codeToText(String(item.bdstUsgCd ?? ""), "rswtBdstType") ?? String(item.bdstUsgCd ?? ""),
|
||||
//주소
|
||||
String(item.daddr) ?? "",
|
||||
//법적의무대상여부
|
||||
String(item.lawTrgtYn) === "Y" ? "대상" : "미대상",
|
||||
//설치완료일자
|
||||
item.instlCmptnDt ? String(item.instlCmptnDt).substring(0, 10) : "",
|
||||
//가동개시일(빈칸)
|
||||
"",
|
||||
//처리용량
|
||||
item.filtrTrtmfctCpct ?? "",
|
||||
//물사용량
|
||||
// item.wtrUsqty ?? "",
|
||||
//중수도+대체수이용량
|
||||
item.rswtAltSum ?? "",
|
||||
//중수도이용량
|
||||
item.rswtrUsqty ?? "",
|
||||
//대체수이용량
|
||||
item.altUswtrUtztnQnt ?? "",
|
||||
//중수도용도
|
||||
toUseTypeText(String(item.utztnUsgCd ?? "")),
|
||||
//연간운영비
|
||||
item.anlOperCst ?? "",
|
||||
]);
|
||||
});
|
||||
return excelData;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,12 +30,12 @@ export const rswtStatHeadListExport :TableUpperProps[]=[
|
||||
row:0,
|
||||
rowSpan:2
|
||||
},
|
||||
{
|
||||
id: "wtrUsqty",
|
||||
title: "물사용량(m³/년)",
|
||||
row:0,
|
||||
rowSpan:2
|
||||
},
|
||||
// {
|
||||
// id: "wtrUsqty",
|
||||
// title: "물사용량(m³/년)",
|
||||
// row:0,
|
||||
// rowSpan:2
|
||||
// },
|
||||
{
|
||||
id: "rswtAltSum",
|
||||
title: "중수도+대체수이용량(m³/년)",
|
||||
@@ -105,4 +105,4 @@ export const rswtStatHeadListExport :TableUpperProps[]=[
|
||||
},
|
||||
|
||||
|
||||
]
|
||||
]
|
||||
|
||||
@@ -35,6 +35,7 @@ export type RswtStatProps = {
|
||||
|
||||
export type GuideStatProps = {
|
||||
utztnUsgCd: string
|
||||
splyUswtrUtztnQnt: number
|
||||
ruseFcltUtztnQnt: number
|
||||
}
|
||||
|
||||
@@ -73,14 +74,22 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = (
|
||||
set: any
|
||||
) => {
|
||||
return {
|
||||
searchRswtStatList: async (year: string, city: SelectDataType[] , rswtBdstType:SelectDataType[]) => {
|
||||
searchRswtStatList: async (year: string, city: SelectDataType[]) => {
|
||||
const res = await getFetch(fetchType.WR, `/reuseChk/reuseWater/stat/${year}`)
|
||||
const data = res.filter(item => {
|
||||
const code = item.ctpvCd.trim();
|
||||
return city.some(el => el.val === code);
|
||||
});
|
||||
const cityArr: any[] = [];
|
||||
excelDownList.length = 0;
|
||||
data.forEach((item: RswtStatProps) => {
|
||||
const itemAltUswtrUtztnQnt =
|
||||
item.guideStats && item.guideStats.length > 0
|
||||
? item.guideStats.reduce(
|
||||
(sum, guide) => sum + (Number(guide.splyUswtrUtztnQnt) || 0),
|
||||
0
|
||||
)
|
||||
: Number(item.altUswtrUtztnQnt) || 0;
|
||||
let obj = cityArr.find((e) => {
|
||||
return e.ctpvCd == item.ctpvCd
|
||||
})
|
||||
@@ -90,9 +99,7 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = (
|
||||
// obj.archArea += item.archArea 건축면적 삭제
|
||||
obj.filtrTrtmfctCpct += item.filtrTrtmfctCpct //처리용량
|
||||
obj.wtrUsqty += item.wtrUsqty //물사용량
|
||||
obj.rswtAltSum += (item.rswtrUsqty + item.altUswtrUtztnQnt) //중수도+대체수
|
||||
obj.rswtrUsqty += item.rswtrUsqty //중수도사용량
|
||||
obj.altUswtrUtztnQnt += item.altUswtrUtztnQnt //대체수이용량
|
||||
obj.altUswtrUtztnQnt += itemAltUswtrUtztnQnt //대체수이용량
|
||||
obj.anlOperCst += item.anlOperCst //연간운영비용
|
||||
} else {
|
||||
obj = {
|
||||
@@ -100,9 +107,9 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = (
|
||||
ctpvCnt: 1,
|
||||
filtrTrtmfctCpct: item.filtrTrtmfctCpct,
|
||||
wtrUsqty: item.wtrUsqty,
|
||||
rswtAltSum: item.rswtrUsqty + item.altUswtrUtztnQnt,
|
||||
rswtrUsqty: item.rswtrUsqty,
|
||||
altUswtrUtztnQnt: item.altUswtrUtztnQnt,
|
||||
rswtAltSum: 0,
|
||||
rswtrUsqty: 0,
|
||||
altUswtrUtztnQnt: itemAltUswtrUtztnQnt,
|
||||
anlOperCst: item.anlOperCst,
|
||||
lndwtr: 0,
|
||||
cleanToilUswtr: 0,
|
||||
@@ -127,6 +134,15 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = (
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
obj.rswtrUsqty =
|
||||
obj.lndwtr +
|
||||
obj.cleanToilUswtr +
|
||||
obj.farmUswtr +
|
||||
obj.indstUswtr +
|
||||
obj.hydUswtr +
|
||||
obj.ithlEtcUswtr;
|
||||
obj.rswtAltSum = obj.rswtrUsqty + obj.altUswtrUtztnQnt;
|
||||
});
|
||||
|
||||
const total: any = {
|
||||
@@ -163,6 +179,11 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = (
|
||||
total.ithlEtcUswtr += item.ithlEtcUswtr;
|
||||
});
|
||||
|
||||
cityArr.forEach(item => {
|
||||
item.anlOperCst = Math.round(Number(item.anlOperCst) / 1000000);
|
||||
});
|
||||
total.anlOperCst = Math.round(Number(total.anlOperCst) / 1000000);
|
||||
|
||||
cityArr.unshift(total);
|
||||
|
||||
cityArr.forEach(item => {
|
||||
@@ -189,16 +210,23 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = (
|
||||
const rswtDetailList: RswtStatProps[] = [];
|
||||
res.forEach((item: RswtStatProps) => {
|
||||
const utztnUsgArray = new Array<string>();
|
||||
let rswtrUsqty = 0;
|
||||
let altUswtrUtztnQnt = 0;
|
||||
item.guideStats.forEach((guideStat) => {
|
||||
utztnUsgArray.push(guideStat.utztnUsgCd);
|
||||
rswtrUsqty += Number(guideStat.ruseFcltUtztnQnt) || 0;
|
||||
altUswtrUtztnQnt += Number(guideStat.splyUswtrUtztnQnt) || 0;
|
||||
})
|
||||
rswtDetailList.push({
|
||||
...item,
|
||||
rswtAltSum: item.rswtrUsqty + item.altUswtrUtztnQnt,
|
||||
rswtAltSum: rswtrUsqty + altUswtrUtztnQnt,
|
||||
rswtrUsqty,
|
||||
altUswtrUtztnQnt,
|
||||
utztnUsgCd: utztnUsgArray.join(', '),
|
||||
anlOperCst: Math.round(Number(item.anlOperCst) / 1000000),
|
||||
})
|
||||
});
|
||||
const excelDetailData = excelRswtStat(rswtDetailList,rswtBdstType);
|
||||
const excelDetailData = excelRswtStat(rswtDetailList);
|
||||
set((state: rswtStatState) => ({
|
||||
rswtStat: {
|
||||
...state.rswtStat,
|
||||
@@ -209,4 +237,4 @@ export const rswtStatActionsExport: (set: any) => rswtStatActions = (
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "../config/";
|
||||
import { CLAIM_NAME, SITE_ID } from "@/src/shared/config";
|
||||
import { tokenManager } from "@/src/entities/auth/lib/token-manager";
|
||||
|
||||
export const fetchType = {
|
||||
COMMON: "common",
|
||||
@@ -7,6 +9,33 @@ export const fetchType = {
|
||||
CS: "CS",
|
||||
};
|
||||
|
||||
const LOGIN_PAGE_URL =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
|
||||
const getDefaultHeaders = (headers: HeadersInit = {}) => ({
|
||||
...headers,
|
||||
"X-Site-Id": SITE_ID,
|
||||
});
|
||||
|
||||
const handleUnauthorizedResponse = () => {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME);
|
||||
const isExpired = remainingMs === null || remainingMs <= 0;
|
||||
|
||||
if (!isExpired) return;
|
||||
|
||||
tokenManager.clearAuthToken();
|
||||
|
||||
const isLoginPage =
|
||||
window.location.pathname === "/auth/login" ||
|
||||
window.location.pathname.startsWith("/auth/login/");
|
||||
|
||||
if (!isLoginPage) {
|
||||
window.location.assign(LOGIN_PAGE_URL);
|
||||
}
|
||||
};
|
||||
|
||||
export const getFetch = async (type: string, url: string) => {
|
||||
const div =
|
||||
type === fetchType.COMMON
|
||||
@@ -19,11 +48,15 @@ export const getFetch = async (type: string, url: string) => {
|
||||
const apiUrl = div + url;
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
headers: getDefaultHeaders({
|
||||
accept: "application/json",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -67,11 +100,15 @@ export const getFetchWithCount = async (type: string, url: string) => {
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
headers: getDefaultHeaders({
|
||||
accept: "application/json",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -108,12 +145,15 @@ export const postFectch = async (url: string, params: any, type?: string) => {
|
||||
const apiUrl = div + url;
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
headers: getDefaultHeaders({
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -133,11 +173,14 @@ export const putFectch = async (url: string, params: any, type?: string) => {
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
headers: getDefaultHeaders({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`오류 발생: ${response.status}`);
|
||||
}
|
||||
@@ -161,11 +204,14 @@ export const deleteFectch = async (
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
headers: getDefaultHeaders({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
body: params ? JSON.stringify(params) : undefined,
|
||||
});
|
||||
if (response.status === 401) {
|
||||
handleUnauthorizedResponse();
|
||||
}
|
||||
if (!response.ok) {
|
||||
// 에러 응답 본문 파싱
|
||||
const errorText = await response.text();
|
||||
|
||||
@@ -2,13 +2,18 @@ import { numSize } from "@/app/store/common/code/basicCode";
|
||||
import type { TableUpperProps } from "@/app/store/common/type/header";
|
||||
import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn";
|
||||
import type { rswtChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn";
|
||||
import { Checkbox, Input, TableCell, TableRow } from "@common/components/ui";
|
||||
import {
|
||||
Checkbox,
|
||||
Input,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@common/components/ui";
|
||||
import moment from "moment";
|
||||
import {
|
||||
codeToText,
|
||||
formatCommaNumber,
|
||||
formatYyyymmdd,
|
||||
handleCommaNumberChange,
|
||||
handleCommaNumberChange
|
||||
} from "./common";
|
||||
|
||||
export const rainChkTableData = (chkData: rainChkZodType[]) => {
|
||||
@@ -68,10 +73,10 @@ export const rswtChkTableData = (chkData: rswtChkZodType[]) => {
|
||||
root.rswtrDetails.forEach((detail) => {
|
||||
let flatRswtChk = {};
|
||||
const findGuide = root.guideDetails.find(
|
||||
(guide) => guide.utztnUsgCd === detail.utztnUsgCd,
|
||||
);
|
||||
(guide) => guide.utztnUsgCd === detail.utztnUsgCd
|
||||
)
|
||||
const findPoluttion = root.rswtrPollutions.find(
|
||||
(polution) => polution.utztnUsgCd === detail.utztnUsgCd,
|
||||
(polution) => polution.utztnUsgCd === detail.utztnUsgCd
|
||||
);
|
||||
flatRswtChk = {
|
||||
...root,
|
||||
@@ -79,7 +84,7 @@ export const rswtChkTableData = (chkData: rswtChkZodType[]) => {
|
||||
...findGuide,
|
||||
...findPoluttion,
|
||||
...flatRswtChk,
|
||||
utztnUsgCdText: codeToText(detail.utztnUsgCd, "dmdUszCd"),
|
||||
utztnUsgCdText: codeToText(detail.utztnUsgCd, 'dmdUszCd'),
|
||||
};
|
||||
makeRswtData.push(flatRswtChk);
|
||||
});
|
||||
@@ -96,17 +101,18 @@ export const chRowspanTable = (
|
||||
handleChange: (
|
||||
rowIndex: number,
|
||||
field: keyof (typeof makeTableData)[0],
|
||||
value: string | number,
|
||||
value: string | number
|
||||
) => void,
|
||||
toggleSelection: (pk: number, checked: boolean) => void,
|
||||
selectedIds: number[],
|
||||
handleCellClick: (index: number, headId: string) => void,
|
||||
disable?: boolean,
|
||||
disable?: boolean
|
||||
) => {
|
||||
let order = 0;
|
||||
// rowspan된 셀 렌더링 여부 추적용
|
||||
const renderedMap = new Map<string, Set<number>>();
|
||||
|
||||
// console.log(makeTableData);
|
||||
return (
|
||||
<>
|
||||
{/* {makeTableData.map((row, rowIndex) => ( */}
|
||||
@@ -135,11 +141,7 @@ export const chRowspanTable = (
|
||||
key={key}
|
||||
rowSpan={rowspan}
|
||||
className="align-middle text-center whitespace-normal break-words"
|
||||
style={{
|
||||
width: head.widthPercent
|
||||
? `${head.widthPercent}%`
|
||||
: undefined,
|
||||
}}
|
||||
style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }}
|
||||
>
|
||||
{key === "checkBox" ? (
|
||||
<Checkbox
|
||||
@@ -152,16 +154,16 @@ export const chRowspanTable = (
|
||||
) : key === "order" ? (
|
||||
++order
|
||||
) : // value
|
||||
["anlOperCst", "dtfcltCpct", "prcsCpct"].includes(
|
||||
//연간운영비, 저류조용량, 처리용량
|
||||
head.id,
|
||||
["anlOperCst", "dtfcltCpct", "prcsCpct"].includes( //연간운영비, 저류조용량, 처리용량
|
||||
head.id
|
||||
) ? (
|
||||
formatCommaNumber(value)
|
||||
) : key === "chckYmd" ? (
|
||||
<>{formatYyyymmdd(value)}</>
|
||||
) : (
|
||||
<>{value}</>
|
||||
)}
|
||||
formatCommaNumber(value)
|
||||
) : key === "chckYmd" ? (
|
||||
<>{formatYyyymmdd(value)}</>
|
||||
) :
|
||||
(
|
||||
<>{value}</>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
}
|
||||
@@ -172,11 +174,7 @@ export const chRowspanTable = (
|
||||
key={key}
|
||||
className="text-center whitespace-normal break-words"
|
||||
onClick={() => handleCellClick(rowIndex, head.id)}
|
||||
style={{
|
||||
width: head.widthPercent
|
||||
? `${head.widthPercent}%`
|
||||
: undefined,
|
||||
}}
|
||||
style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }}
|
||||
>
|
||||
{head.edit === true ? (
|
||||
[
|
||||
@@ -190,7 +188,7 @@ export const chRowspanTable = (
|
||||
type={head.type}
|
||||
onChange={(e) => {
|
||||
handleCommaNumberChange(e, (val) =>
|
||||
handleChange(rowIndex, head.id, val),
|
||||
handleChange(rowIndex, head.id, val)
|
||||
);
|
||||
}}
|
||||
value={formatCommaNumber(value) || ""}
|
||||
@@ -198,10 +196,7 @@ export const chRowspanTable = (
|
||||
maxLength={numSize}
|
||||
onInput={(e: React.FormEvent<HTMLInputElement>) => {
|
||||
const el = e.currentTarget;
|
||||
if (
|
||||
head.type === "number" &&
|
||||
el.value.length > numSize
|
||||
) {
|
||||
if (head.type === "number" && el.value.length > numSize) {
|
||||
el.value = el.value.slice(0, numSize);
|
||||
}
|
||||
}}
|
||||
@@ -234,10 +229,7 @@ export const chRowspanTable = (
|
||||
maxLength={numSize}
|
||||
onInput={(e: React.FormEvent<HTMLInputElement>) => {
|
||||
const el = e.currentTarget;
|
||||
if (
|
||||
head.type === "number" &&
|
||||
el.value.length > numSize
|
||||
) {
|
||||
if (head.type === "number" && el.value.length > numSize) {
|
||||
el.value = el.value.slice(0, numSize);
|
||||
}
|
||||
}}
|
||||
@@ -246,12 +238,12 @@ export const chRowspanTable = (
|
||||
) : ["smll"].includes(head.id) && head.type === "text" ? (
|
||||
<>-</>
|
||||
) : [
|
||||
"totalSplyUswtrUtztnQnt",
|
||||
"totalRuseFcltUtztnQnt",
|
||||
"splyUswtrUtztnQnt",
|
||||
"ruseFcltUtztnQnt",
|
||||
"levyAmt",
|
||||
].includes(head.id) ? (
|
||||
"totalSplyUswtrUtztnQnt",
|
||||
"totalRuseFcltUtztnQnt",
|
||||
"splyUswtrUtztnQnt",
|
||||
"ruseFcltUtztnQnt",
|
||||
"levyAmt",
|
||||
].includes(head.id) ? (
|
||||
<span
|
||||
className={
|
||||
[
|
||||
@@ -290,7 +282,7 @@ export const chRowspanTable = (
|
||||
|
||||
export const headVisibleChange = (
|
||||
headList: TableUpperProps[],
|
||||
isEdit: boolean,
|
||||
isEdit: boolean
|
||||
) => {
|
||||
return headList.forEach((head) => {
|
||||
if (head.edit !== undefined) head.edit = isEdit;
|
||||
@@ -304,7 +296,7 @@ const handleChange = (
|
||||
rowIndex: number,
|
||||
field: any,
|
||||
value: string,
|
||||
setInputData: (data: any) => void,
|
||||
setInputData: (data: any) => void
|
||||
) => {
|
||||
if (Number(value) < 0 || !Number(value)) {
|
||||
value = "0";
|
||||
@@ -318,16 +310,36 @@ const handleChange = (
|
||||
};
|
||||
}
|
||||
return row;
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleTextChange = (
|
||||
rowIndex: number,
|
||||
field: any,
|
||||
value: string,
|
||||
setInputData: (data: any) => void
|
||||
) => {
|
||||
setInputData((prevData: any) =>
|
||||
prevData?.map((row: any, index: number) => {
|
||||
if (index === rowIndex) {
|
||||
return {
|
||||
...row,
|
||||
[field]: value,
|
||||
};
|
||||
}
|
||||
return row;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// 재이용실적등록 > 지도점검결과 등록 - 상세 modal의 body
|
||||
export const InputTableBody = (
|
||||
chgRes: any[] | [],
|
||||
headList: TableUpperProps[],
|
||||
setInputData: (data: any[]) => void,
|
||||
isEditable: boolean = true,
|
||||
isEditable: boolean = true
|
||||
) => {
|
||||
// mergeCell 처리용
|
||||
const getRowSpan = (data: any[], key: string, rowIndex: number) => {
|
||||
@@ -357,28 +369,14 @@ export const InputTableBody = (
|
||||
return (
|
||||
<TableRow key={dataIndex + "row"}>
|
||||
{headList.map((head, headIndex) => {
|
||||
//냄새면 계산 못하게 - 로 수정
|
||||
// if (["smll"].includes(head.id)) {
|
||||
// return (
|
||||
// <TableCell
|
||||
// key={head.id + headIndex}
|
||||
// className={`w-36 ${bgClass} text-center !text-xl`}
|
||||
// >
|
||||
// <span>-</span>
|
||||
// </TableCell>
|
||||
// );
|
||||
// }
|
||||
const isTextColumn =
|
||||
head.id === "구분" || head.id === "utztnUsgCdText";
|
||||
|
||||
const isTextColumn = head.id === "구분" || head.id === "utztnUsgCdText";
|
||||
const textAlignClass = isTextColumn
|
||||
? "text-center"
|
||||
: "text-right";
|
||||
|
||||
// 총합계 행이면 "구분"에서 colSpan=2로 병합
|
||||
if (
|
||||
(data["구분"] === 0 || data["용수명"] === 0) &&
|
||||
head.id === "구분"
|
||||
) {
|
||||
if ( (data["구분"] === 0 || data["용수명"] === 0) && head.id === "구분" ) {
|
||||
return (
|
||||
<TableCell
|
||||
key={head.id + headIndex}
|
||||
@@ -390,10 +388,7 @@ export const InputTableBody = (
|
||||
);
|
||||
}
|
||||
// 총합계(합계) 행이면 "용수명" 컬럼은 렌더링하지 않음
|
||||
if (
|
||||
(data["구분"] === 0 || data["용수명"] === 0) &&
|
||||
head.id === "용수명"
|
||||
) {
|
||||
if ( (data["구분"] === 0 || data["용수명"] === 0) && head.id === "용수명" ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -433,22 +428,39 @@ export const InputTableBody = (
|
||||
key={head.id + headIndex}
|
||||
className={`w-36 ${bgClass}`}
|
||||
>
|
||||
<Input
|
||||
// className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`}
|
||||
className={`${textAlignClass} !text-xl h-10 w-full`}
|
||||
onChange={(e) =>
|
||||
handleCommaNumberChange(e, (val) =>
|
||||
handleChange(
|
||||
{head.id === "smll" ? (
|
||||
<Input
|
||||
className="text-center !text-xl h-10 w-full"
|
||||
type="text"
|
||||
onChange={(e) =>
|
||||
handleTextChange(
|
||||
dataIndex,
|
||||
head.id,
|
||||
String(val),
|
||||
setInputData,
|
||||
),
|
||||
)
|
||||
}
|
||||
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
|
||||
maxLength={numSize}
|
||||
/>
|
||||
e.target.value,
|
||||
setInputData
|
||||
)
|
||||
}
|
||||
value={String(data[head.id as keyof any] ?? "")}
|
||||
maxLength={numSize}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
// className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`}
|
||||
className={`${textAlignClass} !text-xl h-10 w-full`}
|
||||
onChange={(e) =>
|
||||
handleCommaNumberChange(e, (val) =>
|
||||
handleChange(
|
||||
dataIndex,
|
||||
head.id,
|
||||
String(val),
|
||||
setInputData
|
||||
)
|
||||
)
|
||||
}
|
||||
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
|
||||
maxLength={numSize}
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
) : (
|
||||
<TableCell
|
||||
@@ -485,7 +497,7 @@ export const pbnstTableBody = (
|
||||
chgRes: any[] | [],
|
||||
headList: TableUpperProps[],
|
||||
setInputData: (data: any[]) => void,
|
||||
isEditable: boolean = true,
|
||||
isEditable: boolean = true
|
||||
) => {
|
||||
// mergeCell 처리용
|
||||
const getRowSpan = (data: any[], key: string, rowIndex: number) => {
|
||||
@@ -599,8 +611,8 @@ export const pbnstTableBody = (
|
||||
dataIndex,
|
||||
head.id,
|
||||
String(val),
|
||||
setInputData,
|
||||
),
|
||||
setInputData
|
||||
)
|
||||
)
|
||||
}
|
||||
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
|
||||
|
||||
@@ -192,7 +192,7 @@ export default function ChkgdExcel({
|
||||
}
|
||||
})
|
||||
.finally(() => setRswtChkLoading(false));
|
||||
console.log("업로드파일 점검 완료");
|
||||
// console.log("업로드파일 점검 완료");
|
||||
}
|
||||
}
|
||||
}, [file]);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -20,6 +20,10 @@ export class LoginApiService implements LoginService {
|
||||
tokenManager.clearAuthToken();
|
||||
const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: payload.userId,
|
||||
password: payload.password,
|
||||
@@ -52,7 +56,12 @@ export class LoginApiService implements LoginService {
|
||||
try {
|
||||
const response = await fetch(PROXY_HOST+
|
||||
`${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
|
||||
{ method: "PUT" },
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-Site-Id": SITE_ID,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
|
||||
import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie";
|
||||
|
||||
type JwtPayload = {
|
||||
sub?: string; // 토큰 subject
|
||||
exp?: number; // 만료 시각(초 단위)
|
||||
iat?: number; // 발급 시각(초 단위)
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export class TokenManager {
|
||||
private static instance: TokenManager;
|
||||
private refreshTimer: NodeJS.Timeout | null = null;
|
||||
@@ -18,6 +25,49 @@ export class TokenManager {
|
||||
return getCookie(key);
|
||||
}
|
||||
|
||||
private parseJwt(token: string): JwtPayload {
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("유효한 JWT 형식이 아닙니다.");
|
||||
}
|
||||
|
||||
const base64Url = parts[1] ?? ""; // Payload를 가져온다.
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(padded)
|
||||
.split("")
|
||||
.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`)
|
||||
.join("")
|
||||
);
|
||||
|
||||
return JSON.parse(jsonPayload) as JwtPayload;
|
||||
}
|
||||
|
||||
public getRemainingMsFromToken(key: string = CLAIM_NAME): number | null {
|
||||
const token = this.getToken(key);
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.parseJwt(token);
|
||||
if (typeof payload.exp !== "number") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload.exp * 1000 - Date.now();
|
||||
} catch (error) {
|
||||
console.error("JWT 파싱 오류:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public isExpiredToken(key: string = CLAIM_NAME): boolean {
|
||||
const remainingMs = this.getRemainingMsFromToken(key);
|
||||
return remainingMs === null || remainingMs <= 0;
|
||||
}
|
||||
|
||||
public setToken(key: string, value: string): void {
|
||||
setCookie(key, value, {
|
||||
secure: window.location.protocol === "https:",
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { ACCESS_TOKEN, AUTH_USER_ID, REFRESH_TOKEN, CLAIM_NAME } from "@/src/shared/config";
|
||||
import { tokenManager } from "@/src/entities/auth/lib/token-manager";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
} from "@common/components";
|
||||
|
||||
interface OtpModalProps {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export default function OtpModal({ onClose, onSuccess, open, onOpenChange }: OtpModalProps) {
|
||||
const [otp, setOtp] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
const tempToken = localStorage.getItem("tempToken");
|
||||
console.log("tempToken from localStorage:", tempToken);
|
||||
|
||||
if (!tempToken) {
|
||||
setError("임시 토큰이 없습니다. 로그인을 다시 시도해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Proxy를 통해 OTP 인증 처리
|
||||
const response = await fetch(`/api/proxy/user-service/api/v1/auth/2fa/verify`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
otpCode: otp,
|
||||
tempToken
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
let errorMsg = errorData.message || 'OTP 인증 실패';
|
||||
if (Array.isArray(errorData.errors) && errorData.errors.length > 0) {
|
||||
errorMsg = errorData.errors.map((e: { message: string }) => e.message).join(', ');
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("OTP verification response:", data);
|
||||
console.log("Response headers:", Object.fromEntries(response.headers.entries()));
|
||||
console.log("Response data keys:", Object.keys(data));
|
||||
console.log("Full response data:", JSON.stringify(data, null, 2));
|
||||
|
||||
// 2FA 응답에서는 access-token만 있고 token-id는 없을 수 있음
|
||||
const accessToken = data[ACCESS_TOKEN] || data.accessToken || data.access_token || data.token || data['ss-token'];
|
||||
|
||||
console.log("Extracted accessToken:", !!accessToken);
|
||||
console.log("Token value - accessToken:", accessToken?.substring(0, 20) + "...");
|
||||
|
||||
// access-token이 있는 경우 저장
|
||||
if (accessToken) {
|
||||
console.log("Saving access token from OTP verification");
|
||||
tokenManager.setToken(CLAIM_NAME, accessToken); // Authorization으로 저장
|
||||
|
||||
// 리프레시 토큰도 확인
|
||||
const refreshToken = data[REFRESH_TOKEN] || data.refreshToken || data.refresh_token;
|
||||
if (refreshToken) {
|
||||
console.log("Saving refresh token");
|
||||
tokenManager.setToken(REFRESH_TOKEN, refreshToken);
|
||||
}
|
||||
|
||||
// userId는 원래 로그인 응답에서 가져온 값 사용
|
||||
const originalUserId = data.userId;
|
||||
if (originalUserId) {
|
||||
console.log("Saving userId from original login response");
|
||||
tokenManager.setToken(AUTH_USER_ID, originalUserId);
|
||||
}
|
||||
|
||||
console.log("All tokens saved successfully");
|
||||
} else {
|
||||
console.log("No access token in OTP response");
|
||||
console.log("Available data keys:", Object.keys(data));
|
||||
console.log("Data values:", Object.entries(data).map(([k, v]) => `${k}: ${typeof v === 'string' ? v.substring(0, 20) + '...' : v}`));
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
} catch (error: unknown) {
|
||||
console.error("OTP verification error:", error);
|
||||
if (error instanceof Error) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setError("OTP 인증 실패");
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<div className="pt-8 text-center">
|
||||
<h2 className="text-[1.3rem] font-semibold mb-6">OTP 인증</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-lg"
|
||||
placeholder="OTP 코드를 입력하세요"
|
||||
value={otp}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 text-sm text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-2 pt-4">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isLoading || !otp.trim()}
|
||||
className="flex-1"
|
||||
>
|
||||
{isLoading ? "인증 중..." : "인증"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,781 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import type { UserSaveData } from "@/src/entities/user";
|
||||
import { ENDPOINT, BASE_URL } from "@/src/shared/config";
|
||||
import { SignupClient } from "@/src/shared/api/signup-client";
|
||||
import "./signup-modal.css";
|
||||
|
||||
interface SignupModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
function SignupModal({ open, onClose, onSuccess }: SignupModalProps) {
|
||||
const [currentStep, setCurrentStep] = useState<"agreement" | "signup">(
|
||||
"agreement",
|
||||
);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
userId: "",
|
||||
userName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
pswdHnt: "",
|
||||
pswdAns: "",
|
||||
mphn: "",
|
||||
telno: "",
|
||||
zip: "",
|
||||
addr: "",
|
||||
daddr: "",
|
||||
laeSeCd: "",
|
||||
ogdpSeCd: "",
|
||||
expryYmd: "",
|
||||
instSeCd: "",
|
||||
dtlInstCd: "",
|
||||
enoCd: "",
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCheckingId, setIsCheckingId] = useState(false);
|
||||
const [isCheckingEmail, setIsCheckingEmail] = useState(false);
|
||||
const [idAvailable, setIdAvailable] = useState<boolean | null>(null);
|
||||
const [emailAvailable, setEmailAvailable] = useState<boolean | null>(null);
|
||||
|
||||
// 모달이 닫힐 때 초기화
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setCurrentStep("agreement");
|
||||
setAgreed(false);
|
||||
setFormData({
|
||||
userId: "",
|
||||
userName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
pswdHnt: "",
|
||||
pswdAns: "",
|
||||
mphn: "",
|
||||
telno: "",
|
||||
zip: "",
|
||||
addr: "",
|
||||
daddr: "",
|
||||
laeSeCd: "",
|
||||
ogdpSeCd: "",
|
||||
expryYmd: "",
|
||||
instSeCd: "",
|
||||
dtlInstCd: "",
|
||||
enoCd: "",
|
||||
});
|
||||
setErrors({});
|
||||
setIdAvailable(null);
|
||||
setEmailAvailable(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleAgreementChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAgreed(e.target.checked);
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (agreed) {
|
||||
setCurrentStep("signup");
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
|
||||
// 에러 초기화
|
||||
if (errors[name]) {
|
||||
setErrors((prev) => ({ ...prev, [name]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!formData.userId) newErrors.userId = "아이디를 입력해주세요.";
|
||||
if (!formData.userName) newErrors.userName = "이름을 입력해주세요.";
|
||||
if (!formData.password) newErrors.password = "비밀번호를 입력해주세요.";
|
||||
if (!formData.confirmPassword)
|
||||
newErrors.confirmPassword = "비밀번호 확인을 입력해주세요.";
|
||||
if (!formData.pswdHnt) newErrors.pswdHnt = "비밀번호 힌트를 입력해주세요.";
|
||||
if (!formData.laeSeCd) newErrors.laeSeCd = "법인구분을 선택해주세요.";
|
||||
if (!formData.ogdpSeCd) newErrors.ogdpSeCd = "기관구분을 선택해주세요.";
|
||||
|
||||
// 비밀번호 확인
|
||||
if (
|
||||
formData.password &&
|
||||
formData.confirmPassword &&
|
||||
formData.password !== formData.confirmPassword
|
||||
) {
|
||||
newErrors.confirmPassword = "비밀번호가 일치하지 않습니다.";
|
||||
}
|
||||
|
||||
// 비밀번호 정규식 검증
|
||||
const passwordRegex =
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/;
|
||||
if (formData.password && !passwordRegex.test(formData.password)) {
|
||||
newErrors.password =
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.";
|
||||
}
|
||||
|
||||
// 이메일 형식 검증
|
||||
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = "올바른 이메일 형식을 입력해주세요.";
|
||||
}
|
||||
|
||||
// 중복 체크 검증
|
||||
if (idAvailable === false)
|
||||
newErrors.userId = "이미 사용 중인 아이디입니다.";
|
||||
if (emailAvailable === false)
|
||||
newErrors.email = "이미 사용 중인 이메일입니다.";
|
||||
|
||||
setErrors(newErrors);
|
||||
const isValid = Object.keys(newErrors).length === 0;
|
||||
console.log("폼 검증 결과:", { isValid, errors: newErrors, formData });
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const checkUserIdDuplicate = async () => {
|
||||
if (!formData.userId) {
|
||||
setErrors((prev) => ({ ...prev, userId: "아이디를 입력해주세요." }));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingId(true);
|
||||
try {
|
||||
// SignupClient를 통해 중복 체크 API 호출
|
||||
const signupClient = SignupClient.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
const response = await signupClient.post<boolean>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/existsUserId`,
|
||||
{ userId: formData.userId },
|
||||
);
|
||||
|
||||
const exists = response.data;
|
||||
setIdAvailable(!exists);
|
||||
if (exists) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
userId: "이미 사용 중인 아이디입니다.",
|
||||
}));
|
||||
} else {
|
||||
setErrors((prev) => ({ ...prev, userId: "" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("아이디 중복 체크 오류:", error);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
userId: "아이디 중복 체크 중 오류가 발생했습니다.",
|
||||
}));
|
||||
} finally {
|
||||
setIsCheckingId(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkEmailDuplicate = async () => {
|
||||
if (!formData.email) {
|
||||
setErrors((prev) => ({ ...prev, email: "이메일을 입력해주세요." }));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingEmail(true);
|
||||
try {
|
||||
// SignupClient를 통해 중복 체크 API 호출
|
||||
const signupClient = SignupClient.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
const response = await signupClient.post<boolean>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/exists`,
|
||||
{ email: formData.email },
|
||||
);
|
||||
|
||||
const exists = response.data;
|
||||
setEmailAvailable(!exists);
|
||||
if (exists) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
email: "이미 사용 중인 이메일입니다.",
|
||||
}));
|
||||
} else {
|
||||
setErrors((prev) => ({ ...prev, email: "" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("이메일 중복 체크 오류:", error);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
email: "이메일 중복 체크 중 오류가 발생했습니다.",
|
||||
}));
|
||||
} finally {
|
||||
setIsCheckingEmail(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
console.log("회원가입 버튼 클릭됨");
|
||||
|
||||
if (!validateForm()) {
|
||||
console.log("폼 검증 실패");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const signupData: UserSaveData = {
|
||||
userId: formData.userId,
|
||||
userName: formData.userName,
|
||||
email: formData.email || undefined,
|
||||
password: formData.password,
|
||||
pswdHnt: formData.pswdHnt,
|
||||
pswdAns: formData.pswdAns || undefined,
|
||||
mphn: formData.mphn || undefined,
|
||||
telno: formData.telno || undefined,
|
||||
zip: formData.zip || undefined,
|
||||
addr: formData.addr || undefined,
|
||||
daddr: formData.daddr || undefined,
|
||||
laeSeCd: formData.laeSeCd,
|
||||
ogdpSeCd: formData.ogdpSeCd,
|
||||
expryYmd: "",
|
||||
instSeCd: formData.instSeCd || undefined,
|
||||
dtlInstCd: formData.dtlInstCd || undefined,
|
||||
enoCd: formData.enoCd || undefined,
|
||||
};
|
||||
|
||||
// SignupClient를 통해 익명사용자 회원가입 API 호출
|
||||
const signupClient = SignupClient.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
const response = await signupClient.post(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/anonymouse/users/join`,
|
||||
signupData,
|
||||
);
|
||||
|
||||
console.log("회원가입 성공:", response.data);
|
||||
|
||||
// 성공 시 모달 닫기 및 콜백 호출
|
||||
onSuccess();
|
||||
onClose();
|
||||
|
||||
// 폼 초기화
|
||||
setFormData({
|
||||
userId: "",
|
||||
userName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
pswdHnt: "",
|
||||
pswdAns: "",
|
||||
mphn: "",
|
||||
telno: "",
|
||||
zip: "",
|
||||
addr: "",
|
||||
daddr: "",
|
||||
laeSeCd: "",
|
||||
ogdpSeCd: "",
|
||||
expryYmd: "",
|
||||
instSeCd: "",
|
||||
dtlInstCd: "",
|
||||
enoCd: "",
|
||||
});
|
||||
setErrors({});
|
||||
setIdAvailable(null);
|
||||
setEmailAvailable(null);
|
||||
} catch (error: unknown) {
|
||||
console.error("회원가입 오류:", error);
|
||||
if (error instanceof Error) {
|
||||
setErrors((prev) => ({ ...prev, submit: error.message }));
|
||||
} else {
|
||||
setErrors((prev) => ({ ...prev, submit: "회원가입에 실패했습니다." }));
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h2>
|
||||
{currentStep === "agreement" ? "개인정보제공동의" : "회원가입"}
|
||||
</h2>
|
||||
<button className="close-button" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentStep === "agreement" ? (
|
||||
<div className="agreement-content">
|
||||
<div className="agreement-text">
|
||||
<h3>개인정보 수집 및 이용 동의</h3>
|
||||
<p>회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.</p>
|
||||
|
||||
<h4>1. 수집하는 개인정보 항목</h4>
|
||||
<ul>
|
||||
<li>
|
||||
필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분,
|
||||
기관구분
|
||||
</li>
|
||||
<li>
|
||||
선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드,
|
||||
상세기관코드, 사업자등록번호
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>2. 개인정보의 수집 및 이용목적</h4>
|
||||
<ul>
|
||||
<li>회원가입 및 관리</li>
|
||||
<li>서비스 제공 및 운영</li>
|
||||
<li>고객상담 및 문의응답</li>
|
||||
</ul>
|
||||
|
||||
<h4>3. 개인정보의 보유 및 이용기간</h4>
|
||||
<p>
|
||||
회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당
|
||||
기간까지)
|
||||
</p>
|
||||
|
||||
<h4>4. 동의 거부권 및 거부 시 불이익</h4>
|
||||
<p>
|
||||
개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부
|
||||
시 회원가입이 제한됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="agreement-checkbox">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreed}
|
||||
onChange={handleAgreementChange}
|
||||
/>
|
||||
<span>개인정보 수집 및 이용에 동의합니다.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="agreement-actions">
|
||||
<button type="button" onClick={onClose} className="cancel-button">
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNextStep}
|
||||
className="next-button"
|
||||
disabled={!agreed}
|
||||
>
|
||||
다음
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="signup-form">
|
||||
<div className="form-section">
|
||||
<h3>기본 정보</h3>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="userId">아이디 *</label>
|
||||
<div className="input-with-button">
|
||||
<input
|
||||
type="text"
|
||||
id="userId"
|
||||
name="userId"
|
||||
value={formData.userId}
|
||||
onChange={handleInputChange}
|
||||
placeholder="아이디를 입력하세요"
|
||||
maxLength={12}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={checkUserIdDuplicate}
|
||||
disabled={isCheckingId || isLoading || !formData.userId}
|
||||
className="check-button"
|
||||
>
|
||||
{isCheckingId ? "확인중..." : "중복확인"}
|
||||
</button>
|
||||
</div>
|
||||
{idAvailable === true && (
|
||||
<span className="success-message">
|
||||
사용 가능한 아이디입니다.
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span className="error-message">{errors.userId}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="userName">이름 *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="userName"
|
||||
name="userName"
|
||||
value={formData.userName}
|
||||
onChange={handleInputChange}
|
||||
placeholder="이름을 입력하세요"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.userName && (
|
||||
<span className="error-message">{errors.userName}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="email">이메일</label>
|
||||
<div className="input-with-button">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
placeholder="이메일을 입력하세요"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={checkEmailDuplicate}
|
||||
disabled={isCheckingEmail || isLoading || !formData.email}
|
||||
className="check-button"
|
||||
>
|
||||
{isCheckingEmail ? "확인중..." : "중복확인"}
|
||||
</button>
|
||||
</div>
|
||||
{emailAvailable === true && (
|
||||
<span className="success-message">
|
||||
사용 가능한 이메일입니다.
|
||||
</span>
|
||||
)}
|
||||
{errors.email && (
|
||||
<span className="error-message">{errors.email}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">비밀번호 *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder="8~20자, 영문/숫자/특수기호 포함"
|
||||
maxLength={20}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.password && (
|
||||
<span className="error-message">{errors.password}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="confirmPassword">비밀번호 확인 *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="비밀번호를 다시 입력하세요"
|
||||
maxLength={20}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<span className="error-message">
|
||||
{errors.confirmPassword}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="pswdHnt">비밀번호 힌트 *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="pswdHnt"
|
||||
name="pswdHnt"
|
||||
value={formData.pswdHnt}
|
||||
onChange={handleInputChange}
|
||||
placeholder="비밀번호 힌트를 입력하세요"
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.pswdHnt && (
|
||||
<span className="error-message">{errors.pswdHnt}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="pswdAns">비밀번호 답변</label>
|
||||
<input
|
||||
type="text"
|
||||
id="pswdAns"
|
||||
name="pswdAns"
|
||||
value={formData.pswdAns}
|
||||
onChange={handleInputChange}
|
||||
placeholder="비밀번호 답변을 입력하세요"
|
||||
maxLength={100}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.pswdAns && (
|
||||
<span className="error-message">{errors.pswdAns}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>연락처 정보</h3>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="mphn">휴대폰 번호</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="mphn"
|
||||
name="mphn"
|
||||
value={formData.mphn}
|
||||
onChange={handleInputChange}
|
||||
placeholder="휴대폰 번호를 입력하세요"
|
||||
maxLength={11}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.mphn && (
|
||||
<span className="error-message">{errors.mphn}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="telno">전화번호</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="telno"
|
||||
name="telno"
|
||||
value={formData.telno}
|
||||
onChange={handleInputChange}
|
||||
placeholder="전화번호를 입력하세요"
|
||||
maxLength={11}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.telno && (
|
||||
<span className="error-message">{errors.telno}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>주소 정보</h3>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="zip">우편번호</label>
|
||||
<input
|
||||
type="text"
|
||||
id="zip"
|
||||
name="zip"
|
||||
value={formData.zip}
|
||||
onChange={handleInputChange}
|
||||
placeholder="우편번호를 입력하세요"
|
||||
maxLength={5}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.zip && (
|
||||
<span className="error-message">{errors.zip}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="addr">주소</label>
|
||||
<input
|
||||
type="text"
|
||||
id="addr"
|
||||
name="addr"
|
||||
value={formData.addr}
|
||||
onChange={handleInputChange}
|
||||
placeholder="주소를 입력하세요"
|
||||
maxLength={200}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.addr && (
|
||||
<span className="error-message">{errors.addr}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="daddr">상세주소</label>
|
||||
<input
|
||||
type="text"
|
||||
id="daddr"
|
||||
name="daddr"
|
||||
value={formData.daddr}
|
||||
onChange={handleInputChange}
|
||||
placeholder="상세주소를 입력하세요"
|
||||
maxLength={200}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.daddr && (
|
||||
<span className="error-message">{errors.daddr}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>기관 정보</h3>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="laeSeCd">법인구분 *</label>
|
||||
<select
|
||||
id="laeSeCd"
|
||||
name="laeSeCd"
|
||||
value={formData.laeSeCd}
|
||||
onChange={handleInputChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<option value="">법인구분을 선택하세요</option>
|
||||
<option value="01">공공기관</option>
|
||||
<option value="02">민간기업</option>
|
||||
<option value="03">개인</option>
|
||||
</select>
|
||||
{errors.laeSeCd && (
|
||||
<span className="error-message">{errors.laeSeCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="ogdpSeCd">기관구분 *</label>
|
||||
<select
|
||||
id="ogdpSeCd"
|
||||
name="ogdpSeCd"
|
||||
value={formData.ogdpSeCd}
|
||||
onChange={handleInputChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<option value="">기관구분을 선택하세요</option>
|
||||
<option value="01">환경부</option>
|
||||
<option value="02">지방자치단체</option>
|
||||
<option value="03">공공기관</option>
|
||||
<option value="04">민간기업</option>
|
||||
</select>
|
||||
{errors.ogdpSeCd && (
|
||||
<span className="error-message">{errors.ogdpSeCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="instSeCd">기관코드</label>
|
||||
<input
|
||||
type="text"
|
||||
id="instSeCd"
|
||||
name="instSeCd"
|
||||
value={formData.instSeCd}
|
||||
onChange={handleInputChange}
|
||||
placeholder="기관코드를 입력하세요"
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.instSeCd && (
|
||||
<span className="error-message">{errors.instSeCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="dtlInstCd">상세기관코드</label>
|
||||
<input
|
||||
type="text"
|
||||
id="dtlInstCd"
|
||||
name="dtlInstCd"
|
||||
value={formData.dtlInstCd}
|
||||
onChange={handleInputChange}
|
||||
placeholder="상세기관코드를 입력하세요"
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.dtlInstCd && (
|
||||
<span className="error-message">{errors.dtlInstCd}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errors.submit && (
|
||||
<div className="form-row">
|
||||
<div className="error-message submit-error">
|
||||
{errors.submit}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentStep("agreement")}
|
||||
className="back-button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
이전
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="cancel-button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit-button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "가입 중..." : "회원가입"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignupModal;
|
||||
@@ -1,198 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
} from "@common/components";
|
||||
|
||||
interface TwoFAModalProps {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export default function TwoFAModal({ onClose, onSuccess, open, onOpenChange }: TwoFAModalProps) {
|
||||
const [qrImage, setQrImage] = useState("");
|
||||
const [secret, setSecret] = useState("");
|
||||
const [otp, setOtp] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSetupLoading, setIsSetupLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchQR = async () => {
|
||||
try {
|
||||
setIsSetupLoading(true);
|
||||
setError("");
|
||||
|
||||
const tempToken = localStorage.getItem("tempToken");
|
||||
if (!tempToken) {
|
||||
setError("임시 토큰이 없습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
// Proxy를 통해 2FA setup 처리
|
||||
const response = await fetch(`/api/proxy/user-service/api/v1/auth/2fa/setup`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tempToken }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
let errorMsg = errorData.message || 'QR코드 발급 실패';
|
||||
if (Array.isArray(errorData.errors) && errorData.errors.length > 0) {
|
||||
errorMsg = errorData.errors.map((e: { message: string }) => e.message).join(', ');
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("2FA setup response:", data);
|
||||
console.log("Response headers:", Object.fromEntries(response.headers.entries()));
|
||||
|
||||
// Proxy 응답에서 QR 데이터 추출
|
||||
setQrImage(data.qrImage);
|
||||
setSecret(data.secret);
|
||||
} catch (error: unknown) {
|
||||
console.error("2FA setup error:", error);
|
||||
if (error instanceof Error) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setError("QR코드 발급 실패");
|
||||
}
|
||||
} finally {
|
||||
setIsSetupLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
fetchQR();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleEnable2FA = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError("");
|
||||
|
||||
const tempToken = localStorage.getItem("tempToken");
|
||||
if (!tempToken) {
|
||||
setError("임시 토큰이 없습니다");
|
||||
return;
|
||||
}
|
||||
|
||||
// Proxy를 통해 2FA enable 처리
|
||||
const response = await fetch(`/api/proxy/user-service/api/v1/auth/2fa/enable`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': tempToken,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
otpCode: otp,
|
||||
tempToken: tempToken,
|
||||
secret: secret,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
let errorMsg = errorData.message || '2FA 등록 실패';
|
||||
if (Array.isArray(errorData.errors) && errorData.errors.length > 0) {
|
||||
errorMsg = errorData.errors.map((e: { message: string }) => e.message).join(', ');
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("2FA enable response:", data);
|
||||
console.log("Response headers:", Object.fromEntries(response.headers.entries()));
|
||||
console.log("Response data keys:", Object.keys(data));
|
||||
|
||||
// 2FA 설정 완료 후에는 토큰을 저장하지 않음 (실제 로그인은 OTP에서 처리)
|
||||
console.log("2FA setup completed successfully");
|
||||
|
||||
onSuccess();
|
||||
} catch (error: unknown) {
|
||||
console.error("2FA enable error:", error);
|
||||
if (error instanceof Error) {
|
||||
setError(error.message);
|
||||
} else {
|
||||
setError("2FA 등록 실패");
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onOpenChange(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<div className="pt-8 text-center">
|
||||
<h2 className="text-[1.3rem] font-semibold mb-6">2FA 등록</h2>
|
||||
|
||||
{isSetupLoading ? (
|
||||
<div className="py-8">
|
||||
<div className="text-gray-500">QR코드 로딩 중...</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{qrImage && (
|
||||
<div className="flex justify-center mb-4">
|
||||
<Image
|
||||
src={`data:image/png;base64,${qrImage}`}
|
||||
alt="QR코드"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<input
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-lg"
|
||||
placeholder="OTP 코드를 입력하세요"
|
||||
value={otp}
|
||||
onChange={(e) => setOtp(e.target.value)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 text-sm text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-2 pt-4">
|
||||
<Button
|
||||
onClick={handleEnable2FA}
|
||||
disabled={isLoading || !otp.trim()}
|
||||
className="flex-1"
|
||||
>
|
||||
{isLoading ? "2FA 활성화 중..." : "2FA 활성화"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={isLoading}
|
||||
className="flex-1"
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,901 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { SignupClient } from "@/src/shared/api/signup-client";
|
||||
import { BASE_URL, ENDPOINT } from "@/src/shared/config";
|
||||
import "./user-signup-modal.css";
|
||||
|
||||
interface UserSignupModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
interface UserSignupData {
|
||||
userId: string;
|
||||
userName: string;
|
||||
email?: string;
|
||||
password: string;
|
||||
pswdHnt: string;
|
||||
pswdAns?: string;
|
||||
mphn?: string;
|
||||
telno?: string;
|
||||
zip?: string;
|
||||
addr?: string;
|
||||
daddr?: string;
|
||||
laeSeCd: string;
|
||||
ogdpSeCd: string;
|
||||
instSeCd?: string;
|
||||
dtlInstCd?: string;
|
||||
enoCd?: string;
|
||||
}
|
||||
|
||||
// 익명 사용자용 CodeSelect 컴포넌트
|
||||
interface AnonymousCodeSelectProps {
|
||||
upCd: string;
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function AnonymousCodeSelect({
|
||||
upCd,
|
||||
value,
|
||||
onValueChange,
|
||||
disabled,
|
||||
placeholder = "선택",
|
||||
}: AnonymousCodeSelectProps) {
|
||||
const [codes, setCodes] = useState<
|
||||
Array<{ lwpoCd: string; lwpoCdNm: string }>
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCodes = async () => {
|
||||
if (!upCd) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const signupClient = SignupClient.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
|
||||
const response = await signupClient.get(
|
||||
`${ENDPOINT.CMS_SERVICE.CODE}/${upCd}`,
|
||||
);
|
||||
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
setCodes(response.data);
|
||||
} else {
|
||||
setCodes([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`공통 코드 로드 실패 (${upCd}):`, error);
|
||||
setCodes([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCodes();
|
||||
}, [upCd]);
|
||||
|
||||
return (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onValueChange(e.target.value)}
|
||||
disabled={disabled || isLoading}
|
||||
className="user-signup-select"
|
||||
>
|
||||
<option value="">{isLoading ? "로딩 중..." : placeholder}</option>
|
||||
{codes.map((code) => (
|
||||
<option key={code.lwpoCd} value={code.lwpoCd}>
|
||||
{code.lwpoCdNm}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function UserSignupModal({ open, onClose, onSuccess }: UserSignupModalProps) {
|
||||
const [currentStep, setCurrentStep] = useState<"agreement" | "signup">(
|
||||
"agreement",
|
||||
);
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
userId: "",
|
||||
userName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
pswdHnt: "",
|
||||
pswdAns: "",
|
||||
mphn: "",
|
||||
telno: "",
|
||||
zip: "",
|
||||
addr: "",
|
||||
daddr: "",
|
||||
laeSeCd: "",
|
||||
ogdpSeCd: "",
|
||||
instSeCd: "",
|
||||
dtlInstCd: "",
|
||||
enoCd: "",
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCheckingId, setIsCheckingId] = useState(false);
|
||||
const [isCheckingEmail, setIsCheckingEmail] = useState(false);
|
||||
const [idAvailable, setIdAvailable] = useState<boolean | null>(null);
|
||||
const [emailAvailable, setEmailAvailable] = useState<boolean | null>(null);
|
||||
|
||||
// 모달이 닫힐 때 초기화
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setCurrentStep("agreement");
|
||||
setAgreed(false);
|
||||
setFormData({
|
||||
userId: "",
|
||||
userName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
pswdHnt: "",
|
||||
pswdAns: "",
|
||||
mphn: "",
|
||||
telno: "",
|
||||
zip: "",
|
||||
addr: "",
|
||||
daddr: "",
|
||||
laeSeCd: "",
|
||||
ogdpSeCd: "",
|
||||
instSeCd: "",
|
||||
dtlInstCd: "",
|
||||
enoCd: "",
|
||||
});
|
||||
setErrors({});
|
||||
setIdAvailable(null);
|
||||
setEmailAvailable(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handleAgreementChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAgreed(e.target.checked);
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
if (agreed) {
|
||||
setCurrentStep("signup");
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
|
||||
// 에러 초기화
|
||||
if (errors[name]) {
|
||||
setErrors((prev) => ({ ...prev, [name]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors: Record<string, string> = {};
|
||||
|
||||
console.log("폼 검증 시작:", formData);
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!formData.userId) newErrors.userId = "아이디를 입력해주세요.";
|
||||
if (!formData.userName) newErrors.userName = "이름을 입력해주세요.";
|
||||
if (!formData.password) newErrors.password = "비밀번호를 입력해주세요.";
|
||||
if (!formData.confirmPassword)
|
||||
newErrors.confirmPassword = "비밀번호 확인을 입력해주세요.";
|
||||
if (!formData.pswdHnt) newErrors.pswdHnt = "비밀번호 힌트를 선택해주세요.";
|
||||
if (!formData.laeSeCd) newErrors.laeSeCd = "법인구분을 선택해주세요.";
|
||||
if (!formData.ogdpSeCd) newErrors.ogdpSeCd = "기관구분을 선택해주세요.";
|
||||
|
||||
// 비밀번호 확인
|
||||
if (
|
||||
formData.password &&
|
||||
formData.confirmPassword &&
|
||||
formData.password !== formData.confirmPassword
|
||||
) {
|
||||
newErrors.confirmPassword = "비밀번호가 일치하지 않습니다.";
|
||||
}
|
||||
|
||||
// 비밀번호 정규식 검증
|
||||
const passwordRegex =
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/;
|
||||
if (formData.password && !passwordRegex.test(formData.password)) {
|
||||
newErrors.password =
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.";
|
||||
}
|
||||
|
||||
// 이메일 형식 검증
|
||||
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = "올바른 이메일 형식을 입력해주세요.";
|
||||
}
|
||||
|
||||
// 중복 체크 검증
|
||||
if (idAvailable === false)
|
||||
newErrors.userId = "이미 사용 중인 아이디입니다.";
|
||||
if (emailAvailable === false)
|
||||
newErrors.email = "이미 사용 중인 이메일입니다.";
|
||||
|
||||
setErrors(newErrors);
|
||||
const isValid = Object.keys(newErrors).length === 0;
|
||||
console.log("폼 검증 결과:", { isValid, errors: newErrors, formData });
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const checkUserIdDuplicate = async () => {
|
||||
if (!formData.userId) {
|
||||
setErrors((prev) => ({ ...prev, userId: "아이디를 입력해주세요." }));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingId(true);
|
||||
try {
|
||||
const signupClient = SignupClient.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
const response = await signupClient.post<boolean>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/existsUserId`,
|
||||
{ userId: formData.userId },
|
||||
);
|
||||
|
||||
const exists = response.data;
|
||||
setIdAvailable(!exists);
|
||||
if (exists) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
userId: "이미 사용 중인 아이디입니다.",
|
||||
}));
|
||||
} else {
|
||||
setErrors((prev) => ({ ...prev, userId: "" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("아이디 중복 체크 오류:", error);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
userId: "아이디 중복 체크 중 오류가 발생했습니다.",
|
||||
}));
|
||||
} finally {
|
||||
setIsCheckingId(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkEmailDuplicate = async () => {
|
||||
if (!formData.email) {
|
||||
setErrors((prev) => ({ ...prev, email: "이메일을 입력해주세요." }));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingEmail(true);
|
||||
try {
|
||||
const signupClient = SignupClient.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
const response = await signupClient.post<boolean>(
|
||||
`${ENDPOINT.USER_SERVICE.USERS}/exists`,
|
||||
{ email: formData.email },
|
||||
);
|
||||
|
||||
const exists = response.data;
|
||||
setEmailAvailable(!exists);
|
||||
if (exists) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
email: "이미 사용 중인 이메일입니다.",
|
||||
}));
|
||||
} else {
|
||||
setErrors((prev) => ({ ...prev, email: "" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("이메일 중복 체크 오류:", error);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
email: "이메일 중복 체크 중 오류가 발생했습니다.",
|
||||
}));
|
||||
} finally {
|
||||
setIsCheckingEmail(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
console.log("회원가입 버튼 클릭됨 - handleSubmit");
|
||||
console.log("현재 폼 데이터:", formData);
|
||||
|
||||
if (!validateForm()) {
|
||||
console.log("폼 검증 실패");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("폼 검증 성공, 회원가입 API 호출 시작");
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const signupData: UserSignupData = {
|
||||
userId: formData.userId,
|
||||
userName: formData.userName,
|
||||
email: formData.email || undefined,
|
||||
password: formData.password,
|
||||
pswdHnt: formData.pswdHnt,
|
||||
pswdAns: formData.pswdAns || undefined,
|
||||
mphn: formData.mphn || undefined,
|
||||
telno: formData.telno || undefined,
|
||||
zip: formData.zip || undefined,
|
||||
addr: formData.addr || undefined,
|
||||
daddr: formData.daddr || undefined,
|
||||
laeSeCd: formData.laeSeCd,
|
||||
ogdpSeCd: formData.ogdpSeCd,
|
||||
instSeCd: formData.instSeCd || undefined,
|
||||
dtlInstCd: formData.dtlInstCd || undefined,
|
||||
enoCd: formData.enoCd || undefined,
|
||||
};
|
||||
|
||||
// SignupClient를 통해 익명사용자 회원가입 API 호출
|
||||
console.log("SignupClient 생성 및 API 호출:", {
|
||||
baseURL: BASE_URL,
|
||||
endpoint: ENDPOINT.USER_SERVICE.USER_JOIN,
|
||||
data: signupData,
|
||||
});
|
||||
|
||||
const signupClient = SignupClient.getInstance({
|
||||
baseURL: BASE_URL,
|
||||
});
|
||||
const response = await signupClient.post(
|
||||
ENDPOINT.USER_SERVICE.USER_JOIN,
|
||||
signupData,
|
||||
);
|
||||
|
||||
console.log("회원가입 성공:", response.data);
|
||||
|
||||
// 성공 시 모달 닫기 및 콜백 호출
|
||||
onSuccess();
|
||||
onClose();
|
||||
|
||||
// 폼 초기화
|
||||
setFormData({
|
||||
userId: "",
|
||||
userName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
pswdHnt: "",
|
||||
pswdAns: "",
|
||||
mphn: "",
|
||||
telno: "",
|
||||
zip: "",
|
||||
addr: "",
|
||||
daddr: "",
|
||||
laeSeCd: "",
|
||||
ogdpSeCd: "",
|
||||
instSeCd: "",
|
||||
dtlInstCd: "",
|
||||
enoCd: "",
|
||||
});
|
||||
setErrors({});
|
||||
setIdAvailable(null);
|
||||
setEmailAvailable(null);
|
||||
} catch (error: unknown) {
|
||||
console.error("회원가입 오류:", error);
|
||||
if (error instanceof Error) {
|
||||
setErrors((prev) => ({ ...prev, submit: error.message }));
|
||||
} else {
|
||||
setErrors((prev) => ({ ...prev, submit: "회원가입에 실패했습니다." }));
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="user-signup-modal-overlay">
|
||||
<div className="user-signup-modal-content">
|
||||
<div className="user-signup-modal-header">
|
||||
<h2>
|
||||
{currentStep === "agreement" ? "개인정보제공동의" : "회원가입"}
|
||||
</h2>
|
||||
<button className="user-signup-close-button" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentStep === "agreement" ? (
|
||||
<div className="user-signup-agreement-content">
|
||||
<div className="user-signup-agreement-text">
|
||||
<h3>개인정보 수집 및 이용 동의</h3>
|
||||
<p>회원가입을 위해 다음과 같이 개인정보를 수집 및 이용합니다.</p>
|
||||
|
||||
<h4>1. 수집하는 개인정보 항목</h4>
|
||||
<ul>
|
||||
<li>
|
||||
필수항목: 아이디, 이름, 비밀번호, 비밀번호 힌트, 법인구분,
|
||||
기관구분
|
||||
</li>
|
||||
<li>
|
||||
선택항목: 이메일, 휴대폰번호, 전화번호, 주소, 기관코드,
|
||||
상세기관코드, 사업자등록번호
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>2. 개인정보의 수집 및 이용목적</h4>
|
||||
<ul>
|
||||
<li>회원가입 및 관리</li>
|
||||
<li>서비스 제공 및 운영</li>
|
||||
<li>고객상담 및 문의응답</li>
|
||||
</ul>
|
||||
|
||||
<h4>3. 개인정보의 보유 및 이용기간</h4>
|
||||
<p>
|
||||
회원탈퇴 시까지 (단, 관련 법령에 따라 보존이 필요한 경우 해당
|
||||
기간까지)
|
||||
</p>
|
||||
|
||||
<h4>4. 동의 거부권 및 거부 시 불이익</h4>
|
||||
<p>
|
||||
개인정보 수집 및 이용에 대한 동의를 거부할 수 있으며, 동의 거부
|
||||
시 회원가입이 제한됩니다.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-agreement-checkbox">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreed}
|
||||
onChange={handleAgreementChange}
|
||||
/>
|
||||
<span>개인정보 수집 및 이용에 동의합니다.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-agreement-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="user-signup-cancel-button"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNextStep}
|
||||
className="user-signup-next-button"
|
||||
disabled={!agreed}
|
||||
>
|
||||
다음
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="user-signup-form">
|
||||
<div className="user-signup-form-section">
|
||||
<h3>기본 정보</h3>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="userId">아이디 *</label>
|
||||
<div className="user-signup-input-with-button">
|
||||
<input
|
||||
type="text"
|
||||
id="userId"
|
||||
name="userId"
|
||||
value={formData.userId}
|
||||
onChange={handleInputChange}
|
||||
placeholder="아이디를 입력하세요"
|
||||
maxLength={12}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={checkUserIdDuplicate}
|
||||
disabled={isCheckingId || isLoading || !formData.userId}
|
||||
className="user-signup-check-button"
|
||||
>
|
||||
{isCheckingId ? "확인중..." : "중복확인"}
|
||||
</button>
|
||||
</div>
|
||||
{idAvailable === true && (
|
||||
<span className="user-signup-success-message">
|
||||
사용 가능한 아이디입니다.
|
||||
</span>
|
||||
)}
|
||||
{errors.userId && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.userId}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="userName">이름 *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="userName"
|
||||
name="userName"
|
||||
value={formData.userName}
|
||||
onChange={handleInputChange}
|
||||
placeholder="이름을 입력하세요"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.userName && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.userName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="email">이메일</label>
|
||||
<div className="user-signup-input-with-button">
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
placeholder="이메일을 입력하세요"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={checkEmailDuplicate}
|
||||
disabled={isCheckingEmail || isLoading || !formData.email}
|
||||
className="user-signup-check-button"
|
||||
>
|
||||
{isCheckingEmail ? "확인중..." : "중복확인"}
|
||||
</button>
|
||||
</div>
|
||||
{emailAvailable === true && (
|
||||
<span className="user-signup-success-message">
|
||||
사용 가능한 이메일입니다.
|
||||
</span>
|
||||
)}
|
||||
{errors.email && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="password">비밀번호 *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder="8~20자, 영문/숫자/특수기호 포함"
|
||||
maxLength={20}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.password && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.password}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="confirmPassword">비밀번호 확인 *</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="비밀번호를 다시 입력하세요"
|
||||
maxLength={20}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.confirmPassword}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="pswdHnt">비밀번호 힌트 *</label>
|
||||
<AnonymousCodeSelect
|
||||
upCd="PSWDHNT"
|
||||
value={formData.pswdHnt}
|
||||
onValueChange={(value: string) =>
|
||||
setFormData((prev) => ({ ...prev, pswdHnt: value }))
|
||||
}
|
||||
disabled={isLoading}
|
||||
placeholder="비밀번호 힌트를 선택하세요"
|
||||
/>
|
||||
{errors.pswdHnt && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.pswdHnt}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="pswdAns">비밀번호 답변</label>
|
||||
<input
|
||||
type="text"
|
||||
id="pswdAns"
|
||||
name="pswdAns"
|
||||
value={formData.pswdAns}
|
||||
onChange={handleInputChange}
|
||||
placeholder="비밀번호 답변을 입력하세요"
|
||||
maxLength={100}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.pswdAns && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.pswdAns}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-section">
|
||||
<h3>연락처 정보</h3>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="mphn">휴대폰 번호</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="mphn"
|
||||
name="mphn"
|
||||
value={formData.mphn}
|
||||
onChange={handleInputChange}
|
||||
placeholder="휴대폰 번호를 입력하세요"
|
||||
maxLength={11}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.mphn && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.mphn}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="telno">전화번호</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="telno"
|
||||
name="telno"
|
||||
value={formData.telno}
|
||||
onChange={handleInputChange}
|
||||
placeholder="전화번호를 입력하세요"
|
||||
maxLength={11}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.telno && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.telno}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-section">
|
||||
<h3>주소 정보</h3>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="zip">우편번호</label>
|
||||
<input
|
||||
type="text"
|
||||
id="zip"
|
||||
name="zip"
|
||||
value={formData.zip}
|
||||
onChange={handleInputChange}
|
||||
placeholder="우편번호를 입력하세요"
|
||||
maxLength={5}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.zip && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.zip}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="addr">주소</label>
|
||||
<input
|
||||
type="text"
|
||||
id="addr"
|
||||
name="addr"
|
||||
value={formData.addr}
|
||||
onChange={handleInputChange}
|
||||
placeholder="주소를 입력하세요"
|
||||
maxLength={200}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.addr && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.addr}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="daddr">상세주소</label>
|
||||
<input
|
||||
type="text"
|
||||
id="daddr"
|
||||
name="daddr"
|
||||
value={formData.daddr}
|
||||
onChange={handleInputChange}
|
||||
placeholder="상세주소를 입력하세요"
|
||||
maxLength={200}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.daddr && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.daddr}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-section">
|
||||
<h3>기관 정보</h3>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="laeSeCd">법인구분 *</label>
|
||||
<AnonymousCodeSelect
|
||||
upCd="LAESECD"
|
||||
value={formData.laeSeCd}
|
||||
onValueChange={(value: string) =>
|
||||
setFormData((prev) => ({ ...prev, laeSeCd: value }))
|
||||
}
|
||||
disabled={isLoading}
|
||||
placeholder="법인구분을 선택하세요"
|
||||
/>
|
||||
{errors.laeSeCd && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.laeSeCd}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="ogdpSeCd">기관구분 *</label>
|
||||
<AnonymousCodeSelect
|
||||
upCd="OGDPSECD"
|
||||
value={formData.ogdpSeCd}
|
||||
onValueChange={(value: string) =>
|
||||
setFormData((prev) => ({ ...prev, ogdpSeCd: value }))
|
||||
}
|
||||
disabled={isLoading}
|
||||
placeholder="기관구분을 선택하세요"
|
||||
/>
|
||||
{errors.ogdpSeCd && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.ogdpSeCd}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="instSeCd">기관코드</label>
|
||||
<input
|
||||
type="text"
|
||||
id="instSeCd"
|
||||
name="instSeCd"
|
||||
value={formData.instSeCd}
|
||||
onChange={handleInputChange}
|
||||
placeholder="기관코드를 입력하세요"
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.instSeCd && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.instSeCd}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-form-group">
|
||||
<label htmlFor="dtlInstCd">상세기관코드</label>
|
||||
<input
|
||||
type="text"
|
||||
id="dtlInstCd"
|
||||
name="dtlInstCd"
|
||||
value={formData.dtlInstCd}
|
||||
onChange={handleInputChange}
|
||||
placeholder="상세기관코드를 입력하세요"
|
||||
maxLength={10}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.dtlInstCd && (
|
||||
<span className="user-signup-error-message">
|
||||
{errors.dtlInstCd}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errors.submit && (
|
||||
<div className="user-signup-form-row">
|
||||
<div className="user-signup-error-message user-signup-submit-error">
|
||||
{errors.submit}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="user-signup-form-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentStep("agreement")}
|
||||
className="user-signup-back-button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
이전
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="user-signup-cancel-button"
|
||||
disabled={isLoading}
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="user-signup-submit-button"
|
||||
disabled={isLoading}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
console.log("회원가입 버튼 클릭됨 - onClick");
|
||||
handleSubmit(e);
|
||||
}}
|
||||
>
|
||||
{isLoading ? "가입 중..." : "회원가입"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserSignupModal;
|
||||
@@ -1,16 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useEffect } from "react";
|
||||
import { images } from "@common/assets";
|
||||
import { LoginApi } from "@/src/entities/auth";
|
||||
import "./login.css";
|
||||
|
||||
function LoginPage() {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes("hasuon.or.kr"))
|
||||
window.location.href =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
|
||||
useEffect(() => {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes("hasuon.or.kr")) {
|
||||
window.location.href =
|
||||
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
/* 모달 오버레이 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 모달 컨텐츠 */
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 모달 헤더 */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 폼 스타일 */
|
||||
.signup-form {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 1.1rem;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.form-group input:disabled,
|
||||
.form-group select:disabled {
|
||||
background-color: #f3f4f6;
|
||||
color: #6b7280;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 입력 필드와 버튼이 함께 있는 경우 */
|
||||
.input-with-button {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.input-with-button input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check-button {
|
||||
padding: 10px 16px;
|
||||
background-color: #6b7280;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
white-space: nowrap;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.check-button:hover:not(:disabled) {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.check-button:disabled {
|
||||
background-color: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 메시지 스타일 */
|
||||
.error-message {
|
||||
color: #dc2626;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: #059669;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.submit-error {
|
||||
background-color: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 폼 액션 버튼 */
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.cancel-button,
|
||||
.submit-button {
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.cancel-button:hover:not(:disabled) {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.submit-button:hover:not(:disabled) {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.cancel-button:disabled,
|
||||
.submit-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
margin: 10px;
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.signup-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cancel-button,
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-with-button {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.check-button {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 스크롤바 스타일 */
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
/* 신규 사용자 가입 모달 오버레이 */
|
||||
.user-signup-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 신규 사용자 가입 모달 컨텐츠 */
|
||||
.user-signup-modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 신규 사용자 가입 모달 헤더 */
|
||||
.user-signup-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.user-signup-modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.user-signup-close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.user-signup-close-button:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 신규 사용자 가입 폼 스타일 */
|
||||
.user-signup-form {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.user-signup-form-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.user-signup-form-section h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.user-signup-form-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-signup-form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-signup-form-group label {
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.user-signup-form-group input,
|
||||
.user-signup-form-group select {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 1.1rem;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* CodeSelect 컴포넌트 스타일링 */
|
||||
.user-signup-form-group .CodeSelect {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-signup-form-group .CodeSelect select {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 1.1rem;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.user-signup-form-group .CodeSelect select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.user-signup-form-group .CodeSelect select:disabled {
|
||||
background-color: #f3f4f6;
|
||||
color: #6b7280;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Select 요소 스타일링 */
|
||||
.user-signup-select {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 1.1rem;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-signup-select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.user-signup-select:disabled {
|
||||
background-color: #f3f4f6;
|
||||
color: #6b7280;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.user-signup-select option {
|
||||
padding: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.user-signup-form-group input:focus,
|
||||
.user-signup-form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.user-signup-form-group input:disabled,
|
||||
.user-signup-form-group select:disabled {
|
||||
background-color: #f3f4f6;
|
||||
color: #6b7280;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 입력 필드와 버튼이 함께 있는 경우 */
|
||||
.user-signup-input-with-button {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.user-signup-input-with-button input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-signup-check-button {
|
||||
padding: 10px 16px;
|
||||
background-color: #6b7280;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
white-space: nowrap;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.user-signup-check-button:hover:not(:disabled) {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.user-signup-check-button:disabled {
|
||||
background-color: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 메시지 스타일 */
|
||||
.user-signup-error-message {
|
||||
color: #dc2626;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user-signup-success-message {
|
||||
color: #059669;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.user-signup-submit-error {
|
||||
background-color: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #dc2626;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 폼 액션 버튼 */
|
||||
.user-signup-form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.user-signup-cancel-button,
|
||||
.user-signup-submit-button,
|
||||
.user-signup-back-button {
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.user-signup-cancel-button {
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.user-signup-cancel-button:hover:not(:disabled) {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.user-signup-submit-button {
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-signup-submit-button:hover:not(:disabled) {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.user-signup-back-button {
|
||||
background-color: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.user-signup-back-button:hover:not(:disabled) {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
|
||||
.user-signup-cancel-button:disabled,
|
||||
.user-signup-submit-button:disabled,
|
||||
.user-signup-back-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 개인정보제공동의 스타일 */
|
||||
.user-signup-agreement-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.user-signup-agreement-text {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.user-signup-agreement-text h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.user-signup-agreement-text h4 {
|
||||
margin: 20px 0 8px 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.user-signup-agreement-text p {
|
||||
margin: 8px 0;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.user-signup-agreement-text ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.user-signup-agreement-text li {
|
||||
margin: 4px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.user-signup-agreement-checkbox {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background-color: #f9fafb;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.user-signup-agreement-checkbox label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.user-signup-agreement-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.user-signup-agreement-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.user-signup-next-button {
|
||||
padding: 12px 24px;
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.user-signup-next-button:hover:not(:disabled) {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.user-signup-next-button:disabled {
|
||||
background-color: #d1d5db;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 반응형 디자인 */
|
||||
@media (max-width: 768px) {
|
||||
.user-signup-modal-content {
|
||||
margin: 10px;
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.user-signup-modal-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.user-signup-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.user-signup-form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.user-signup-cancel-button,
|
||||
.user-signup-submit-button,
|
||||
.user-signup-back-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-signup-input-with-button {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.user-signup-check-button {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 스크롤바 스타일 */
|
||||
.user-signup-modal-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.user-signup-modal-content::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.user-signup-modal-content::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.user-signup-modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
interface PromptContext {
|
||||
|
||||
@@ -246,7 +246,7 @@ export const filePickedArray=(oFile: File, headerNum:number=0):Promise<JSON[] |
|
||||
wb.SheetNames.forEach(function (sheetName) {
|
||||
const sheet = wb.Sheets[sheetName];
|
||||
if (sheet) {
|
||||
console.log(sheet)
|
||||
// console.log(sheet)
|
||||
jsonXls = XLSX.utils.sheet_to_json(sheet, { range: headerNum, raw:false }) as unknown as JSON;
|
||||
arrayXls.push(jsonXls)
|
||||
} else {
|
||||
|
||||
@@ -26,6 +26,7 @@ type SearchBaseType = {
|
||||
leftSelectClassName?: string;
|
||||
buttonOrder?: ("query" | "init" | "excel")[];
|
||||
initSidoValue?: string | '';
|
||||
initSigunValue?: string | '';
|
||||
excelDisabled?: boolean;
|
||||
}
|
||||
export type ReturnType = {
|
||||
@@ -61,11 +62,12 @@ function SearchBaseForm ({
|
||||
leftSelectClassName,
|
||||
buttonOrder,
|
||||
initSidoValue='',
|
||||
initSigunValue='',
|
||||
excelDisabled=false,
|
||||
}: SearchBaseType) {
|
||||
const [partValue, setPartValue] = useState<string>("A");
|
||||
const [sidoValue, setSidoValue] = useState<string>(initSidoValue?initSidoValue:"0");
|
||||
const [sigunValue, setSigunValue] = useState<string>("0");
|
||||
const [sigunValue, setSigunValue] = useState<string>(initSigunValue?initSigunValue:"0");
|
||||
const [sidoText, setSidoText] = useState<string>("");
|
||||
const [sigunText, setSigunText] = useState<string>("");
|
||||
const [regionValue, setRegionValue] = useState<string>("0");
|
||||
@@ -115,18 +117,24 @@ function SearchBaseForm ({
|
||||
if(initSidoValue){
|
||||
setSidoValue(initSidoValue);
|
||||
}
|
||||
if(initSigunValue){
|
||||
setSigunValue(initSigunValue);
|
||||
}
|
||||
setResetTrigger(true);
|
||||
initClick();
|
||||
}, [initSidoValue, setResetTrigger, initClick]);
|
||||
}, [initSidoValue, initSigunValue, setResetTrigger, initClick]);
|
||||
useEffect(() => {
|
||||
if (resetTrigger) {
|
||||
if(initSidoValue!=""){
|
||||
setSidoValue(initSidoValue);
|
||||
}
|
||||
if(initSigunValue!=""){
|
||||
setSigunValue(initSigunValue);
|
||||
}
|
||||
|
||||
setResetTrigger(false);
|
||||
}
|
||||
}, [resetTrigger]);
|
||||
}, [resetTrigger, initSidoValue, initSigunValue]);
|
||||
const excelDownload = () => {
|
||||
excelClick({
|
||||
partValue: partValue,
|
||||
@@ -179,6 +187,7 @@ function SearchBaseForm ({
|
||||
leftClassName={leftClassName}
|
||||
leftSelectClassName={leftSelectClassName}
|
||||
initSidoValue={initSidoValue}
|
||||
initSigunValue={initSigunValue}
|
||||
>
|
||||
{leftChildren}
|
||||
</SearchFormLeft>
|
||||
|
||||
@@ -47,6 +47,7 @@ type SearchFormLeftProps = {
|
||||
leftClassName?: string;
|
||||
leftSelectClassName?: string;
|
||||
initSidoValue?: string;
|
||||
initSigunValue?: string;
|
||||
}
|
||||
|
||||
export type EnvPartSelectDataType = SelectDataType & {
|
||||
@@ -59,12 +60,12 @@ const selectPartData:SelectDataType[] = [
|
||||
{ text: "환경청별", val: 'G' },
|
||||
];
|
||||
|
||||
function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false, leftClassName, leftSelectClassName, initSidoValue }: SearchFormLeftProps) {
|
||||
function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false, leftClassName, leftSelectClassName, initSidoValue, initSigunValue }: SearchFormLeftProps) {
|
||||
/******************선택된 데이터******************/
|
||||
const [partValue, setPartValue] = useState<string>('A');
|
||||
//행정별 시도 시군구
|
||||
const [sidoValue, setSidoValue] = useState<string>(initSidoValue?initSidoValue : '0');
|
||||
const [sigunValue, setSigunValue] = useState<string>('0');
|
||||
const [sigunValue, setSigunValue] = useState<string>(initSigunValue?initSigunValue : '0');
|
||||
const [sidoText, setSidoText] = useState<string>('');
|
||||
const [sigunText, setSigunText] = useState<string>('');
|
||||
//유역별
|
||||
@@ -87,6 +88,39 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
|
||||
const [filteredEnvPartSigunData, setFilteredEnvPartSigunData] = useState<SelectDataType[]>([{ text: "전체", val: '0' }]);
|
||||
|
||||
const [initTrigger, setInitTrigger] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initSidoValue) return;
|
||||
if (!selectSidoData.some((item) => item.val === initSidoValue)) return;
|
||||
|
||||
const selectedSido = selectSidoData.find((item) => item.val === initSidoValue);
|
||||
setSidoValue(initSidoValue);
|
||||
setSidoText(selectedSido?.text || "");
|
||||
if (setRtnSidoValue) setRtnSidoValue(initSidoValue);
|
||||
if (setRtnSidoText) setRtnSidoText(selectedSido?.text || "");
|
||||
initializeSigunData(initSidoValue);
|
||||
}, [initSidoValue, selectSidoData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initSidoValue || !initSigunValue) return;
|
||||
|
||||
const initializedSigunData = [
|
||||
{ text: "전체", val: '0' },
|
||||
...selectSigunData.filter(
|
||||
(item: SelectDataType) => item.val.substring(0, 2) === initSidoValue
|
||||
)
|
||||
];
|
||||
const selectedSigun = initializedSigunData.find((item) => item.val === initSigunValue);
|
||||
|
||||
if (!selectedSigun) return;
|
||||
|
||||
setFilteredSigunData(initializedSigunData);
|
||||
setSigunValue(initSigunValue);
|
||||
setSigunText(selectedSigun.text || "");
|
||||
if (setRtnSigunValue) setRtnSigunValue(initSigunValue);
|
||||
if (setRtnSigunText) setRtnSigunText(selectedSigun.text || "");
|
||||
}, [initSidoValue, initSigunValue, selectSigunData, setRtnSigunText, setRtnSigunValue]);
|
||||
|
||||
// 시군구 세팅
|
||||
const initializeSigunData = (sidoCd: string) => {
|
||||
if (sidoCd === '0') {
|
||||
@@ -204,7 +238,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
|
||||
setRtnSidoText(selectedSido?.text || '');
|
||||
}
|
||||
initializeSigunData(val);
|
||||
onSelectValue('0', 'sigun');
|
||||
onSelectValue(val === initSidoValue && initSigunValue ? initSigunValue : '0', 'sigun');
|
||||
break;
|
||||
case 'sigun':
|
||||
if (setRtnSigunValue) {
|
||||
@@ -276,7 +310,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
|
||||
const resetAllValues = () => {
|
||||
setPartValue('A');
|
||||
setSidoValue(initSidoValue?initSidoValue:'0');
|
||||
setSigunValue('0');
|
||||
setSigunValue(initSigunValue?initSigunValue:'0');
|
||||
setRegionValue('0');
|
||||
setUnitWaterShedValue('0');
|
||||
setMiddleRegionValue('0');
|
||||
@@ -291,7 +325,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
|
||||
|
||||
if (setRtnPartValue) setRtnPartValue('A');
|
||||
if (setRtnSidoValue) setRtnSidoValue(initSidoValue?initSidoValue:'0');
|
||||
if (setRtnSigunValue) setRtnSigunValue('0');
|
||||
if (setRtnSigunValue) setRtnSigunValue(initSigunValue?initSigunValue:'0');
|
||||
if (setRtnRegionValue) setRtnRegionValue('0');
|
||||
if (setRtnUnitWaterShedValue) setRtnUnitWaterShedValue('0');
|
||||
if (setRtnMiddleRegionValue) setRtnMiddleRegionValue('0');
|
||||
|
||||
@@ -22,7 +22,7 @@ const SearchInput = React.forwardRef<HTMLInputElement, {className:string, label:
|
||||
return (
|
||||
<div className={className+" flex items-center"} >
|
||||
{label && <Label className="w-20 text-xl text-gray-600">{label}</Label>}
|
||||
<Input className="!text-lg flex-1 w-3/4 h-14 border rounded" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input>
|
||||
<Input className="!text-lg flex-1 w-3/4 h-13 border rounded" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
@@ -7,14 +7,14 @@ export function getAuthLoginRedirectUrl(): string {
|
||||
}
|
||||
|
||||
export function isAtLoginRedirectUrl(loginRedirectUrl: string): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
if (typeof window === "undefined") return false; // 서버 환경에서는 현재 브라우저 URL을 알 수 없으므로 false 반환
|
||||
try {
|
||||
const target = new URL(loginRedirectUrl, window.location.href);
|
||||
const target = new URL(loginRedirectUrl, window.location.href); // 상대경로/절대경로 모두 현재 주소 기준의 완전한 URL로 변환
|
||||
return (
|
||||
target.origin === window.location.origin &&
|
||||
target.pathname === window.location.pathname
|
||||
target.origin === window.location.origin && // 도메인(프로토콜 + 호스트 + 포트)이 같은지 확인
|
||||
target.pathname === window.location.pathname // 현재 페이지 경로가 로그인 이동 경로와 같은지 확인
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
return false; // 잘못된 URL 문자열이면 비교할 수 없으므로 false 반환
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user