Merge branch 'iipbs/sn/dev'

This commit is contained in:
2026-04-20 18:12:56 +09:00
86 changed files with 2796 additions and 3706 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
#NEXT_PUBLIC_SERVER_API_URL = http://localhost:8883/operation-service
+1 -1
View File
@@ -16,4 +16,4 @@ NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com/dc-service
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
-1
View File
@@ -17,4 +17,3 @@ NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/dc-s
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000/sewerage-service
NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact
NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x}
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
+2
View File
@@ -0,0 +1,2 @@
import { LoginPage } from "@/src/features/auth/login";
export default LoginPage;
+46 -2
View File
@@ -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;
}
}
+5 -1
View File
@@ -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:",
+1
View File
@@ -0,0 +1 @@
export { LoginPage } from "./ui/login-page";
@@ -0,0 +1,139 @@
"use client";
import Image from "next/image";
import { useEffect } from "react";
import { images } from "@common/assets";
import { LoginApi } from "@/src/entities/auth";
import "./login.css";
function LoginPage() {
useEffect(() => {
const hostname = window.location.hostname;
if (hostname.includes("hasuon.or.kr")) {
window.location.href =
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
}
}, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const userId = formData.get("id") as string;
const password = formData.get("password") as string;
await LoginApi.login({ userId, password });
window.location.href = "/pages/dashboard";
};
return (
<div className="login fx">
<div className="left_box fx">
<div className="text">
<h1>
하수도정보시스템
<br />
<span>CMS 통합관리시스템</span>
</h1>
<p>하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.</p>
</div>
<div className="img">
<Image src={images.logo} width={229} height={46} alt="logo" />
</div>
</div>
<form
id="loginForm"
name="loginForm"
method="post"
onSubmit={handleSubmit}
>
<div className="login_box fx">
<ul className="fx">
<li>
<h2>ID</h2>
<div className="input_st st1">
<i className="diveicon di-account-box" />
<input
type="text"
className="id"
title="아이디를 입력하세요."
id="id"
name="id"
maxLength={12}
/>
</div>
</li>
<li>
<h2>PASSWORD</h2>
<div className="input_st st2">
<i className="diveicon di-key" />
<input
type="password"
className="input_style"
maxLength={25}
title="비밀번호를 입력하세요."
id="password"
name="password"
/>
</div>
</li>
</ul>
<div className="bottom_box fx">
<div className="checks fx">
<input
type="checkbox"
name="checkId"
title="로그인ID 저장여부"
id="checkId"
/>
<label htmlFor="checkId">아이디저장</label>
</div>
{/* <a href="javascript:fnUserSearch();" className=" pw_message fx"><i className="xi-external-link"></i>비밀번호 또는 아이디를 잃어버리셨나요?</a> */}
</div>
<button type="submit" className="login_btn fx">
<span>LOGIN</span>
</button>
<div className="footer">
<div className="copy fx">
<p>copyright © 2023 KECO. All rights reserved.</p>
</div>
<div className="fot_btn fx">
<a
href="https://keco.or.kr/web/lay1/S1T6C1348/contents.do"
target="_blank"
>
개인정보처리방침
</a>
<a
href="https://keco.or.kr/web/lay1/S1T6C1349/contents.do"
target="_blank"
>
저작권정책안내
</a>
<a
href="https://keco.or.kr/web/lay1/S1T6C1350/contents.do"
target="_blank"
>
이메일무단수집거부
</a>
</div>
<p className="addr fx">
<span>
주소 : (22689) 인천광역시 서구 환경로 42(경서동
종합환경연구단지)
</span>
<span>
전화 : (시스템) 032-590-4321, 3752, 4314 / (통 계) 032-590-3723,
3722
</span>
<span>팩스 :032-590-3729</span>
</p>
</div>
</div>
</form>
</div>
);
}
export { LoginPage };
@@ -0,0 +1,310 @@
/**
* OLYM Cascading Style Sheets
* File Name : login.css
* Description : 로그인 스타일시트
* Author : Web business Team / Choi Hyun Mi
* Email : desingtj@olym.co.kr
* Date : 2023.07.20
* Update : 2023.07.26
* Copyright(c) 2023 OLYM Communications. All Rights Reserved.
*/
html,
body {
background: #fff;
}
.fx {
display: flex;
-ms-display: flex;
-webkit-display: flex;
box-sizing: border-box;
}
.login {
width: 100%;
height: 100vh;
}
.login .left_box {
width: 100%;
background: url(/images/bg_login.jpg) no-repeat center/cover;
flex-direction: column;
justify-content: space-between;
padding: 7% 5% 5% 5%;
}
.login .left_box .text h1 {
font-size: 53px;
color: #fff;
}
.login .left_box .text h1 span {
font-weight: 800;
}
.login .left_box .text p {
margin-top: 40px;
font-size: 18px;
color: #fff;
}
.login .login_box {
width: 100%;
flex-direction: column;
justify-content: center;
padding: 4% 12% 4% 6%;
}
.login .login_box > ul {
flex-direction: column;
gap: 40px;
}
.login .login_box > ul > li h2 {
font-size: 22px;
font-weight: 600;
color: #2640a5;
margin-bottom: 14px;
}
.login .login_box > ul > li .input_st {
position: relative;
}
.login .login_box > ul > li .input_st .diveicon {
position: absolute;
top: 50%;
left: 28px;
transform: translateY(-50%);
font-family: "Xeicon";
font-size: 25px;
color: #003288;
}
.login .login_box > ul > li input {
width: 100%;
padding: 20px 20px 20px 70px;
background: #f7f7f7;
box-sizing: border-box;
border: none;
border-radius: 5px;
font-size: 18px;
font-weight: 700;
color: #000;
height: auto;
}
.login .login_box > ul > li input:focus {
outline: 1px solid #3c5fe5;
}
.login .login_box > ul > li input::placeholder {
color: #afabab;
font-weight: 500;
}
.login .login_box .bottom_box {
justify-content: space-between;
gap: 20px;
padding: 50px 0 45px 0;
}
.login .login_box .bottom_box .checks {
display: flex;
align-items: center;
gap: 8px;
}
.login .login_box .bottom_box .checks input[type="checkbox"] {
width: 18px;
height: 18px;
}
.login .login_box .bottom_box .checks label {
font-size: 14px;
}
.login .login_box .bottom_box .pw_message {
align-items: center;
gap: 5px;
color: #636363;
font-weight: 500;
border-bottom: 1px solid #ccc;
padding-bottom: 5px;
}
.login .login_box .bottom_box .pw_message i {
color: #327af5;
font-size: 20px;
}
.login .login_box .login_btn {
position: relative;
border-radius: 5px;
padding: 24px 15px;
background: linear-gradient(
to left,
#1d3969 0%,
#264078 10%,
#172558 40%,
#162254 50%,
#111c43 60%,
#111c42 100%
);
overflow: hidden;
justify-content: center;
align-items: center;
gap: 10px;
color: #fff;
cursor: pointer;
}
.login .login_box .login_btn span {
font-family: "Montserrat";
font-size: 19px;
font-weight: 700;
position: relative;
}
.login .login_box .login_btn i {
font-size: 22px;
transform: translateY(-2px);
}
.login .login_box .login_btn:before {
display: block;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
background: linear-gradient(
to right,
#1d3969 0%,
#264078 10%,
#172558 40%,
#162254 50%,
#111c43 60%,
#111c42 100%
);
transition: opacity 0.5s;
}
.login .login_box .login_btn:hover::before {
opacity: 1;
}
.login .footer .copy {
margin-top: 36px;
justify-content: space-between;
align-items: center;
gap: 3%;
}
.login .footer .copy p {
font-size: 13px;
color: #d4d4d4;
width: 44%;
text-align: center;
}
.login .footer .copy::before,
.login .login_box .copy::after {
content: "";
width: 20%;
height: 1px;
background: #d4d4d4;
}
.login .footer .fot_btn {
gap: 20px;
margin: 30px 0 10px 0;
}
.login .footer .fot_btn a {
color: #333;
font-weight: 700;
font-size: 13px;
}
.login .footer .fot_btn a:hover {
color: #003288;
}
.login .footer .addr {
color: #666;
font-size: 13px;
gap: 3px 20px;
flex-wrap: wrap;
}
/* 비밀번호 변경 팝업창 */
.login .modal .modal_container {
width: 750px;
}
.login .pw_desc {
padding: 10px 10px 30px 10px;
color: #666;
}
.login .pw_desc p {
font-size: 1.25rem;
color: #000;
font-weight: 700;
margin-bottom: 10px;
}
.login .pw_desc span {
color: #f60;
font-weight: 700;
}
.login .modal .form_write {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 20px 50px;
}
@media (max-width: 1024px) {
.login {
flex-direction: column;
height: auto;
}
.login .left_box {
padding: 30px;
flex-direction: column-reverse;
gap: 20px;
}
.login .left_box .text h1 {
font-size: 30px;
line-height: 1.2em;
}
.login .left_box .text p {
margin-top: 20px;
font-size: 14px;
}
.login .left_box .img img {
height: 40px;
}
.login .login_box {
padding: 30px;
}
.login .login_box > ul {
gap: 20px;
}
.login .login_box > ul > li h2 {
margin-bottom: 7px;
}
.login .login_box > ul > li .input_st::after {
left: 15px;
font-size: 20px;
}
.login .login_box > ul > li input {
padding: 15px 10px 15px 50px;
font-size: 16px;
}
.login .login_box .bottom_box {
flex-direction: column;
align-items: flex-start;
padding: 30px 0;
}
.login .login_box .bottom_box .id_save label {
font-size: 14px;
}
.login .login_box .login_btn {
padding: 15px;
}
.login .login_box .login_btn span {
font-size: 16px;
}
.login .login_box .login_btn i {
font-size: 18px;
transform: translateY(-1px);
}
.login .login_box .copy p {
width: 100%;
}
.login .login_box .copy::before,
.login .login_box .copy::after {
display: none;
}
.login .footer .addr {
font-size: 11px;
gap: 3px 10px;
}
.login .modal .modal_container {
width: 90%;
}
.login .pw_desc {
padding: 0 0 20px 0;
}
.login .modal .form_write {
padding: 10px;
}
}
/* // login */
+2 -2
View File
@@ -14,5 +14,5 @@ NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&ver
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /
NEXT_PUBLIC_SEWERAGE_API_URL=http://localhost:8883
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883
+1 -1
View File
@@ -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
-1
View File
@@ -15,4 +15,3 @@ NEXT_PUBLIC_NGII_URL = http://172.29.143.87/poiact
NEXT_PUBLIC_NGII_PARAM = BDE2BEF39DAFEC293AB6730150F222AC&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={y}&tilerow={x}
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-was.svc.cluster.local:8000
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
@@ -18,7 +18,7 @@ import { Button, TableHead, TableRow } from '@common/components/ui';
import React, { useState, useEffect } from 'react';
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
import { VerticalTable } from '@/components/ui/verticalTable';
import { getData } from "@/components/lib/api";
import { getRawJson } from "@/components/lib/api";
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import { Label } from '@radix-ui/react-label';
@@ -65,22 +65,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
size: '3',
});
let result;
const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') result = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') result = null;
// JSON 파싱 시도
result = JSON.parse(text);
}
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`);
// helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM"
const formatNoYearNoSec = (val: any) => {
@@ -138,22 +123,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
size: '3',
});
let result;
const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') result = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') result = null;
// JSON 파싱 시도
result = JSON.parse(text);
}
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`);
// 각 아이템 전처리
const processedDataArray = result.content.map((data: any) => {
@@ -355,4 +325,4 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
)
}
export default Ssc
export default Ssc
@@ -15,40 +15,20 @@
import { VerticalTable } from '@/components/ui/verticalTable';
import { useEffect, useState } from 'react';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { getData } from '@/components/lib/api';
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
const BASE_URL = "/sewerage-service/api";
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
method: "GET",
});
let data: any = null;
if (res.ok) {
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get("content-length");
if (contentLength === "0") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
try {
data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`);
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === "") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// JSON 파싱 시도
const response = JSON.parse(text);
data = response.data;
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
} else {
if (!data) {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
@@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: ()
)
}
export default Smh;
export default Smh;
+2 -17
View File
@@ -18,7 +18,7 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { getData } from "../../components/lib/api";
import { getRawJson } from "../../components/lib/api";
import { useCallback, useEffect, useRef, useState } from "react";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import SearchModal from "./searchModal/searchModal";
@@ -43,22 +43,7 @@ const Map = () => {
const fetchData = async () => {
try {
let data;
const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') data = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') data = null;
// JSON 파싱 시도
data = JSON.parse(text);
}
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
setTreeData(data);
} catch (error) {
console.error('JSON parsing error:', error);
@@ -21,6 +21,7 @@
// FileUpload.js
import React, { useState } from 'react';
import axios from 'axios';
import { SITE_ID } from '@/shared/config';
const SEWERAGE_URL = "/sewerage-service/api";
function FileUpload() {
@@ -49,6 +50,7 @@ function FileUpload() {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Site-Id": SITE_ID,
},
body: JSON.stringify({authToken: auth})
}).then((data) => {
@@ -58,7 +60,11 @@ function FileUpload() {
const handleFindUserOna = () => {
const auth = getCookie("Authorization");
if (auth) console.log("쿠키 auth : " + auth);
fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b").then((data) => {
fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b", {
headers: {
"X-Site-Id": SITE_ID,
},
}).then((data) => {
console.log(data);
});
};
@@ -86,7 +92,11 @@ function FileUpload() {
formData.append('inpFile', file);
try {
const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData);
const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData, {
headers: {
"X-Site-Id": SITE_ID,
},
});
alert('업로드 성공: ' + response.data.message);
} catch (err) {
@@ -17,6 +17,7 @@ import { useState, useEffect } from 'react'
import LoadingOverlay from '@/components/ui/LoadingBar'
import { UITab, UiTable } from '@common/business_components/ui'
import * as XLSX from 'xlsx';
import { getBlobData } from '@/components/lib/api';
// import { Table, TableBody, TableHeader } from "@common/components/ui";
// import { HeadMakeFloodPre } from './headMakeFloodPre';
// import { InputTableBody } from './inputTableBody';
@@ -54,12 +55,7 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
return false
}
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
if (!response.ok) {
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
}
const blob = await response.blob();
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
const file = new File([blob], fileNm, {
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
@@ -267,4 +263,4 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
)
};
export default TableModal;
export default TableModal;
@@ -19,6 +19,7 @@ import { UITab, UiTable } from '@common/business_components/ui'
import * as XLSX from 'xlsx';
import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan';
import { oldPpTable2HeadList, oldPpTable2NodeList, oldPpTable6HeadList, oldPpTable6NodeList } from './headList';
import { getBlobData } from '@/components/lib/api';
const TABS = [
{ val: "1", text: "raw data" },
@@ -63,12 +64,7 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
return false;
}
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
if (!response.ok) {
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
}
const blob = await response.blob();
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
const file = new File([blob], fileNm, {
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
@@ -476,4 +472,4 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
)
};
export default TableModal;
export default TableModal;
+2
View File
@@ -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)); // 공단 환경
+116 -9
View File
@@ -11,13 +11,51 @@
*/
import { useEffect, useState } from "react";
import axios from 'axios';
import { CLAIM_NAME, SITE_ID } from "@/shared/config";
import { tokenManager } from "@/entities/auth/lib/token-manager";
const BASE_URL = '/sewerage-service/api';
const DEFAULT_HEADERS = {
"X-Site-Id": SITE_ID,
};
const LOGIN_PAGE_URL =
process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login";
const withSiteIdHeaders = (headers: HeadersInit = {}) => ({
...headers,
...DEFAULT_HEADERS,
});
const resolveApiUrl = (url: string) =>
url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`;
const handleUnauthorizedResponse = () => {
if (typeof window === "undefined") return;
const remainingMs = tokenManager.getRemainingMsFromToken(CLAIM_NAME);
const isExpired = remainingMs === null || remainingMs <= 0;
if (!isExpired) return;
tokenManager.clearAuthToken();
const isLoginPage =
window.location.pathname === "/auth/login" ||
window.location.pathname.startsWith("/auth/login/");
if (!isLoginPage) {
window.location.assign(LOGIN_PAGE_URL);
}
};
export const getData = async (url: string): Promise<any> => {
const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, {
const res = await fetch(resolveApiUrl(url), {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
try {
// 응답이 비어있는지 확인
@@ -47,12 +85,15 @@ export const getData = async (url: string): Promise<any> => {
export const postData = async (url: string, data: object): Promise<any> => {
const options: RequestInit = {
method: "POST",
headers: {
headers: withSiteIdHeaders({
"Content-Type": "application/json",
},
}),
body: JSON.stringify(data),
};
const res = await fetch(`${BASE_URL}/${url}`, options);
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
const resData = await res.json();
console.log('postData response:', resData);
@@ -62,6 +103,7 @@ export const postData = async (url: string, data: object): Promise<any> => {
export const postMutiPart = async (url: string, data: FormData): Promise<any> => {
const res = await axios.post(`${BASE_URL}/${url}`, data, {
headers: DEFAULT_HEADERS,
timeout: 600000, // 10분
});
console.log('postMultiPart response:', res);
@@ -71,12 +113,15 @@ export const postMutiPart = async (url: string, data: FormData): Promise<any> =>
export const putData = async (url: string, data: object): Promise<any> => {
const options: RequestInit = {
method: "PUT",
headers: {
headers: withSiteIdHeaders({
"Content-Type": "application/json",
},
}),
body: JSON.stringify(data),
};
const res = await fetch(`${BASE_URL}/${url}`, options);
if (res.status === 401) {
handleUnauthorizedResponse();
}
const resData = await res.json();
console.log('putData response:', resData);
return resData.data;
@@ -85,21 +130,75 @@ export const putData = async (url: string, data: object): Promise<any> => {
export const deleteData = async (url: string, data?: object): Promise<any> => {
const options: RequestInit = {
method: "DELETE",
headers: {
headers: withSiteIdHeaders({
"Content-Type": "application/json",
},
}),
body: data ? JSON.stringify(data) : undefined,
};
const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, options);
const res = await fetch(resolveApiUrl(url), options);
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.status === 204) return true
return res;
};
export const getRawJson = async (url: string): Promise<any> => {
const res = await fetch(resolveApiUrl(url), {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (!res.ok) {
throw new Error('Failed to fetch data');
}
const contentLength = res.headers.get('content-length');
if (contentLength === '0') {
return null;
}
const text = await res.text();
if (!text || text.trim() === '') {
return null;
}
try {
return JSON.parse(text);
} catch (error) {
console.error('JSON parsing error:', error);
throw new Error('Invalid JSON response from server');
}
};
export const getBlobData = async (url: string): Promise<Blob> => {
const res = await fetch(resolveApiUrl(url), {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.blob();
};
// DC API 호출
export const getDcData = async (url: string, resType: string = "json"): Promise<any> => {
const res = await fetch('/dc-service' + url, {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
let data;
if (resType === "json") data = await res.json();
@@ -114,7 +213,11 @@ export const getDcData = async (url: string, resType: string = "json"): Promise<
export const getCmsData = async (url: string, data?: object): Promise<any> => {
const res = await fetch('/cms-service' + url, {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
const data = await res.json();
return data;
@@ -126,7 +229,11 @@ export const getCmsData = async (url: string, data?: object): Promise<any> => {
export const getGeoData = async (url: string, data?: object): Promise<any> => {
const res = await fetch('/geoserver' + url, {
method: "GET",
headers: withSiteIdHeaders(),
});
if (res.status === 401) {
handleUnauthorizedResponse();
}
if (res.ok) {
const data = await res.json();
return data;
@@ -157,4 +264,4 @@ export const useCookieAuth = () => {
}, []);
return data;
};
};
@@ -13,6 +13,7 @@
* - 첨부파일 삭제및 Soft Delete 삭제(첨부파일코드로 삭제): DELETE /api/v1/atfl/attachments/atflCd/{atflCd} (attach-service)
*/
import { getCookie } from "@/shared/lib";
import { SITE_ID } from "@/shared/config";
import { ENDPOINT } from "../api/attachment/attachment-service";
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
@@ -43,6 +44,7 @@ export const handleDelete = async (
method: "DELETE",
headers: {
"Content-Type": "application/json",
"X-Site-Id": SITE_ID,
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
},
@@ -118,4 +120,4 @@ export const handleDelete = async (
// options?.handleDeleteError?.(deleteError);
// throw deleteError;
// }
// };
// };
@@ -10,10 +10,15 @@
* - Fetch 기반 HTTP 요청 처리
*/
import { SITE_ID } from "@/shared/config";
export const handleDownload = async (api: string, fileNm: string) => {
try {
const response = await fetch(api, {
method: 'GET',
headers: {
"X-Site-Id": SITE_ID,
},
});
if (!response.ok) throw new Error('파일 다운로드 실패');
@@ -34,4 +39,4 @@ export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터
// 메모리 해제
link.remove();
window.URL.revokeObjectURL(url);
}
}
@@ -7,7 +7,7 @@ import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
import Stroke from "ol/style/Stroke.js";
import Style from "ol/style/Style.js";
import useStore from "@/store/useStore";
import { getData } from "@/components/lib/api";
import { getRawJson } from "@/components/lib/api";
import AgedPipeFilterPanel from "./AgedPipeFilterPanel";
import AgedPipeDetailPanel from "./AgedPipeDetailPanel";
import AgedPipeListModal from "./AgedPipeListModal";
@@ -102,19 +102,7 @@ const Map = () => {
useEffect(() => {
const fetchData = async () => {
try {
let data;
const response = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
method: "GET",
});
if (response.ok) {
const contentLength = response.headers.get("content-length");
if (contentLength === "0") data = null;
const text = await response.text();
if (!text || text.trim() === "") data = null;
else data = JSON.parse(text);
}
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
setTreeData(data);
} catch (error) {
console.error("노후관로 레이어 설정 조회 중 오류:", error);
@@ -18,7 +18,7 @@ import { Button, TableHead, TableRow } from '@common/components/ui';
import React, { useState, useEffect } from 'react';
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
import { VerticalTable } from '@/components/ui/verticalTable';
import { getData } from "@/components/lib/api";
import { getRawJson } from "@/components/lib/api";
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import { Label } from '@radix-ui/react-label';
@@ -65,22 +65,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
size: '3',
});
let result;
const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') result = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') result = null;
// JSON 파싱 시도
result = JSON.parse(text);
}
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`);
// helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM"
const formatNoYearNoSec = (val: any) => {
@@ -138,22 +123,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
size: '3',
});
let result;
const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') result = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') result = null;
// JSON 파싱 시도
result = JSON.parse(text);
}
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`);
// 각 아이템 전처리
const processedDataArray = result.content.map((data: any) => {
@@ -355,4 +325,4 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
)
}
export default Ssc
export default Ssc
@@ -15,40 +15,20 @@
import { VerticalTable } from '@/components/ui/verticalTable';
import { useEffect, useState } from 'react';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { getData } from '@/components/lib/api';
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
const BASE_URL = "/sewerage-service/api";
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
method: "GET",
});
let data: any = null;
if (res.ok) {
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get("content-length");
if (contentLength === "0") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
try {
data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`);
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === "") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// JSON 파싱 시도
const response = JSON.parse(text);
data = response.data;
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
} else {
if (!data) {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
@@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: ()
)
}
export default Smh;
export default Smh;
+2 -17
View File
@@ -18,7 +18,7 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { getData } from "../../../components/lib/api";
import { getRawJson } from "../../../components/lib/api";
import { useCallback, useEffect, useRef, useState } from "react";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import SearchModal from "./searchModal/searchModal";
@@ -43,22 +43,7 @@ const Map = () => {
const fetchData = async () => {
try {
let data;
const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') data = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') data = null;
// JSON 파싱 시도
data = JSON.parse(text);
}
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
setTreeData(data);
} catch (error) {
console.error('JSON parsing error:', error);
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

+12 -1
View File
@@ -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();
+1
View File
@@ -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 {
+1 -2
View File
@@ -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);
+3
View File
@@ -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,
},
});
+164
View File
@@ -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부터 시작
+6 -5
View File
@@ -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: {
+106 -52
View File
@@ -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] || [];
+2
View File
@@ -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,
+31 -21
View File
@@ -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;
}
};
+27 -151
View File
@@ -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 },
];
+28 -6
View File
@@ -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),
});
});
+48 -45
View File
@@ -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[]=[
},
]
]
+38 -10
View File
@@ -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 = (
}));
}
}
}
}
+56 -10
View File
@@ -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();
+96 -84
View File
@@ -2,13 +2,18 @@ import { numSize } from "@/app/store/common/code/basicCode";
import type { TableUpperProps } from "@/app/store/common/type/header";
import type { rainChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rainChkgdn";
import type { rswtChkZodType } from "@/app/store/prfmncReg/chkgdnReg/rswrtChkgdn";
import { Checkbox, Input, TableCell, TableRow } from "@common/components/ui";
import {
Checkbox,
Input,
TableCell,
TableRow,
} from "@common/components/ui";
import moment from "moment";
import {
codeToText,
formatCommaNumber,
formatYyyymmdd,
handleCommaNumberChange,
handleCommaNumberChange
} from "./common";
export const rainChkTableData = (chkData: rainChkZodType[]) => {
@@ -68,10 +73,10 @@ export const rswtChkTableData = (chkData: rswtChkZodType[]) => {
root.rswtrDetails.forEach((detail) => {
let flatRswtChk = {};
const findGuide = root.guideDetails.find(
(guide) => guide.utztnUsgCd === detail.utztnUsgCd,
);
(guide) => guide.utztnUsgCd === detail.utztnUsgCd
)
const findPoluttion = root.rswtrPollutions.find(
(polution) => polution.utztnUsgCd === detail.utztnUsgCd,
(polution) => polution.utztnUsgCd === detail.utztnUsgCd
);
flatRswtChk = {
...root,
@@ -79,7 +84,7 @@ export const rswtChkTableData = (chkData: rswtChkZodType[]) => {
...findGuide,
...findPoluttion,
...flatRswtChk,
utztnUsgCdText: codeToText(detail.utztnUsgCd, "dmdUszCd"),
utztnUsgCdText: codeToText(detail.utztnUsgCd, 'dmdUszCd'),
};
makeRswtData.push(flatRswtChk);
});
@@ -96,17 +101,18 @@ export const chRowspanTable = (
handleChange: (
rowIndex: number,
field: keyof (typeof makeTableData)[0],
value: string | number,
value: string | number
) => void,
toggleSelection: (pk: number, checked: boolean) => void,
selectedIds: number[],
handleCellClick: (index: number, headId: string) => void,
disable?: boolean,
disable?: boolean
) => {
let order = 0;
// rowspan된 셀 렌더링 여부 추적용
const renderedMap = new Map<string, Set<number>>();
// console.log(makeTableData);
return (
<>
{/* {makeTableData.map((row, rowIndex) => ( */}
@@ -135,11 +141,7 @@ export const chRowspanTable = (
key={key}
rowSpan={rowspan}
className="align-middle text-center whitespace-normal break-words"
style={{
width: head.widthPercent
? `${head.widthPercent}%`
: undefined,
}}
style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }}
>
{key === "checkBox" ? (
<Checkbox
@@ -152,16 +154,16 @@ export const chRowspanTable = (
) : key === "order" ? (
++order
) : // value
["anlOperCst", "dtfcltCpct", "prcsCpct"].includes(
//연간운영비, 저류조용량, 처리용량
head.id,
["anlOperCst", "dtfcltCpct", "prcsCpct"].includes( //연간운영비, 저류조용량, 처리용량
head.id
) ? (
formatCommaNumber(value)
) : key === "chckYmd" ? (
<>{formatYyyymmdd(value)}</>
) : (
<>{value}</>
)}
formatCommaNumber(value)
) : key === "chckYmd" ? (
<>{formatYyyymmdd(value)}</>
) :
(
<>{value}</>
)}
</TableCell>
);
}
@@ -172,11 +174,7 @@ export const chRowspanTable = (
key={key}
className="text-center whitespace-normal break-words"
onClick={() => handleCellClick(rowIndex, head.id)}
style={{
width: head.widthPercent
? `${head.widthPercent}%`
: undefined,
}}
style={{ width: head.widthPercent ? `${head.widthPercent}%` : undefined }}
>
{head.edit === true ? (
[
@@ -190,7 +188,7 @@ export const chRowspanTable = (
type={head.type}
onChange={(e) => {
handleCommaNumberChange(e, (val) =>
handleChange(rowIndex, head.id, val),
handleChange(rowIndex, head.id, val)
);
}}
value={formatCommaNumber(value) || ""}
@@ -198,10 +196,7 @@ export const chRowspanTable = (
maxLength={numSize}
onInput={(e: React.FormEvent<HTMLInputElement>) => {
const el = e.currentTarget;
if (
head.type === "number" &&
el.value.length > numSize
) {
if (head.type === "number" && el.value.length > numSize) {
el.value = el.value.slice(0, numSize);
}
}}
@@ -234,10 +229,7 @@ export const chRowspanTable = (
maxLength={numSize}
onInput={(e: React.FormEvent<HTMLInputElement>) => {
const el = e.currentTarget;
if (
head.type === "number" &&
el.value.length > numSize
) {
if (head.type === "number" && el.value.length > numSize) {
el.value = el.value.slice(0, numSize);
}
}}
@@ -246,12 +238,12 @@ export const chRowspanTable = (
) : ["smll"].includes(head.id) && head.type === "text" ? (
<>-</>
) : [
"totalSplyUswtrUtztnQnt",
"totalRuseFcltUtztnQnt",
"splyUswtrUtztnQnt",
"ruseFcltUtztnQnt",
"levyAmt",
].includes(head.id) ? (
"totalSplyUswtrUtztnQnt",
"totalRuseFcltUtztnQnt",
"splyUswtrUtztnQnt",
"ruseFcltUtztnQnt",
"levyAmt",
].includes(head.id) ? (
<span
className={
[
@@ -290,7 +282,7 @@ export const chRowspanTable = (
export const headVisibleChange = (
headList: TableUpperProps[],
isEdit: boolean,
isEdit: boolean
) => {
return headList.forEach((head) => {
if (head.edit !== undefined) head.edit = isEdit;
@@ -304,7 +296,7 @@ const handleChange = (
rowIndex: number,
field: any,
value: string,
setInputData: (data: any) => void,
setInputData: (data: any) => void
) => {
if (Number(value) < 0 || !Number(value)) {
value = "0";
@@ -318,16 +310,36 @@ const handleChange = (
};
}
return row;
}),
})
);
};
const handleTextChange = (
rowIndex: number,
field: any,
value: string,
setInputData: (data: any) => void
) => {
setInputData((prevData: any) =>
prevData?.map((row: any, index: number) => {
if (index === rowIndex) {
return {
...row,
[field]: value,
};
}
return row;
})
);
};
// 재이용실적등록 > 지도점검결과 등록 - 상세 modal의 body
export const InputTableBody = (
chgRes: any[] | [],
headList: TableUpperProps[],
setInputData: (data: any[]) => void,
isEditable: boolean = true,
isEditable: boolean = true
) => {
// mergeCell 처리용
const getRowSpan = (data: any[], key: string, rowIndex: number) => {
@@ -357,28 +369,14 @@ export const InputTableBody = (
return (
<TableRow key={dataIndex + "row"}>
{headList.map((head, headIndex) => {
//냄새면 계산 못하게 - 로 수정
// if (["smll"].includes(head.id)) {
// return (
// <TableCell
// key={head.id + headIndex}
// className={`w-36 ${bgClass} text-center !text-xl`}
// >
// <span>-</span>
// </TableCell>
// );
// }
const isTextColumn =
head.id === "구분" || head.id === "utztnUsgCdText";
const isTextColumn = head.id === "구분" || head.id === "utztnUsgCdText";
const textAlignClass = isTextColumn
? "text-center"
: "text-right";
// 총합계 행이면 "구분"에서 colSpan=2로 병합
if (
(data["구분"] === 0 || data["용수명"] === 0) &&
head.id === "구분"
) {
if ( (data["구분"] === 0 || data["용수명"] === 0) && head.id === "구분" ) {
return (
<TableCell
key={head.id + headIndex}
@@ -390,10 +388,7 @@ export const InputTableBody = (
);
}
// 총합계(합계) 행이면 "용수명" 컬럼은 렌더링하지 않음
if (
(data["구분"] === 0 || data["용수명"] === 0) &&
head.id === "용수명"
) {
if ( (data["구분"] === 0 || data["용수명"] === 0) && head.id === "용수명" ) {
return null;
}
@@ -433,22 +428,39 @@ export const InputTableBody = (
key={head.id + headIndex}
className={`w-36 ${bgClass}`}
>
<Input
// className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`}
className={`${textAlignClass} !text-xl h-10 w-full`}
onChange={(e) =>
handleCommaNumberChange(e, (val) =>
handleChange(
{head.id === "smll" ? (
<Input
className="text-center !text-xl h-10 w-full"
type="text"
onChange={(e) =>
handleTextChange(
dataIndex,
head.id,
String(val),
setInputData,
),
)
}
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
maxLength={numSize}
/>
e.target.value,
setInputData
)
}
value={String(data[head.id as keyof any] ?? "")}
maxLength={numSize}
/>
) : (
<Input
// className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`}
className={`${textAlignClass} !text-xl h-10 w-full`}
onChange={(e) =>
handleCommaNumberChange(e, (val) =>
handleChange(
dataIndex,
head.id,
String(val),
setInputData
)
)
}
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
maxLength={numSize}
/>
)}
</TableCell>
) : (
<TableCell
@@ -485,7 +497,7 @@ export const pbnstTableBody = (
chgRes: any[] | [],
headList: TableUpperProps[],
setInputData: (data: any[]) => void,
isEditable: boolean = true,
isEditable: boolean = true
) => {
// mergeCell 처리용
const getRowSpan = (data: any[], key: string, rowIndex: number) => {
@@ -599,8 +611,8 @@ export const pbnstTableBody = (
dataIndex,
head.id,
String(val),
setInputData,
),
setInputData
)
)
}
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
@@ -192,7 +192,7 @@ export default function ChkgdExcel({
}
})
.finally(() => setRswtChkLoading(false));
console.log("업로드파일 점검 완료");
// console.log("업로드파일 점검 완료");
}
}
}, [file]);
Binary file not shown.
Binary file not shown.
+10 -1
View 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 {
@@ -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>
)
});
+5 -5
View File
@@ -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 반환
}
}