Merge iipbs/sn/dev_kic into iipbs/sn/dev2_kic
This commit is contained in:
@@ -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 */
|
||||
+1
-1
@@ -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_CM_LOGIN_PAGE_URL = /auth/login
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL=http://localhost: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
|
||||
|
||||
@@ -33,6 +33,15 @@ export default function MapLayout({
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
|
||||
const { fetchCtpv, fetchSgg, fetchBIZ_SE_CD, fetchBIZ_TY_CD } = useStore();
|
||||
const isPopupPage = pathname === "/map/aged-pipe-popup";
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add("sn-map-page-lock");
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove("sn-map-page-lock");
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCtpv();
|
||||
@@ -46,6 +55,10 @@ export default function MapLayout({
|
||||
router.push(item.urlPath);
|
||||
};
|
||||
|
||||
if (isPopupPage) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<NoFooterLayout
|
||||
queryClient={queryClient}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Suspense } from "react";
|
||||
import AgedPipePopupPage from "../components/agedPipe/AgedPipePopupPage";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<AgedPipePopupPage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import type { AgedPipeFilters } from "./agedPipeUtils";
|
||||
|
||||
type Props = {
|
||||
filters: AgedPipeFilters;
|
||||
onFiltersChange: (filters: AgedPipeFilters) => void;
|
||||
onApply: (filters?: AgedPipeFilters) => void;
|
||||
};
|
||||
|
||||
export default function AgedPipeLayerSettingPanel({
|
||||
filters,
|
||||
onFiltersChange,
|
||||
onApply,
|
||||
}: Props) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [elapsedYearsText, setElapsedYearsText] = useState(String(filters.elapsedYears ?? ""));
|
||||
|
||||
useEffect(() => {
|
||||
setElapsedYearsText(String(filters.elapsedYears ?? ""));
|
||||
}, [filters.elapsedYears]);
|
||||
|
||||
const commitElapsedYears = () => {
|
||||
const nextFilters = {
|
||||
...filters,
|
||||
elapsedYears: elapsedYearsText === "" ? ("" as unknown as number) : Number(elapsedYearsText),
|
||||
};
|
||||
onFiltersChange(nextFilters);
|
||||
onApply(nextFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-sm border border-red-100 bg-red-50/35">
|
||||
<div className="relative flex min-h-[30px] items-center justify-center border-b border-red-100 bg-white px-8">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute left-2 top-1/2 h-[18px] w-[18px] -translate-y-1/2 bg-transparent text-[1.2rem] leading-none text-[#555]"
|
||||
onClick={() => setIsCollapsed((value) => !value)}
|
||||
title={isCollapsed ? "펼치기" : "접기"}
|
||||
>
|
||||
{isCollapsed ? "›" : "⌄"}
|
||||
</button>
|
||||
<span className="text-[1.2rem] font-semibold text-red-700">노후관로 설정</span>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="px-2 py-2">
|
||||
<div className="flex min-h-[30px] items-center gap-1 whitespace-nowrap text-[1.2rem] text-[#333]">
|
||||
<label htmlFor="aged-pipe-elapsed-years">경과년도(년)</label>
|
||||
<input
|
||||
id="aged-pipe-elapsed-years"
|
||||
className="h-[22px] w-14 border border-[#ccd6df] px-1 text-center text-[1.2rem]"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
autoComplete="off"
|
||||
value={elapsedYearsText}
|
||||
onFocus={(event) => event.currentTarget.select()}
|
||||
onChange={(event) => {
|
||||
const digits = event.target.value.replace(/[^0-9]/g, "");
|
||||
setElapsedYearsText(digits);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") commitElapsedYears();
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-[22px] w-[22px] place-items-center rounded-full bg-[#31a83b] text-[1.4rem] font-bold leading-none text-white"
|
||||
onClick={commitElapsedYears}
|
||||
title="적용"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import {
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
AGED_PIPE_EXCEL_HEADER,
|
||||
AGED_PIPE_LAYER_TYPE,
|
||||
AGED_PIPE_LIST_HEADLIST,
|
||||
AGED_PIPE_PAGE_SIZE,
|
||||
buildAgedPipeCqlFilters,
|
||||
normalizeAgedPipeFilters,
|
||||
toDisplayPipeRow,
|
||||
validateElapsedYears,
|
||||
type AgedPipeFilters,
|
||||
type AgedPipeRow,
|
||||
} from "./agedPipeUtils";
|
||||
|
||||
const POPUP_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
|
||||
const POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
|
||||
const POPUP_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
|
||||
|
||||
export default function AgedPipePopupPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
|
||||
const [filters, setFilters] = useState<AgedPipeFilters>(() =>
|
||||
normalizeAgedPipeFilters({
|
||||
elapsedYears: Number(searchParams.get("elapsedYears") ?? AGED_PIPE_DEFAULT_FILTERS.elapsedYears),
|
||||
ctpvCd: searchParams.get("ctpvCd") ?? AGED_PIPE_DEFAULT_FILTERS.ctpvCd,
|
||||
sggCd: searchParams.get("sggCd") ?? AGED_PIPE_DEFAULT_FILTERS.sggCd,
|
||||
}),
|
||||
);
|
||||
const [rows, setRows] = useState<AgedPipeRow[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const sggOptions = useMemo(
|
||||
() =>
|
||||
filters.ctpvCd === "0"
|
||||
? sgg
|
||||
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))),
|
||||
[filters.ctpvCd, sgg],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (ctpv.length === 0) fetchCtpv();
|
||||
if (sgg.length === 0) fetchSgg();
|
||||
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
||||
|
||||
const fetchRows = useCallback(
|
||||
async (page: number, nextFilters: AgedPipeFilters, size: number) => {
|
||||
const normalized = normalizeAgedPipeFilters(nextFilters);
|
||||
const queryParams = new URLSearchParams({
|
||||
lyrTypeNm: AGED_PIPE_LAYER_TYPE,
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
sort: "string",
|
||||
});
|
||||
const cqlFilters = buildAgedPipeCqlFilters(normalized);
|
||||
if (cqlFilters.length > 0) {
|
||||
queryParams.append("cqlFiter", cqlFilters.join(" and "));
|
||||
}
|
||||
|
||||
const response = await getData(`v1/fclts?${queryParams.toString()}`);
|
||||
const responseRows = Array.isArray(response?.data) ? response.data : [];
|
||||
|
||||
return {
|
||||
rows: responseRows.map((item: Record<string, unknown>, index: number) =>
|
||||
toDisplayPipeRow(item, index, page, ctpv, sgg),
|
||||
),
|
||||
total: Number(response?.count ?? 0),
|
||||
};
|
||||
},
|
||||
[ctpv, sgg],
|
||||
);
|
||||
|
||||
const loadRows = useCallback(
|
||||
async (page = 1, nextFilters = filters) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetchRows(page, nextFilters, AGED_PIPE_PAGE_SIZE);
|
||||
setRows(response.rows);
|
||||
setTotal(response.total);
|
||||
setCurrentPage(page);
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 조회 중 오류:", error);
|
||||
setRows([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[fetchRows, filters],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadRows(1, filters);
|
||||
// 최초 로딩만 수행한다. 이후 조회는 버튼/페이지 변경으로 처리한다.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleFilterSyncMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type !== POPUP_FILTER_SYNC_MESSAGE) return;
|
||||
if (!event.data?.filters) return;
|
||||
|
||||
const normalized = normalizeAgedPipeFilters(event.data.filters as AgedPipeFilters);
|
||||
setFilters(normalized);
|
||||
setSelectedIndex(-1);
|
||||
loadRows(1, normalized);
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleFilterSyncMessage);
|
||||
return () => window.removeEventListener("message", handleFilterSyncMessage);
|
||||
}, [loadRows]);
|
||||
|
||||
const handleSearch = (page = 1) => {
|
||||
if (!validateElapsedYears(Number(filters.elapsedYears))) {
|
||||
alert("경과년도는 1년~100년 범위 내에서 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
setFilters(normalized);
|
||||
loadRows(page, normalized);
|
||||
};
|
||||
|
||||
const handleRowClick = (index?: number) => {
|
||||
if (typeof index !== "number") return;
|
||||
const row = rows[index];
|
||||
if (!row) return;
|
||||
|
||||
setSelectedIndex(index);
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: POPUP_ROW_SELECT_MESSAGE,
|
||||
row,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (total === 0) {
|
||||
alert("다운로드할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetchRows(1, filters, 10000);
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: response.rows as JSON[],
|
||||
fileName: `노후관로_현황_${new Date().toISOString().slice(0, 10)}`,
|
||||
sheetNameArray: ["노후관로 현황"],
|
||||
headerRange: "A1:J1",
|
||||
headerMapping: AGED_PIPE_EXCEL_HEADER,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 다운로드 중 오류:", error);
|
||||
alert("노후관로 목록 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMinimize = () => {
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: POPUP_MINIMIZE_MESSAGE,
|
||||
filters: normalizeAgedPipeFilters(filters),
|
||||
total,
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
window.close();
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex h-screen flex-col overflow-hidden bg-white">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<header className="flex min-h-[44px] items-center justify-between border-b border-[#dbe2e6] bg-primary px-5 py-3">
|
||||
<h1 className="text-[1.45rem] font-semibold text-white">노후관로 현황</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="border border-white/30 px-3 py-1 text-[1.05rem] text-white"
|
||||
onClick={handleMinimize}
|
||||
>
|
||||
최소화
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-1 gap-3 border-b border-slate-200 bg-slate-50 pb-4 md:grid-cols-[140px_minmax(0,1fr)_140px_minmax(0,1fr)_auto_auto]">
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년도</div>
|
||||
<Input
|
||||
className="h-10 !text-[1.1rem]"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
autoComplete="off"
|
||||
value={String(filters.elapsedYears ?? "")}
|
||||
onChange={(event) => {
|
||||
const digits = event.target.value.replace(/[^0-9]/g, "");
|
||||
setFilters({
|
||||
...filters,
|
||||
elapsedYears: digits === "" ? ("" as unknown as number) : Number(digits),
|
||||
});
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") handleSearch(1);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시도</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "0",
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{ctpv.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시군구</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.sggCd}
|
||||
onChange={(event) => setFilters({ ...filters, sggCd: event.target.value })}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{sggOptions.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-2 md:col-span-3 md:justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="h-10 px-5 !text-[1.1rem]"
|
||||
onClick={() => handleSearch(1)}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="green"
|
||||
className="h-10 px-5 !text-[1.1rem]"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UiTable<AgedPipeRow>
|
||||
headName={<>노후관로 조회 결과 총 {total.toLocaleString()}건</>}
|
||||
tableData={rows}
|
||||
headlist={AGED_PIPE_LIST_HEADLIST}
|
||||
pageSize={10}
|
||||
total={total}
|
||||
cellClick={(index) => handleRowClick(index)}
|
||||
onPageChange={(page) => handleSearch(page)}
|
||||
curPage={currentPage}
|
||||
index={selectedIndex}
|
||||
widthFixed={true}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
"use client";
|
||||
|
||||
import type { SelectDataType } from "@common/business_components";
|
||||
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||
import {
|
||||
MS_SWAGE_PPL_CODES,
|
||||
MS_SWAGE_PPL_USE_CODES,
|
||||
} from "../common/table";
|
||||
|
||||
export const AGED_PIPE_LAYER_TYPE = "tb_ms_swage_ppl";
|
||||
export const AGED_PIPE_DEFAULT_ELAPSED_YEARS = 30;
|
||||
export const AGED_PIPE_PAGE_SIZE = 10;
|
||||
export const AGED_PIPE_MIN_ZOOM = 14;
|
||||
export const AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES = ["19000101", "19990101"];
|
||||
|
||||
export type AgedPipeFilters = {
|
||||
elapsedYears: number;
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
};
|
||||
|
||||
export type AgedPipeRow = {
|
||||
fid: string;
|
||||
lyrTypeNm: string;
|
||||
rowNumber: number;
|
||||
fcltMngNo: string;
|
||||
instlYmd: string;
|
||||
pdmtVl: string | number;
|
||||
prlgVl: string | number;
|
||||
usgCd: string;
|
||||
mqltCd: string;
|
||||
elapsedYears: number | string;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type PipeSource = Record<string, unknown>;
|
||||
|
||||
const useMap = new Map(MS_SWAGE_PPL_USE_CODES.map((item) => [item.val.trim(), item.text]));
|
||||
const materialMap = new Map(MS_SWAGE_PPL_CODES.map((item) => [item.val.trim(), item.text]));
|
||||
|
||||
export const AGED_PIPE_DEFAULT_FILTERS: AgedPipeFilters = {
|
||||
elapsedYears: AGED_PIPE_DEFAULT_ELAPSED_YEARS,
|
||||
ctpvCd: "0",
|
||||
sggCd: "0",
|
||||
};
|
||||
|
||||
export const AGED_PIPE_LIST_HEADLIST = [
|
||||
{ id: "rowNumber", title: "NO", widthPercent: 6 },
|
||||
{ id: "fcltMngNo", title: "시설관리번호", widthPercent: 18 },
|
||||
{ id: "instlYmd", title: "설치일자", widthPercent: 12 },
|
||||
{ id: "pdmtVl", title: "관경(mm)", widthPercent: 10 },
|
||||
{ id: "prlgVl", title: "연장(m)", widthPercent: 10 },
|
||||
{ id: "usgCd", title: "용도", widthPercent: 14 },
|
||||
{ id: "mqltCd", title: "재질", widthPercent: 18 },
|
||||
{ id: "elapsedYears", title: "경과년도", widthPercent: 12, align: "right" },
|
||||
];
|
||||
|
||||
export const AGED_PIPE_EXCEL_HEADER = {
|
||||
rowNumber: "NO",
|
||||
fcltMngNo: "시설관리번호",
|
||||
instlYmd: "설치일자",
|
||||
pdmtVl: "관경(mm)",
|
||||
prlgVl: "연장(m)",
|
||||
usgCd: "용도",
|
||||
mqltCd: "재질",
|
||||
elapsedYears: "경과년도",
|
||||
ctpvNm: "시도",
|
||||
sggNm: "시군구",
|
||||
};
|
||||
|
||||
const pad2 = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
const normalizeYmd = (value: unknown) =>
|
||||
String(value ?? "").replace(/[^0-9]/g, "").slice(0, 8);
|
||||
|
||||
const getToday = () => new Date();
|
||||
|
||||
export const normalizeAgedPipeFilters = (filters: AgedPipeFilters): AgedPipeFilters => {
|
||||
const elapsedYears = Math.trunc(Number(filters.elapsedYears));
|
||||
|
||||
return {
|
||||
...filters,
|
||||
elapsedYears:
|
||||
Number.isFinite(elapsedYears) && elapsedYears >= 1 && elapsedYears <= 100
|
||||
? elapsedYears
|
||||
: AGED_PIPE_DEFAULT_ELAPSED_YEARS,
|
||||
};
|
||||
};
|
||||
|
||||
export const validateElapsedYears = (value: number) =>
|
||||
Number.isInteger(value) && value >= 1 && value <= 100;
|
||||
|
||||
export const getCutoffYmd = (elapsedYears: number) => {
|
||||
const today = getToday();
|
||||
const cutoff = new Date(today.getFullYear() - elapsedYears, today.getMonth(), today.getDate());
|
||||
return `${cutoff.getFullYear()}${pad2(cutoff.getMonth() + 1)}${pad2(cutoff.getDate())}`;
|
||||
};
|
||||
|
||||
export const getElapsedYears = (value: unknown) => {
|
||||
const normalized = normalizeYmd(value);
|
||||
if (normalized.length < 4 || isInvalidInstallYmd(value)) return null;
|
||||
|
||||
const year = Number(normalized.slice(0, 4));
|
||||
const month = Number(normalized.slice(4, 6) || "1");
|
||||
const day = Number(normalized.slice(6, 8) || "1");
|
||||
if (!year) return null;
|
||||
|
||||
const today = getToday();
|
||||
let elapsed = today.getFullYear() - year;
|
||||
if (
|
||||
today.getMonth() + 1 < month ||
|
||||
(today.getMonth() + 1 === month && today.getDate() < day)
|
||||
) {
|
||||
elapsed -= 1;
|
||||
}
|
||||
|
||||
return Math.max(elapsed, 0);
|
||||
};
|
||||
|
||||
export const isInvalidInstallYmd = (value: unknown) => {
|
||||
const normalized = normalizeYmd(value);
|
||||
return (
|
||||
normalized.length < 8 ||
|
||||
AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES.some((prefix) => normalized.startsWith(prefix))
|
||||
);
|
||||
};
|
||||
|
||||
export const getElapsedYearsDisplayText = (value: unknown) => {
|
||||
const elapsedYears = getElapsedYears(value);
|
||||
return elapsedYears == null ? "확인불가" : `${elapsedYears}년`;
|
||||
};
|
||||
|
||||
export const buildAgedPipeCqlFilters = (filters: AgedPipeFilters) => {
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
const cqlFilters = [
|
||||
"instl_ymd IS NOT NULL",
|
||||
...AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES.map((prefix) => `instl_ymd NOT LIKE '${prefix}%'`),
|
||||
`instl_ymd <= '${getCutoffYmd(normalized.elapsedYears)}'`,
|
||||
];
|
||||
|
||||
if (normalized.sggCd !== "0") {
|
||||
cqlFilters.push(`pbadms_emd_cd LIKE '${normalized.sggCd}%'`);
|
||||
} else if (normalized.ctpvCd !== "0") {
|
||||
cqlFilters.push(`pbadms_emd_cd LIKE '${normalized.ctpvCd}%'`);
|
||||
}
|
||||
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
const getRegionCode = (source: PipeSource) =>
|
||||
String(source.pbadmsEmdCd ?? source.pbadms_emd_cd ?? "").trim();
|
||||
|
||||
export const matchesAgedPipeFilters = (source: PipeSource, filters: AgedPipeFilters) => {
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
const instlYmd = normalizeYmd(source.instl_ymd ?? source.instlYmd);
|
||||
if (!instlYmd || isInvalidInstallYmd(instlYmd) || instlYmd > getCutoffYmd(normalized.elapsedYears)) return false;
|
||||
|
||||
const regionCode = getRegionCode(source);
|
||||
if (normalized.sggCd !== "0") return regionCode.startsWith(normalized.sggCd);
|
||||
if (normalized.ctpvCd !== "0") return regionCode.startsWith(normalized.ctpvCd);
|
||||
return true;
|
||||
};
|
||||
|
||||
const resolveRegionName = (
|
||||
code: string,
|
||||
directName: unknown,
|
||||
options: SelectDataType[],
|
||||
prefixLength: number,
|
||||
) => {
|
||||
const direct = String(directName ?? "").trim();
|
||||
if (direct) return direct;
|
||||
|
||||
const prefix = code.slice(0, prefixLength);
|
||||
return options.find((item) => String(item.val).trim() === prefix)?.text ?? "";
|
||||
};
|
||||
|
||||
export const toDisplayPipeRow = (
|
||||
source: PipeSource,
|
||||
index: number,
|
||||
page: number,
|
||||
ctpvOptions: SelectDataType[],
|
||||
sggOptions: SelectDataType[],
|
||||
): AgedPipeRow => {
|
||||
const formatted = formatDisplayData(source) as PipeSource;
|
||||
const codePrefix = getRegionCode(source);
|
||||
const instlYmd = formatted.instlYmd ?? source.instlYmd ?? formatted.instl_ymd ?? source.instl_ymd;
|
||||
const invalidInstallYmd = isInvalidInstallYmd(source.instlYmd ?? source.instl_ymd ?? instlYmd);
|
||||
const trimmedUse = String(source.usgCd ?? source.usg_cd ?? "").trim();
|
||||
const trimmedMaterial = String(source.mqltCd ?? source.mqlt_cd ?? "").trim();
|
||||
const pdmtVl = formatted.pdmtVl ?? source.pdmtVl ?? source.pdmt_vl ?? "-";
|
||||
const prlgVl = formatted.prlgVl ?? source.prlgVl ?? source.prlg_vl ?? "-";
|
||||
const fcltMngNo = String(formatted.fcltMngNo ?? source.fcltMngNo ?? source.fclt_mng_no ?? "-");
|
||||
|
||||
return {
|
||||
...source,
|
||||
...formatted,
|
||||
fid: String(source.fid ?? ""),
|
||||
lyrTypeNm: String(source.lyrTypeNm ?? AGED_PIPE_LAYER_TYPE),
|
||||
rowNumber: (page - 1) * AGED_PIPE_PAGE_SIZE + index + 1,
|
||||
fcltMngNo,
|
||||
swtrmtFcltCd: source.swtrmtFcltCd ?? fcltMngNo,
|
||||
instlYmd: invalidInstallYmd ? "확인불가" : String(instlYmd ?? "-"),
|
||||
pdmtVl: typeof pdmtVl === "number" ? pdmtVl : String(pdmtVl),
|
||||
prlgVl: typeof prlgVl === "number" ? prlgVl : String(prlgVl),
|
||||
usgCd: useMap.get(trimmedUse) ?? String(formatted.usgCd ?? source.usgCd ?? source.usg_cd ?? "-"),
|
||||
mqltCd:
|
||||
materialMap.get(trimmedMaterial) ??
|
||||
String(formatted.mqltCd ?? source.mqltCd ?? source.mqlt_cd ?? "-"),
|
||||
elapsedYears: getElapsedYears(source.instlYmd ?? source.instl_ymd) ?? "확인불가",
|
||||
ctpvNm: resolveRegionName(
|
||||
codePrefix,
|
||||
formatted.ctpvNm ?? source.ctpvNm ?? source.ctpv_nm,
|
||||
ctpvOptions,
|
||||
2,
|
||||
),
|
||||
sggNm: resolveRegionName(
|
||||
codePrefix,
|
||||
formatted.sggNm ?? source.sggNm ?? source.sgg_nm,
|
||||
sggOptions,
|
||||
5,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const isAgedPipeLayerNode = (node: any) => {
|
||||
const data = node?.data ?? node;
|
||||
const layerName = String(data?.lyrNm ?? node?.text ?? "");
|
||||
const layerTypeName = String(data?.lyrTyNm ?? "");
|
||||
|
||||
return (
|
||||
layerTypeName === "tb_old_pipe" ||
|
||||
layerName.includes("노후관로")
|
||||
);
|
||||
};
|
||||
@@ -3,6 +3,7 @@ export const MsSwagePplCol = [
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
{ id: 'inst_mng_no', title: '기관관리번호' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'elapsedYears', title: '경과년도' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
{ id: 'pdmt_vl', title: '관경(mm)' },
|
||||
{ id: 'prlg_vl', title: '연장(m)' },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,6 +41,7 @@ import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { codeMapper } from "../common/codeMapper";
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { searchUpperPpls } from "./smh";
|
||||
import { getElapsedYearsDisplayText, isInvalidInstallYmd } from "../agedPipe/agedPipeUtils";
|
||||
|
||||
const linkedFacilityHighlightStyle = new Style({
|
||||
image: new CircleStyle({
|
||||
@@ -63,7 +64,6 @@ const linkedFacilityHighlightStyle = new Style({
|
||||
zIndex: 9999,
|
||||
});
|
||||
|
||||
|
||||
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
|
||||
const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; };
|
||||
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
|
||||
@@ -88,7 +88,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
const linkedFacilityOriginalStyleRef = useRef<any>(null);
|
||||
|
||||
// 🎯 관로관리번호 추출
|
||||
const pplMngNo = featureData?.values_?.fclt_mng_no || "";
|
||||
const pplMngNo = featureData?.values_?.fclt_mng_no || featureData?.fcltMngNo || "";
|
||||
|
||||
const featureId = typeof featureData.getId === 'function'
|
||||
? featureData.getId()
|
||||
@@ -258,7 +258,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
case 'tb_ms_swage_ppl': {
|
||||
result = '하수관로';
|
||||
setHeadlist(MsSwagePplCol2)
|
||||
setLayerSpecificNm(featureData.swtrmtFcltCd);
|
||||
setLayerSpecificNm(featureData.fcltMngNo ?? featureData.swtrmtFcltCd);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_mhl': {
|
||||
@@ -341,6 +341,14 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
|
||||
// 🎯 기본 데이터 포맷팅
|
||||
const formattedData = formatDisplayData(data);
|
||||
if (selectedLayerName === '하수관로') {
|
||||
const installYmd = data.instl_ymd ?? data.instlYmd;
|
||||
formattedData.elapsedYears = getElapsedYearsDisplayText(installYmd);
|
||||
if (isInvalidInstallYmd(installYmd)) {
|
||||
formattedData.instl_ymd = "확인불가";
|
||||
formattedData.instlYmd = "확인불가";
|
||||
}
|
||||
}
|
||||
|
||||
setFetchData(formattedData);
|
||||
setTableData(formattedData);
|
||||
@@ -407,8 +415,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
|
||||
// 자체입력정보 API 엔드포인트 선택
|
||||
let apiEndpoint = '';
|
||||
const fcltMngNo = featureData?.values_?.fclt_mng_no || null;
|
||||
const mngInstCd = featureData?.values_?.mng_inst_cd || null;
|
||||
const fcltMngNo = featureData?.values_?.fclt_mng_no || featureData?.fcltMngNo || null;
|
||||
const mngInstCd = featureData?.values_?.mng_inst_cd || featureData?.mngInstCd || null;
|
||||
if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${fcltMngNo}/${mngInstCd}`;
|
||||
else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${fcltMngNo}/${mngInstCd}`;
|
||||
else if (selectedLayerName === '하수펌프장') apiEndpoint = `v1/fcltpump/${fcltMngNo}/${mngInstCd}`;
|
||||
@@ -598,7 +606,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
|
||||
</div>
|
||||
<div className="overflow-y-auto overflow-x-hidden flex-1" ref={contentRef}>
|
||||
<VerticalTable
|
||||
headName='기본정보'
|
||||
headName={
|
||||
layerNm === '하수관로' && tableData?.elapsedYears
|
||||
? <>기본정보 <span className="ml-2 rounded-sm bg-red-50 px-2 py-0.5 text-[1.1rem] font-semibold text-red-700">경과년도 {tableData.elapsedYears}</span></>
|
||||
: '기본정보'
|
||||
}
|
||||
tableName1={layerNm === '중점우수토실' ? "측정지점" : undefined}
|
||||
tableData={[tableData]}
|
||||
headlist={headlist}
|
||||
|
||||
@@ -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,48 +18,130 @@
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import { getData } from "../../components/lib/api";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { getData, getRawJson } from "../../components/lib/api";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import SearchModal from "./searchModal/searchModal";
|
||||
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
|
||||
import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth";
|
||||
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
import { Button } from "@common/components";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useStore from "@/store/useStore";
|
||||
import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import AgedPipeLayerSettingPanel from "./components/agedPipe/AgedPipeLayerSettingPanel";
|
||||
import {
|
||||
AGED_PIPE_EXCEL_HEADER,
|
||||
AGED_PIPE_MIN_ZOOM,
|
||||
AGED_PIPE_LAYER_TYPE,
|
||||
AGED_PIPE_PAGE_SIZE,
|
||||
buildAgedPipeCqlFilters,
|
||||
isAgedPipeLayerNode,
|
||||
matchesAgedPipeFilters,
|
||||
normalizeAgedPipeFilters,
|
||||
toDisplayPipeRow,
|
||||
validateElapsedYears,
|
||||
type AgedPipeFilters,
|
||||
type AgedPipeRow,
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
} from "./components/agedPipe/agedPipeUtils";
|
||||
|
||||
const AGED_PIPE_CONTROL_NODE_ID = "SN_AGED_PIPE";
|
||||
const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT";
|
||||
const AGED_PIPE_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
|
||||
const AGED_PIPE_POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
|
||||
const AGED_PIPE_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
|
||||
|
||||
const AGED_PIPE_FOCUS_PADDING: [number, number, number, number] = [120, 120, 120, 120];
|
||||
const DEFAULT_FEATURE_FIT_PADDING: [number, number, number, number] = [50, 50, 50, 50];
|
||||
|
||||
const appendAgedPipeControlNode = (data: any[]) => {
|
||||
if (!Array.isArray(data)) return data;
|
||||
if (data.some((item) => String(item.lyrSn) === AGED_PIPE_CONTROL_NODE_ID || item.lyrNm === "노후관로")) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return [
|
||||
...data,
|
||||
{
|
||||
lyrSn: AGED_PIPE_CONTROL_NODE_ID,
|
||||
id: AGED_PIPE_CONTROL_NODE_ID,
|
||||
lyrNm: "노후관로",
|
||||
upperLyrSn: null,
|
||||
upId: null,
|
||||
sortOrdr: "4",
|
||||
drawOrdr: null,
|
||||
flterSql: null,
|
||||
gobjTy: "",
|
||||
lyrTyNm: "tb_old_pipe",
|
||||
useYn: "Y",
|
||||
minViewSize: null,
|
||||
iconNm: null,
|
||||
iconSize: null,
|
||||
lblAtrbNm: null,
|
||||
fontNm: null,
|
||||
fontIntnColor: null,
|
||||
fontExtnColor: null,
|
||||
fontExtnWdth: null,
|
||||
lnWdth: null,
|
||||
lnColor: null,
|
||||
polgnColor: null,
|
||||
initEprssYn: "Y",
|
||||
clickPsblYn: "N",
|
||||
exprsYn: "Y",
|
||||
cmncNm: "all",
|
||||
srvcNm: "wfs",
|
||||
actlLyrYn: "N",
|
||||
clckZoom: null,
|
||||
children: null,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const Map = () => {
|
||||
const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore();
|
||||
|
||||
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
|
||||
const [treeData, setTreeData] = useState<any[]>([]); // 초기값을 객체 또는 null로 설정
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
||||
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
|
||||
const [featureData, setFeatureData] = useState<any>(null); // 선택된 피쳐 데이터
|
||||
const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [mapReady, setMapReady] = useState(false);
|
||||
const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false);
|
||||
const [draftAgedPipeFilters, setDraftAgedPipeFilters] =
|
||||
useState<AgedPipeFilters>(AGED_PIPE_DEFAULT_FILTERS);
|
||||
const [appliedAgedPipeFilters, setAppliedAgedPipeFilters] =
|
||||
useState<AgedPipeFilters>(AGED_PIPE_DEFAULT_FILTERS);
|
||||
const [isAgedPipeListOpen, setIsAgedPipeListOpen] = useState(false);
|
||||
const [isAgedPipeListMinimized, setIsAgedPipeListMinimized] = useState(false);
|
||||
const [agedPipeRows, setAgedPipeRows] = useState<AgedPipeRow[]>([]);
|
||||
const [agedPipeTotalCount, setAgedPipeTotalCount] = useState(0);
|
||||
const [agedPipeCurrentPage, setAgedPipeCurrentPage] = useState(1);
|
||||
const [isAgedPipeListLoading, setIsAgedPipeListLoading] = useState(false);
|
||||
const { ctpv, sgg } = useStore();
|
||||
const shouldFocusAgedPipeOnNextSelectRef = useRef(false);
|
||||
const agedPipePopupWindowRef = useRef<Window | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
if (gisMaps.current) {
|
||||
setMapReady(true);
|
||||
window.clearInterval(timer);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
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);
|
||||
}
|
||||
setTreeData(data);
|
||||
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
|
||||
setTreeData(appendAgedPipeControlNode(data));
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
// JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환
|
||||
@@ -112,13 +194,23 @@ const Map = () => {
|
||||
if (geometry) {
|
||||
let extent = geometry.getExtent();
|
||||
|
||||
const layerTypeName = typeof foundFeature.getId === "function"
|
||||
? String(foundFeature.getId()).split(".")[0]
|
||||
: data?.lyrTypeNm;
|
||||
const shouldUseAgedPipeFocus =
|
||||
shouldFocusAgedPipeOnNextSelectRef.current && layerTypeName === AGED_PIPE_LAYER_TYPE;
|
||||
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16,
|
||||
padding: shouldUseAgedPipeFocus ? AGED_PIPE_FOCUS_PADDING : DEFAULT_FEATURE_FIT_PADDING,
|
||||
maxZoom: shouldUseAgedPipeFocus ? 18 : 16,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
});
|
||||
|
||||
if (shouldUseAgedPipeFocus) {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = false;
|
||||
}
|
||||
}
|
||||
select.selectFeature(foundFeature, true);
|
||||
} else {
|
||||
@@ -131,6 +223,181 @@ const Map = () => {
|
||||
};
|
||||
|
||||
const facilityTypes = [{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" }];
|
||||
|
||||
const fetchAgedPipeRows = useCallback(
|
||||
async (page: number, filters: AgedPipeFilters, size: number) => {
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
const queryParams = new URLSearchParams({
|
||||
lyrTypeNm: AGED_PIPE_LAYER_TYPE,
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
sort: "string",
|
||||
});
|
||||
|
||||
const cqlFilters = buildAgedPipeCqlFilters(normalized);
|
||||
if (cqlFilters.length > 0) {
|
||||
queryParams.append("cqlFiter", cqlFilters.join(" and "));
|
||||
}
|
||||
|
||||
const response = await getData(`v1/fclts?${queryParams.toString()}`);
|
||||
const rows = Array.isArray(response?.data) ? response.data : [];
|
||||
|
||||
return {
|
||||
rows: rows.map((item: Record<string, unknown>, index: number) =>
|
||||
toDisplayPipeRow(item, index, page, ctpv, sgg),
|
||||
),
|
||||
total: Number(response?.count ?? 0),
|
||||
};
|
||||
},
|
||||
[ctpv, sgg],
|
||||
);
|
||||
|
||||
const loadAgedPipeList = useCallback(
|
||||
async (page = 1, filters = appliedAgedPipeFilters) => {
|
||||
setIsAgedPipeListLoading(true);
|
||||
try {
|
||||
const response = await fetchAgedPipeRows(page, filters, AGED_PIPE_PAGE_SIZE);
|
||||
setAgedPipeRows(response.rows);
|
||||
setAgedPipeTotalCount(response.total);
|
||||
setAgedPipeCurrentPage(page);
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 조회 중 오류:", error);
|
||||
setAgedPipeRows([]);
|
||||
setAgedPipeTotalCount(0);
|
||||
} finally {
|
||||
setIsAgedPipeListLoading(false);
|
||||
}
|
||||
},
|
||||
[appliedAgedPipeFilters, fetchAgedPipeRows],
|
||||
);
|
||||
|
||||
const syncAgedPipePopupFilters = useCallback((filters: AgedPipeFilters) => {
|
||||
const popup = agedPipePopupWindowRef.current;
|
||||
if (!popup || popup.closed) {
|
||||
agedPipePopupWindowRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
popup.postMessage(
|
||||
{
|
||||
type: AGED_PIPE_FILTER_SYNC_MESSAGE,
|
||||
filters: normalizeAgedPipeFilters(filters),
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleApplyAgedPipeFilters = useCallback((filters = draftAgedPipeFilters) => {
|
||||
if (!validateElapsedYears(Number(filters.elapsedYears))) {
|
||||
alert("경과년도는 1년~100년 범위 내에서 입력해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeAgedPipeFilters(filters);
|
||||
setDraftAgedPipeFilters(normalized);
|
||||
setAppliedAgedPipeFilters(normalized);
|
||||
syncAgedPipePopupFilters(normalized);
|
||||
|
||||
if (isAgedPipeListOpen) {
|
||||
loadAgedPipeList(1, normalized);
|
||||
}
|
||||
}, [draftAgedPipeFilters, isAgedPipeListOpen, loadAgedPipeList, syncAgedPipePopupFilters]);
|
||||
|
||||
const openAgedPipePopupWindow = useCallback(() => {
|
||||
const popupParams = new URLSearchParams({
|
||||
elapsedYears: String(appliedAgedPipeFilters.elapsedYears),
|
||||
ctpvCd: appliedAgedPipeFilters.ctpvCd,
|
||||
sggCd: appliedAgedPipeFilters.sggCd,
|
||||
});
|
||||
const popup = window.open(
|
||||
`/map/aged-pipe-popup?${popupParams.toString()}`,
|
||||
"sn-aged-pipe-list",
|
||||
"popup=yes,width=1180,height=760,left=120,top=80,resizable=yes,scrollbars=no",
|
||||
);
|
||||
|
||||
if (!popup) {
|
||||
alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요.");
|
||||
return null;
|
||||
}
|
||||
|
||||
agedPipePopupWindowRef.current = popup;
|
||||
setIsAgedPipeListOpen(true);
|
||||
setIsAgedPipeListMinimized(false);
|
||||
return popup;
|
||||
}, [appliedAgedPipeFilters]);
|
||||
|
||||
const openAgedPipeList = useCallback(() => {
|
||||
openAgedPipePopupWindow();
|
||||
}, [openAgedPipePopupWindow]);
|
||||
|
||||
const handleAgedPipeRowDoubleClick = useCallback(
|
||||
async (row: AgedPipeRow) => {
|
||||
try {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = true;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid);
|
||||
} catch (error) {
|
||||
shouldFocusAgedPipeOnNextSelectRef.current = false;
|
||||
console.error("노후관로 위치 이동 중 오류:", error);
|
||||
}
|
||||
|
||||
setFeatureData(row as any);
|
||||
setIsOpen(true);
|
||||
setIsSearchModalOpen(false);
|
||||
},
|
||||
[setIsSearchModalOpen],
|
||||
);
|
||||
|
||||
const handleAgedPipeDownload = useCallback(async () => {
|
||||
if (agedPipeTotalCount === 0) {
|
||||
alert("다운로드할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsAgedPipeListLoading(true);
|
||||
const response = await fetchAgedPipeRows(1, appliedAgedPipeFilters, 10000);
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: response.rows as JSON[],
|
||||
fileName: `노후관로_현황_${new Date().toISOString().slice(0, 10)}`,
|
||||
sheetNameArray: ["노후관로 현황"],
|
||||
headerRange: "A1:J1",
|
||||
headerMapping: AGED_PIPE_EXCEL_HEADER,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 다운로드 중 오류:", error);
|
||||
alert("노후관로 목록 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsAgedPipeListLoading(false);
|
||||
}
|
||||
}, [agedPipeTotalCount, appliedAgedPipeFilters, fetchAgedPipeRows]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAgedPipePopupMessage = (event: MessageEvent) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data?.type === AGED_PIPE_ROW_SELECT_MESSAGE) {
|
||||
if (!event.data?.row) return;
|
||||
handleAgedPipeRowDoubleClick(event.data.row as AgedPipeRow);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === AGED_PIPE_POPUP_MINIMIZE_MESSAGE) {
|
||||
agedPipePopupWindowRef.current = null;
|
||||
if (event.data?.filters) {
|
||||
const normalized = normalizeAgedPipeFilters(event.data.filters as AgedPipeFilters);
|
||||
setDraftAgedPipeFilters(normalized);
|
||||
setAppliedAgedPipeFilters(normalized);
|
||||
}
|
||||
setAgedPipeTotalCount(Number(event.data?.total ?? agedPipeTotalCount));
|
||||
setIsAgedPipeListMinimized(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleAgedPipePopupMessage);
|
||||
return () => window.removeEventListener("message", handleAgedPipePopupMessage);
|
||||
}, [agedPipeTotalCount, handleAgedPipeRowDoubleClick]);
|
||||
|
||||
// WaterDepth 표시 여부를 결정하는 함수
|
||||
const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
|
||||
const waterDepthLayers = checkedNodesData.filter(node =>
|
||||
@@ -144,12 +411,83 @@ const Map = () => {
|
||||
// 체크된 레이어 변경 처리 함수
|
||||
const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
|
||||
handleWaterDepthVisibility(checkedIds, checkedNodesData);
|
||||
setIsAgedPipeChecked(checkedNodesData.some((node) => isAgedPipeLayerNode(node)));
|
||||
}, [handleWaterDepthVisibility]);
|
||||
|
||||
const handleOpenLayerPopup = useCallback(
|
||||
(_layerNm: string, node?: any) => {
|
||||
if (isAgedPipeLayerNode(node)) {
|
||||
openAgedPipeList();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[openAgedPipeList],
|
||||
);
|
||||
|
||||
const inlineLayerSettingView = useCallback(
|
||||
(node: any) => {
|
||||
if (!isAgedPipeChecked || !isAgedPipeLayerNode(node)) return null;
|
||||
|
||||
return (
|
||||
<AgedPipeLayerSettingPanel
|
||||
filters={draftAgedPipeFilters}
|
||||
onFiltersChange={setDraftAgedPipeFilters}
|
||||
onApply={handleApplyAgedPipeFilters}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[draftAgedPipeFilters, handleApplyAgedPipeFilters, isAgedPipeChecked],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const core = GisApp.Module.core as any;
|
||||
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
|
||||
|
||||
if (!isAgedPipeChecked) return;
|
||||
|
||||
const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find(
|
||||
(item: any) => item?.lyrTyNm === AGED_PIPE_LAYER_TYPE && item?.actlLyrYn === "Y",
|
||||
) as any;
|
||||
|
||||
if (!baseLayerCode) return;
|
||||
|
||||
const layerCode = {
|
||||
...baseLayerCode,
|
||||
id: AGED_PIPE_HIGHLIGHT_LAYER_ID,
|
||||
lyrSn: AGED_PIPE_HIGHLIGHT_LAYER_ID,
|
||||
lyrNm: "노후관로",
|
||||
lyrTyNm: AGED_PIPE_LAYER_TYPE,
|
||||
gobjTy: "line",
|
||||
actlLyrYn: "Y",
|
||||
exprsYn: "Y",
|
||||
clickPsblYn: "Y",
|
||||
cmncNm: "bbox",
|
||||
srvcNm: "wfs",
|
||||
flterSql: buildAgedPipeCqlFilters(appliedAgedPipeFilters).join(" and "),
|
||||
lnWdth: "4",
|
||||
lnColor: "rgba(235, 40, 40, 0.82)",
|
||||
drawOrdr: "9999",
|
||||
minViewSize: String(AGED_PIPE_MIN_ZOOM),
|
||||
};
|
||||
|
||||
const layer = core?.addWfsLayer?.(layerCode);
|
||||
layer?.set?.("id", AGED_PIPE_HIGHLIGHT_LAYER_ID);
|
||||
layer?.set?.("lyrNm", "노후관로");
|
||||
layer?.set?.("lyrTyNm", AGED_PIPE_LAYER_TYPE);
|
||||
|
||||
return () => {
|
||||
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
|
||||
};
|
||||
}, [appliedAgedPipeFilters, isAgedPipeChecked, mapReady]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-container">
|
||||
<div className="bg-container relative h-[calc(100dvh-5rem)] overflow-hidden">
|
||||
<Gis
|
||||
treeData={treeData}
|
||||
isSearch={true}
|
||||
@@ -165,8 +503,11 @@ const Map = () => {
|
||||
isSearchModalOpen={isSearchModalOpen}
|
||||
setIsSearchModalOpen={setIsSearchModalOpen}
|
||||
onCheckedLayersChange={handleCheckedLayersChange}
|
||||
onOpenPopup={handleOpenLayerPopup}
|
||||
searchBar={<ProcSearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} />}
|
||||
waterDepth={showWaterDepth ? WaterDepth : undefined}
|
||||
inlineLayerSettingView={inlineLayerSettingView}
|
||||
layerIconVisible={isAgedPipeLayerNode}
|
||||
isPlaying={isPlaying}
|
||||
setIsPlaying={setIsPlaying as any}
|
||||
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
|
||||
@@ -176,6 +517,37 @@ const Map = () => {
|
||||
defaultZoom={7.5}
|
||||
defaultLonLat={[127.75, 36]}
|
||||
/>
|
||||
{isAgedPipeListMinimized && (
|
||||
<div className="fixed left-[480px] top-[68px] z-[90] flex min-w-[250px] items-center gap-3 rounded-sm border border-red-200 bg-white/95 px-3 py-2 shadow-[0_8px_22px_rgba(15,23,42,0.22)] backdrop-blur">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm bg-red-50">
|
||||
<span className="block h-4 w-4 rounded-sm border-2 border-red-500 bg-red-500/20" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[1.25rem] font-semibold leading-tight text-red-700">노후관로 현황</div>
|
||||
<div className="text-[1rem] leading-tight text-slate-500">
|
||||
{appliedAgedPipeFilters.elapsedYears}년 이상 · {agedPipeTotalCount.toLocaleString()}건
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
size="xs"
|
||||
className="h-7 px-3"
|
||||
onClick={openAgedPipeList}
|
||||
>
|
||||
열기
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="h-7 px-3"
|
||||
onClick={() => setIsAgedPipeListMinimized(false)}
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@common/components";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import {
|
||||
AGED_PIPE_DETAIL_FIELDS,
|
||||
getElapsedYears,
|
||||
@@ -16,39 +17,48 @@ export default function AgedPipeDetailPanel({ featureData, onClose }: Props) {
|
||||
const source = featureData?.values_ ?? featureData ?? {};
|
||||
const facilityNo = String(getPipeDisplayValue(source, "fclt_mng_no") ?? "-");
|
||||
const elapsedYears = getElapsedYears(source.instl_ymd ?? source.instlYmd);
|
||||
const detailFields = [
|
||||
...AGED_PIPE_DETAIL_FIELDS,
|
||||
{ id: "elapsedYears", title: "경과년도" },
|
||||
];
|
||||
const tableData = detailFields.reduce<Record<string, string>>((acc, field) => {
|
||||
acc[field.id] =
|
||||
field.id === "elapsedYears"
|
||||
? elapsedYears === null
|
||||
? "확인불가"
|
||||
: `${elapsedYears}년`
|
||||
: String(getPipeDisplayValue(source, field.id) ?? "-");
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[320px] flex-col overflow-hidden border-l border-slate-200 bg-white shadow-[-12px_0_24px_rgba(15,23,42,0.1)]">
|
||||
<div className="flex items-start justify-between border-b border-slate-200 bg-slate-900 px-4 py-3">
|
||||
<div className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[350px] flex-col overflow-hidden border-l border-gray-300 bg-white">
|
||||
<div className="card-header relative flex items-center justify-between bg-primary p-4">
|
||||
<div>
|
||||
<div className="text-[1.45rem] font-semibold text-white">노후관로 상세</div>
|
||||
<div className="mt-1 text-[1.05rem] text-slate-300">시설관리번호 {facilityNo}</div>
|
||||
<div className="mt-1 text-[1.05rem] text-amber-300">
|
||||
경과년도 {elapsedYears ?? "-"}년
|
||||
<div className="text-2xl font-medium text-white text-label">
|
||||
하수관로 정보 <br />관리번호 : {facilityNo}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="border border-white/30 px-3 py-1 text-[1.05rem] text-white"
|
||||
className="text-white hover:text-gray-300"
|
||||
onClick={onClose}
|
||||
>
|
||||
닫기
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto px-4 py-3">
|
||||
<div className="space-y-2">
|
||||
{AGED_PIPE_DETAIL_FIELDS.map((field) => (
|
||||
<div key={field.id} className="rounded-lg border border-slate-200 px-3 py-2.5">
|
||||
<div className="text-[1rem] font-medium text-slate-500">{field.title}</div>
|
||||
<div className="mt-1 break-words text-[1.15rem] leading-6 text-slate-900">
|
||||
{String(getPipeDisplayValue(source, field.id) ?? "-")}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<VerticalTable
|
||||
headName="기본정보"
|
||||
tableData={[tableData]}
|
||||
headlist={detailFields}
|
||||
pageSize={20}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
onPageChange={() => undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Input } from "@common/components";
|
||||
import useStore from "@/store/useStore";
|
||||
import { useState } from "react";
|
||||
import type { AgedPipeFilters } from "./agedPipeUtils";
|
||||
|
||||
type OverlayProps = {
|
||||
@@ -13,89 +12,62 @@ type OverlayProps = {
|
||||
};
|
||||
|
||||
type Props = OverlayProps & {
|
||||
title?: string;
|
||||
filters: AgedPipeFilters;
|
||||
onFiltersChange: (filters: AgedPipeFilters) => void;
|
||||
onApply: () => void;
|
||||
};
|
||||
|
||||
export default function AgedPipeFilterPanel({
|
||||
title = "지반침하정보",
|
||||
filters,
|
||||
onFiltersChange,
|
||||
onApply,
|
||||
}: Props) {
|
||||
const { ctpv, sgg } = useStore();
|
||||
const sggOptions = filters.ctpvCd === "0"
|
||||
? sgg
|
||||
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)));
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="w-[280px] rounded-xl border border-slate-200 bg-white/95 p-3 shadow-[0_14px_30px_rgba(15,23,42,0.16)] backdrop-blur">
|
||||
<div className="mb-2">
|
||||
<div className="text-[1.35rem] font-semibold text-slate-900">노후관로 기준</div>
|
||||
<div className="mt-1 text-[1.05rem] leading-5 text-slate-500">
|
||||
경과년수 이상 관로만 지도와 목록에 반영합니다.
|
||||
<div className="overflow-hidden rounded-sm border border-[#bdbdbd] bg-white">
|
||||
<div className="relative flex min-h-[34px] items-center justify-center border-b border-[#eeeeee] bg-white px-8">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute left-2 top-1/2 h-[18px] w-[18px] -translate-y-1/2 bg-transparent text-[1.2rem] leading-none text-[#555]"
|
||||
onClick={() => setIsCollapsed((value) => !value)}
|
||||
title={isCollapsed ? "펼치기" : "접기"}
|
||||
>
|
||||
{isCollapsed ? "›" : "⌄"}
|
||||
</button>
|
||||
<span className="text-[1.3rem] font-semibold text-[#4b5563]">{title}</span>
|
||||
</div>
|
||||
|
||||
{!isCollapsed && (
|
||||
<div className="px-3 py-2">
|
||||
<div className="flex min-h-[30px] items-center gap-1 whitespace-nowrap text-[1.2rem] text-[#333]">
|
||||
<label htmlFor="aged-pipe-elapsed-years">경과년도(년)</label>
|
||||
<input
|
||||
id="aged-pipe-elapsed-years"
|
||||
className="h-[22px] w-10 border border-[#ccd6df] px-1 text-center text-[1.2rem]"
|
||||
inputMode="numeric"
|
||||
value={String(filters.elapsedYears)}
|
||||
onChange={(event) => {
|
||||
const next = Number(event.target.value.replace(/[^0-9]/g, "") || "0");
|
||||
onFiltersChange({ ...filters, elapsedYears: next });
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") onApply();
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-[22px] w-[22px] place-items-center rounded-full bg-[#31a83b] text-[1.4rem] font-bold leading-none text-white"
|
||||
onClick={onApply}
|
||||
title="적용"
|
||||
>
|
||||
✓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년수</div>
|
||||
<Input
|
||||
className="h-10 !text-[1.15rem]"
|
||||
inputMode="numeric"
|
||||
value={String(filters.elapsedYears)}
|
||||
onChange={(event) => {
|
||||
const next = Number(event.target.value.replace(/[^0-9]/g, "") || "0");
|
||||
onFiltersChange({ ...filters, elapsedYears: next });
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") onApply();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시도</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "0",
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{ctpv.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시군구</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.sggCd}
|
||||
onChange={(event) => onFiltersChange({ ...filters, sggCd: event.target.value })}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{sggOptions.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button className="mt-3 h-10 w-full !text-[1.15rem]" onClick={onApply}>
|
||||
GIS 반영
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -64,14 +64,11 @@ export default function AgedPipeListModal({
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-[70] flex items-center justify-center px-6 py-8">
|
||||
<div className="pointer-events-auto relative flex max-h-[82vh] w-[min(1120px,94vw)] flex-col overflow-hidden rounded-2xl border border-slate-300 bg-white shadow-[0_28px_80px_rgba(15,23,42,0.28)]">
|
||||
<div className="pointer-events-auto relative flex max-h-[82vh] w-[min(1120px,94vw)] flex-col overflow-hidden border border-[#dbe2e6] bg-white shadow-[0_0_12px_rgba(0,0,0,0.25)]">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-900 px-5 py-3">
|
||||
<div className="flex items-center justify-between border-b border-[#dbe2e6] bg-primary px-5 py-3">
|
||||
<div>
|
||||
<div className="text-[1.45rem] font-semibold text-white">노후관로 목록</div>
|
||||
<div className="mt-1 text-[1rem] text-slate-300">
|
||||
지도 위에 띄워지는 윈도우형 팝업 미리보기
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -85,7 +82,7 @@ export default function AgedPipeListModal({
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-1 gap-3 pb-4 md:grid-cols-[110px_minmax(0,1fr)_110px_minmax(0,1fr)_auto_auto]">
|
||||
<div className="grid grid-cols-1 gap-3 bg-slate-50 pb-4 md:grid-cols-[110px_minmax(0,1fr)_110px_minmax(0,1fr)_auto_auto]">
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년수</div>
|
||||
<Input
|
||||
@@ -141,7 +138,7 @@ export default function AgedPipeListModal({
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-2 md:col-span-3 md:justify-end">
|
||||
<Button type="button" size="sm" className="h-10 px-5 !text-[1.1rem]" onClick={() => onSearch(1)}>
|
||||
<Button type="button" size="sm" color="primary" className="h-10 px-5 !text-[1.1rem]" onClick={() => onSearch(1)}>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -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 { getData, 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);
|
||||
@@ -333,12 +321,12 @@ const Map = () => {
|
||||
};
|
||||
}, [appliedFilters, mapReady]);
|
||||
|
||||
const FilterOverlay = useMemo(
|
||||
const LayerSettingView = useMemo(
|
||||
() =>
|
||||
function FilterOverlayComponent(props: any) {
|
||||
function LayerSettingComponent() {
|
||||
return (
|
||||
<AgedPipeFilterPanel
|
||||
{...props}
|
||||
title="지반침하정보"
|
||||
filters={draftFilters}
|
||||
onFiltersChange={setDraftFilters}
|
||||
onApply={handleApplyFilters}
|
||||
@@ -368,7 +356,7 @@ const Map = () => {
|
||||
setIsSearchModalOpen={setIsSearchModalOpen}
|
||||
onCheckedLayersChange={() => undefined}
|
||||
searchBar={<></>}
|
||||
waterDepth={FilterOverlay}
|
||||
layerSettingView={LayerSettingView}
|
||||
showZoomLevel={true}
|
||||
defaultZoom={7.5}
|
||||
defaultLonLat={[127.75, 36]}
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -10,3 +10,7 @@ body {
|
||||
font-family: "Noto Sans KR", sans-serif;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
body.sn-map-page-lock {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -107,6 +107,7 @@ export const chRowspanTable = (
|
||||
// rowspan된 셀 렌더링 여부 추적용
|
||||
const renderedMap = new Map<string, Set<number>>();
|
||||
|
||||
// console.log(makeTableData);
|
||||
return (
|
||||
<>
|
||||
{/* {makeTableData.map((row, rowIndex) => ( */}
|
||||
@@ -322,6 +323,26 @@ const handleChange = (
|
||||
);
|
||||
};
|
||||
|
||||
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[] | [],
|
||||
@@ -357,19 +378,8 @@ 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";
|
||||
@@ -433,22 +443,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
|
||||
|
||||
@@ -192,7 +192,7 @@ export default function ChkgdExcel({
|
||||
}
|
||||
})
|
||||
.finally(() => setRswtChkLoading(false));
|
||||
console.log("업로드파일 점검 완료");
|
||||
// console.log("업로드파일 점검 완료");
|
||||
}
|
||||
}
|
||||
}, [file]);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -32,6 +32,9 @@ export const GisContext = createContext({
|
||||
});
|
||||
export const useGisContext = () => useContext(GisContext);
|
||||
|
||||
/**
|
||||
* @param {any} props
|
||||
*/
|
||||
function Gis({
|
||||
isOpen, //우측 상세 레이어 팝업
|
||||
gisMaps,
|
||||
@@ -49,7 +52,10 @@ function Gis({
|
||||
setIsSearchModalOpen,
|
||||
onCheckedLayersChange,
|
||||
searchBar: SearchBar,
|
||||
waterDepth: WaterDepth,
|
||||
waterDepth: WaterDepth = null,
|
||||
layerSettingView: LayerSettingView = null,
|
||||
inlineLayerSettingView,
|
||||
layerIconVisible,
|
||||
isPlaying = false,
|
||||
setIsPlaying = () => { },
|
||||
targetLayerTypes = [],
|
||||
@@ -281,18 +287,21 @@ function Gis({
|
||||
visible={tabVisible}
|
||||
setVisible={setTabVisible}
|
||||
onCheckedChange={handleCheckedLayersChange}
|
||||
onLayerIconClick={(layerNm) => {
|
||||
onLayerIconClick={(layerNm, node) => {
|
||||
layerNmRef.current = layerNm;
|
||||
setLayerNm(layerNm);
|
||||
setIsOpen(true);
|
||||
onOpenPopup(layerNm)
|
||||
const shouldOpenDetail = onOpenPopup(layerNm, node)
|
||||
if (shouldOpenDetail !== false) setIsOpen(true);
|
||||
}}
|
||||
imageVisible={imgVisble}
|
||||
layerIconVisible={layerIconVisible}
|
||||
isPlaying={isPlaying}
|
||||
setIsPlaying={setIsPlaying}
|
||||
targetLayerTypes={targetLayerTypes}
|
||||
checkInterval={checkInterval}
|
||||
onProgressChange={handleProgressChange}
|
||||
layerSettingView={LayerSettingView}
|
||||
inlineLayerSettingView={inlineLayerSettingView}
|
||||
/>
|
||||
|
||||
{typeof WaterDepth === "function" && (
|
||||
|
||||
@@ -27,6 +27,8 @@ import GisApp from "./lib/gis.core";
|
||||
* - orderedLayers: 레이어 재생 순서를 지정하는 id 배열 (lyrSn)
|
||||
* - checkInterval: 자동 애니메이션 토글 간격(ms)
|
||||
* - onProgressChange(total, current): 애니메이션 진행상태 콜백
|
||||
* - layerSettingView: (선택) 좌측 레이어 트리 하단에 표시할 설정 영역
|
||||
* - layerIconVisible: (선택) 레이어 행 우측 액션 아이콘 노출 여부를 결정하는 함수
|
||||
*/
|
||||
export default React.memo(function GisTree({
|
||||
checkedNodes = [],
|
||||
@@ -41,6 +43,9 @@ export default React.memo(function GisTree({
|
||||
targetLayerTypes = [],
|
||||
checkInterval = 3000,
|
||||
onProgressChange = () => { },
|
||||
layerSettingView: LayerSettingView = null,
|
||||
layerIconVisible,
|
||||
inlineLayerSettingView,
|
||||
}) {
|
||||
const { gisCore, treeData } = useGisContext();
|
||||
|
||||
@@ -494,9 +499,9 @@ export default React.memo(function GisTree({
|
||||
/**
|
||||
* 각 레이어 행의 아이콘 클릭 시 호출. 선택되지 않은 레이어면 경고, 선택된 경우 콜백 전달.
|
||||
*/
|
||||
const handleLayerImgClick = (nodeNm, nodeId) => {
|
||||
const handleLayerImgClick = (nodeNm, nodeId, node) => {
|
||||
if (!checked.includes(nodeId)) alert("시설을 선택해주세요");
|
||||
else onLayerIconClick(nodeNm);
|
||||
else onLayerIconClick(nodeNm, node);
|
||||
};
|
||||
|
||||
// =====================
|
||||
@@ -534,48 +539,59 @@ export default React.memo(function GisTree({
|
||||
child.parent === node.id && child.data.exprsYn === "Y"
|
||||
);
|
||||
|
||||
const inlineLayerSetting = inlineLayerSettingView?.(node);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 rounded px-2 py-1 hover:bg-gray-100"
|
||||
style={{
|
||||
marginInlineStart: depth * 16,
|
||||
display: isVisible ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
{hasVisibleChildren ? (
|
||||
<i
|
||||
onClick={onToggle}
|
||||
className={`diveicon di-arrow-forward mr-2 text-base text-label ${isOpen ? "rotate-90" : ""
|
||||
}`}
|
||||
title={isOpen ? "닫기" : "열기"}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ display: "inline-block", width: 16 }} />
|
||||
)}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked.includes(node.id)}
|
||||
onChange={() => handleCheck(node.id)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
{node.icon && <span>{node.icon}</span>}
|
||||
<span className="text-[1.3rem] font-medium text-label">
|
||||
{node.text}
|
||||
</span>
|
||||
{imageVisible && depth > 0 && (
|
||||
<span>
|
||||
<img
|
||||
className="h-5 w-5 cursor-pointer"
|
||||
src="/images/map/layericon/Layer_Select_BTN.png"
|
||||
onClick={() =>
|
||||
handleLayerImgClick(
|
||||
node.data.lyrTyNm,
|
||||
node.id
|
||||
)
|
||||
}
|
||||
alt={node.text}
|
||||
<div style={{ display: isVisible ? "block" : "none" }}>
|
||||
<div
|
||||
className="flex items-center gap-2 rounded px-2 py-1 hover:bg-gray-100"
|
||||
style={{
|
||||
marginInlineStart: depth * 16,
|
||||
}}
|
||||
>
|
||||
{hasVisibleChildren ? (
|
||||
<i
|
||||
onClick={onToggle}
|
||||
className={`diveicon di-arrow-forward mr-2 text-base text-label ${isOpen ? "rotate-90" : ""
|
||||
}`}
|
||||
title={isOpen ? "닫기" : "열기"}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ display: "inline-block", width: 16 }} />
|
||||
)}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked.includes(node.id)}
|
||||
onChange={() => handleCheck(node.id)}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
{node.icon && <span>{node.icon}</span>}
|
||||
<span className="text-[1.3rem] font-medium text-label">
|
||||
{node.text}
|
||||
</span>
|
||||
{((depth > 0 && imageVisible) || layerIconVisible?.(node)) && (
|
||||
<span>
|
||||
<img
|
||||
className="h-5 w-5 cursor-pointer"
|
||||
src="/images/map/layericon/Layer_Select_BTN.png"
|
||||
onClick={() =>
|
||||
handleLayerImgClick(
|
||||
node.data.lyrTyNm,
|
||||
node.id,
|
||||
node
|
||||
)
|
||||
}
|
||||
alt={node.text}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{inlineLayerSetting && (
|
||||
<div className="mt-2 pb-3 pr-1" style={{ marginInlineStart: depth * 8 + 12 }}>
|
||||
{typeof inlineLayerSetting === "function"
|
||||
? React.createElement(inlineLayerSetting)
|
||||
: inlineLayerSetting}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -588,6 +604,11 @@ export default React.memo(function GisTree({
|
||||
/>
|
||||
</DndProvider>
|
||||
</div>
|
||||
{LayerSettingView && (
|
||||
<div className="shrink-0 bg-white px-2 pb-3">
|
||||
<LayerSettingView />
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setVisible(false)}
|
||||
className="absolute right-[-3px] top-1/2 z-50 -translate-y-1/2 transform rounded-l-none rounded-r border border-l border-gray-300 bg-white px-1 py-2 shadow-sm"
|
||||
|
||||
@@ -57,6 +57,40 @@ GisOlSelect.prototype = {
|
||||
width : 10,
|
||||
})
|
||||
});
|
||||
let focusedLineStyle = [
|
||||
new Style({
|
||||
stroke : new Stroke({
|
||||
color : 'rgba(88, 28, 135, 0.26)',
|
||||
width : 18,
|
||||
lineCap : 'round',
|
||||
lineJoin : 'round',
|
||||
}),
|
||||
}),
|
||||
new Style({
|
||||
stroke : new Stroke({
|
||||
color : 'rgba(255, 255, 255, 0.96)',
|
||||
width : 12,
|
||||
lineCap : 'round',
|
||||
lineJoin : 'round',
|
||||
}),
|
||||
}),
|
||||
new Style({
|
||||
stroke : new Stroke({
|
||||
color : 'rgba(109, 40, 217, 0.98)',
|
||||
width : 8,
|
||||
lineCap : 'round',
|
||||
lineJoin : 'round',
|
||||
}),
|
||||
}),
|
||||
new Style({
|
||||
stroke : new Stroke({
|
||||
color : 'rgba(216, 180, 254, 0.92)',
|
||||
width : 3,
|
||||
lineCap : 'round',
|
||||
lineJoin : 'round',
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
this.selectInteraction = new Select({
|
||||
style: function (feature, resolution) {
|
||||
@@ -72,7 +106,12 @@ GisOlSelect.prototype = {
|
||||
styles.push(style);
|
||||
}
|
||||
}
|
||||
styles.push(baseStyle);
|
||||
const geometryType = feature?.getGeometry?.()?.getType?.();
|
||||
if (geometryType === 'LineString' || geometryType === 'MultiLineString') {
|
||||
styles.push(...focusedLineStyle);
|
||||
} else {
|
||||
styles.push(baseStyle);
|
||||
}
|
||||
return styles;
|
||||
},
|
||||
layers: function (layer) {
|
||||
|
||||
@@ -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