Merge iipbs/sn/dev_kic into iipbs/sn/dev2_kic

This commit is contained in:
kiminchan
2026-04-27 16:56:40 +09:00
97 changed files with 5197 additions and 3807 deletions
+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 */
+1 -1
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_CM_LOGIN_PAGE_URL = /auth/login
NEXT_PUBLIC_SEWERAGE_API_URL=http://localhost: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
+13
View File
@@ -33,6 +33,15 @@ export default function MapLayout({
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
const { fetchCtpv, fetchSgg, fetchBIZ_SE_CD, fetchBIZ_TY_CD } = useStore();
const isPopupPage = pathname === "/map/aged-pipe-popup";
useEffect(() => {
document.body.classList.add("sn-map-page-lock");
return () => {
document.body.classList.remove("sn-map-page-lock");
};
}, []);
useEffect(() => {
fetchCtpv();
@@ -46,6 +55,10 @@ export default function MapLayout({
router.push(item.urlPath);
};
if (isPopupPage) {
return <>{children}</>;
}
return (
<NoFooterLayout
queryClient={queryClient}
@@ -0,0 +1,10 @@
import { Suspense } from "react";
import AgedPipePopupPage from "../components/agedPipe/AgedPipePopupPage";
export default function Page() {
return (
<Suspense fallback={null}>
<AgedPipePopupPage />
</Suspense>
);
}
@@ -0,0 +1,81 @@
"use client";
import { useEffect, useState } from "react";
import type { AgedPipeFilters } from "./agedPipeUtils";
type Props = {
filters: AgedPipeFilters;
onFiltersChange: (filters: AgedPipeFilters) => void;
onApply: (filters?: AgedPipeFilters) => void;
};
export default function AgedPipeLayerSettingPanel({
filters,
onFiltersChange,
onApply,
}: Props) {
const [isCollapsed, setIsCollapsed] = useState(false);
const [elapsedYearsText, setElapsedYearsText] = useState(String(filters.elapsedYears ?? ""));
useEffect(() => {
setElapsedYearsText(String(filters.elapsedYears ?? ""));
}, [filters.elapsedYears]);
const commitElapsedYears = () => {
const nextFilters = {
...filters,
elapsedYears: elapsedYearsText === "" ? ("" as unknown as number) : Number(elapsedYearsText),
};
onFiltersChange(nextFilters);
onApply(nextFilters);
};
return (
<div className="overflow-hidden rounded-sm border border-red-100 bg-red-50/35">
<div className="relative flex min-h-[30px] items-center justify-center border-b border-red-100 bg-white px-8">
<button
type="button"
className="absolute left-2 top-1/2 h-[18px] w-[18px] -translate-y-1/2 bg-transparent text-[1.2rem] leading-none text-[#555]"
onClick={() => setIsCollapsed((value) => !value)}
title={isCollapsed ? "펼치기" : "접기"}
>
{isCollapsed ? "›" : "⌄"}
</button>
<span className="text-[1.2rem] font-semibold text-red-700">노후관로 설정</span>
</div>
{!isCollapsed && (
<div className="px-2 py-2">
<div className="flex min-h-[30px] items-center gap-1 whitespace-nowrap text-[1.2rem] text-[#333]">
<label htmlFor="aged-pipe-elapsed-years">경과년도(년)</label>
<input
id="aged-pipe-elapsed-years"
className="h-[22px] w-14 border border-[#ccd6df] px-1 text-center text-[1.2rem]"
type="text"
inputMode="numeric"
pattern="[0-9]*"
autoComplete="off"
value={elapsedYearsText}
onFocus={(event) => event.currentTarget.select()}
onChange={(event) => {
const digits = event.target.value.replace(/[^0-9]/g, "");
setElapsedYearsText(digits);
}}
onKeyDown={(event) => {
if (event.key === "Enter") commitElapsedYears();
}}
/>
<button
type="button"
className="grid h-[22px] w-[22px] place-items-center rounded-full bg-[#31a83b] text-[1.4rem] font-bold leading-none text-white"
onClick={commitElapsedYears}
title="적용"
>
✓
</button>
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,304 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { Button, Input } from "@common/components";
import { UiTable, downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
import { getData } from "@/components/lib/api";
import useStore from "@/store/useStore";
import LoadingOverlay from "@/components/ui/LoadingBar";
import {
AGED_PIPE_DEFAULT_FILTERS,
AGED_PIPE_EXCEL_HEADER,
AGED_PIPE_LAYER_TYPE,
AGED_PIPE_LIST_HEADLIST,
AGED_PIPE_PAGE_SIZE,
buildAgedPipeCqlFilters,
normalizeAgedPipeFilters,
toDisplayPipeRow,
validateElapsedYears,
type AgedPipeFilters,
type AgedPipeRow,
} from "./agedPipeUtils";
const POPUP_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
const POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
const POPUP_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
export default function AgedPipePopupPage() {
const searchParams = useSearchParams();
const { ctpv, sgg, fetchCtpv, fetchSgg } = useStore();
const [filters, setFilters] = useState<AgedPipeFilters>(() =>
normalizeAgedPipeFilters({
elapsedYears: Number(searchParams.get("elapsedYears") ?? AGED_PIPE_DEFAULT_FILTERS.elapsedYears),
ctpvCd: searchParams.get("ctpvCd") ?? AGED_PIPE_DEFAULT_FILTERS.ctpvCd,
sggCd: searchParams.get("sggCd") ?? AGED_PIPE_DEFAULT_FILTERS.sggCd,
}),
);
const [rows, setRows] = useState<AgedPipeRow[]>([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [isLoading, setIsLoading] = useState(false);
const sggOptions = useMemo(
() =>
filters.ctpvCd === "0"
? sgg
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2))),
[filters.ctpvCd, sgg],
);
useEffect(() => {
if (ctpv.length === 0) fetchCtpv();
if (sgg.length === 0) fetchSgg();
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
const fetchRows = useCallback(
async (page: number, nextFilters: AgedPipeFilters, size: number) => {
const normalized = normalizeAgedPipeFilters(nextFilters);
const queryParams = new URLSearchParams({
lyrTypeNm: AGED_PIPE_LAYER_TYPE,
page: String(page - 1),
size: String(size),
sort: "string",
});
const cqlFilters = buildAgedPipeCqlFilters(normalized);
if (cqlFilters.length > 0) {
queryParams.append("cqlFiter", cqlFilters.join(" and "));
}
const response = await getData(`v1/fclts?${queryParams.toString()}`);
const responseRows = Array.isArray(response?.data) ? response.data : [];
return {
rows: responseRows.map((item: Record<string, unknown>, index: number) =>
toDisplayPipeRow(item, index, page, ctpv, sgg),
),
total: Number(response?.count ?? 0),
};
},
[ctpv, sgg],
);
const loadRows = useCallback(
async (page = 1, nextFilters = filters) => {
setIsLoading(true);
try {
const response = await fetchRows(page, nextFilters, AGED_PIPE_PAGE_SIZE);
setRows(response.rows);
setTotal(response.total);
setCurrentPage(page);
} catch (error) {
console.error("노후관로 목록 조회 중 오류:", error);
setRows([]);
setTotal(0);
} finally {
setIsLoading(false);
}
},
[fetchRows, filters],
);
useEffect(() => {
loadRows(1, filters);
// 최초 로딩만 수행한다. 이후 조회는 버튼/페이지 변경으로 처리한다.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const handleFilterSyncMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type !== POPUP_FILTER_SYNC_MESSAGE) return;
if (!event.data?.filters) return;
const normalized = normalizeAgedPipeFilters(event.data.filters as AgedPipeFilters);
setFilters(normalized);
setSelectedIndex(-1);
loadRows(1, normalized);
};
window.addEventListener("message", handleFilterSyncMessage);
return () => window.removeEventListener("message", handleFilterSyncMessage);
}, [loadRows]);
const handleSearch = (page = 1) => {
if (!validateElapsedYears(Number(filters.elapsedYears))) {
alert("경과년도는 1년~100년 범위 내에서 입력해주세요.");
return;
}
const normalized = normalizeAgedPipeFilters(filters);
setFilters(normalized);
loadRows(page, normalized);
};
const handleRowClick = (index?: number) => {
if (typeof index !== "number") return;
const row = rows[index];
if (!row) return;
setSelectedIndex(index);
window.opener?.postMessage(
{
type: POPUP_ROW_SELECT_MESSAGE,
row,
},
window.location.origin,
);
};
const handleDownload = async () => {
if (total === 0) {
alert("다운로드할 데이터가 없습니다.");
return;
}
try {
setIsLoading(true);
const response = await fetchRows(1, filters, 10000);
await downloadExcelWithKoreanHeaders({
data: response.rows as JSON[],
fileName: `노후관로_현황_${new Date().toISOString().slice(0, 10)}`,
sheetNameArray: ["노후관로 현황"],
headerRange: "A1:J1",
headerMapping: AGED_PIPE_EXCEL_HEADER,
});
} catch (error) {
console.error("노후관로 목록 다운로드 중 오류:", error);
alert("노후관로 목록 다운로드 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
const handleMinimize = () => {
window.opener?.postMessage(
{
type: POPUP_MINIMIZE_MESSAGE,
filters: normalizeAgedPipeFilters(filters),
total,
},
window.location.origin,
);
window.close();
};
return (
<main className="flex h-screen flex-col overflow-hidden bg-white">
<LoadingOverlay isLoading={isLoading} />
<header className="flex min-h-[44px] items-center justify-between border-b border-[#dbe2e6] bg-primary px-5 py-3">
<h1 className="text-[1.45rem] font-semibold text-white">노후관로 현황</h1>
<div className="flex items-center gap-2">
<Button
type="button"
color="white"
size="xs"
className="border border-white/30 px-3 py-1 text-[1.05rem] text-white"
onClick={handleMinimize}
>
최소화
</Button>
</div>
</header>
<section className="overflow-y-auto p-4">
<div className="grid grid-cols-1 gap-3 border-b border-slate-200 bg-slate-50 pb-4 md:grid-cols-[140px_minmax(0,1fr)_140px_minmax(0,1fr)_auto_auto]">
<label className="block">
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년도</div>
<Input
className="h-10 !text-[1.1rem]"
type="text"
inputMode="numeric"
pattern="[0-9]*"
autoComplete="off"
value={String(filters.elapsedYears ?? "")}
onChange={(event) => {
const digits = event.target.value.replace(/[^0-9]/g, "");
setFilters({
...filters,
elapsedYears: digits === "" ? ("" as unknown as number) : Number(digits),
});
}}
onKeyDown={(event) => {
if (event.key === "Enter") handleSearch(1);
}}
/>
</label>
<label className="block">
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시도</div>
<select
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
value={filters.ctpvCd}
onChange={(event) =>
setFilters({
...filters,
ctpvCd: event.target.value,
sggCd: "0",
})
}
>
<option value="0">전체</option>
{ctpv.map((item) => (
<option key={item.val} value={String(item.val)}>
{item.text}
</option>
))}
</select>
</label>
<label className="block">
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시군구</div>
<select
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
value={filters.sggCd}
onChange={(event) => setFilters({ ...filters, sggCd: event.target.value })}
>
<option value="0">전체</option>
{sggOptions.map((item) => (
<option key={item.val} value={String(item.val)}>
{item.text}
</option>
))}
</select>
</label>
<div className="flex flex-wrap gap-2 md:col-span-3 md:justify-end">
<Button
type="button"
size="sm"
color="primary"
className="h-10 px-5 !text-[1.1rem]"
onClick={() => handleSearch(1)}
>
조회
</Button>
<Button
type="button"
size="sm"
color="green"
className="h-10 px-5 !text-[1.1rem]"
onClick={handleDownload}
>
다운로드
</Button>
</div>
</div>
<UiTable<AgedPipeRow>
headName={<>노후관로 조회 결과 총 {total.toLocaleString()}건</>}
tableData={rows}
headlist={AGED_PIPE_LIST_HEADLIST}
pageSize={10}
total={total}
cellClick={(index) => handleRowClick(index)}
onPageChange={(page) => handleSearch(page)}
curPage={currentPage}
index={selectedIndex}
widthFixed={true}
/>
</section>
</main>
);
}
@@ -0,0 +1,236 @@
"use client";
import type { SelectDataType } from "@common/business_components";
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
import {
MS_SWAGE_PPL_CODES,
MS_SWAGE_PPL_USE_CODES,
} from "../common/table";
export const AGED_PIPE_LAYER_TYPE = "tb_ms_swage_ppl";
export const AGED_PIPE_DEFAULT_ELAPSED_YEARS = 30;
export const AGED_PIPE_PAGE_SIZE = 10;
export const AGED_PIPE_MIN_ZOOM = 14;
export const AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES = ["19000101", "19990101"];
export type AgedPipeFilters = {
elapsedYears: number;
ctpvCd: string;
sggCd: string;
};
export type AgedPipeRow = {
fid: string;
lyrTypeNm: string;
rowNumber: number;
fcltMngNo: string;
instlYmd: string;
pdmtVl: string | number;
prlgVl: string | number;
usgCd: string;
mqltCd: string;
elapsedYears: number | string;
ctpvNm?: string;
sggNm?: string;
[key: string]: unknown;
};
type PipeSource = Record<string, unknown>;
const useMap = new Map(MS_SWAGE_PPL_USE_CODES.map((item) => [item.val.trim(), item.text]));
const materialMap = new Map(MS_SWAGE_PPL_CODES.map((item) => [item.val.trim(), item.text]));
export const AGED_PIPE_DEFAULT_FILTERS: AgedPipeFilters = {
elapsedYears: AGED_PIPE_DEFAULT_ELAPSED_YEARS,
ctpvCd: "0",
sggCd: "0",
};
export const AGED_PIPE_LIST_HEADLIST = [
{ id: "rowNumber", title: "NO", widthPercent: 6 },
{ id: "fcltMngNo", title: "시설관리번호", widthPercent: 18 },
{ id: "instlYmd", title: "설치일자", widthPercent: 12 },
{ id: "pdmtVl", title: "관경(mm)", widthPercent: 10 },
{ id: "prlgVl", title: "연장(m)", widthPercent: 10 },
{ id: "usgCd", title: "용도", widthPercent: 14 },
{ id: "mqltCd", title: "재질", widthPercent: 18 },
{ id: "elapsedYears", title: "경과년도", widthPercent: 12, align: "right" },
];
export const AGED_PIPE_EXCEL_HEADER = {
rowNumber: "NO",
fcltMngNo: "시설관리번호",
instlYmd: "설치일자",
pdmtVl: "관경(mm)",
prlgVl: "연장(m)",
usgCd: "용도",
mqltCd: "재질",
elapsedYears: "경과년도",
ctpvNm: "시도",
sggNm: "시군구",
};
const pad2 = (value: number) => String(value).padStart(2, "0");
const normalizeYmd = (value: unknown) =>
String(value ?? "").replace(/[^0-9]/g, "").slice(0, 8);
const getToday = () => new Date();
export const normalizeAgedPipeFilters = (filters: AgedPipeFilters): AgedPipeFilters => {
const elapsedYears = Math.trunc(Number(filters.elapsedYears));
return {
...filters,
elapsedYears:
Number.isFinite(elapsedYears) && elapsedYears >= 1 && elapsedYears <= 100
? elapsedYears
: AGED_PIPE_DEFAULT_ELAPSED_YEARS,
};
};
export const validateElapsedYears = (value: number) =>
Number.isInteger(value) && value >= 1 && value <= 100;
export const getCutoffYmd = (elapsedYears: number) => {
const today = getToday();
const cutoff = new Date(today.getFullYear() - elapsedYears, today.getMonth(), today.getDate());
return `${cutoff.getFullYear()}${pad2(cutoff.getMonth() + 1)}${pad2(cutoff.getDate())}`;
};
export const getElapsedYears = (value: unknown) => {
const normalized = normalizeYmd(value);
if (normalized.length < 4 || isInvalidInstallYmd(value)) return null;
const year = Number(normalized.slice(0, 4));
const month = Number(normalized.slice(4, 6) || "1");
const day = Number(normalized.slice(6, 8) || "1");
if (!year) return null;
const today = getToday();
let elapsed = today.getFullYear() - year;
if (
today.getMonth() + 1 < month ||
(today.getMonth() + 1 === month && today.getDate() < day)
) {
elapsed -= 1;
}
return Math.max(elapsed, 0);
};
export const isInvalidInstallYmd = (value: unknown) => {
const normalized = normalizeYmd(value);
return (
normalized.length < 8 ||
AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES.some((prefix) => normalized.startsWith(prefix))
);
};
export const getElapsedYearsDisplayText = (value: unknown) => {
const elapsedYears = getElapsedYears(value);
return elapsedYears == null ? "확인불가" : `${elapsedYears}년`;
};
export const buildAgedPipeCqlFilters = (filters: AgedPipeFilters) => {
const normalized = normalizeAgedPipeFilters(filters);
const cqlFilters = [
"instl_ymd IS NOT NULL",
...AGED_PIPE_INVALID_INSTALL_YMD_PREFIXES.map((prefix) => `instl_ymd NOT LIKE '${prefix}%'`),
`instl_ymd <= '${getCutoffYmd(normalized.elapsedYears)}'`,
];
if (normalized.sggCd !== "0") {
cqlFilters.push(`pbadms_emd_cd LIKE '${normalized.sggCd}%'`);
} else if (normalized.ctpvCd !== "0") {
cqlFilters.push(`pbadms_emd_cd LIKE '${normalized.ctpvCd}%'`);
}
return cqlFilters;
};
const getRegionCode = (source: PipeSource) =>
String(source.pbadmsEmdCd ?? source.pbadms_emd_cd ?? "").trim();
export const matchesAgedPipeFilters = (source: PipeSource, filters: AgedPipeFilters) => {
const normalized = normalizeAgedPipeFilters(filters);
const instlYmd = normalizeYmd(source.instl_ymd ?? source.instlYmd);
if (!instlYmd || isInvalidInstallYmd(instlYmd) || instlYmd > getCutoffYmd(normalized.elapsedYears)) return false;
const regionCode = getRegionCode(source);
if (normalized.sggCd !== "0") return regionCode.startsWith(normalized.sggCd);
if (normalized.ctpvCd !== "0") return regionCode.startsWith(normalized.ctpvCd);
return true;
};
const resolveRegionName = (
code: string,
directName: unknown,
options: SelectDataType[],
prefixLength: number,
) => {
const direct = String(directName ?? "").trim();
if (direct) return direct;
const prefix = code.slice(0, prefixLength);
return options.find((item) => String(item.val).trim() === prefix)?.text ?? "";
};
export const toDisplayPipeRow = (
source: PipeSource,
index: number,
page: number,
ctpvOptions: SelectDataType[],
sggOptions: SelectDataType[],
): AgedPipeRow => {
const formatted = formatDisplayData(source) as PipeSource;
const codePrefix = getRegionCode(source);
const instlYmd = formatted.instlYmd ?? source.instlYmd ?? formatted.instl_ymd ?? source.instl_ymd;
const invalidInstallYmd = isInvalidInstallYmd(source.instlYmd ?? source.instl_ymd ?? instlYmd);
const trimmedUse = String(source.usgCd ?? source.usg_cd ?? "").trim();
const trimmedMaterial = String(source.mqltCd ?? source.mqlt_cd ?? "").trim();
const pdmtVl = formatted.pdmtVl ?? source.pdmtVl ?? source.pdmt_vl ?? "-";
const prlgVl = formatted.prlgVl ?? source.prlgVl ?? source.prlg_vl ?? "-";
const fcltMngNo = String(formatted.fcltMngNo ?? source.fcltMngNo ?? source.fclt_mng_no ?? "-");
return {
...source,
...formatted,
fid: String(source.fid ?? ""),
lyrTypeNm: String(source.lyrTypeNm ?? AGED_PIPE_LAYER_TYPE),
rowNumber: (page - 1) * AGED_PIPE_PAGE_SIZE + index + 1,
fcltMngNo,
swtrmtFcltCd: source.swtrmtFcltCd ?? fcltMngNo,
instlYmd: invalidInstallYmd ? "확인불가" : String(instlYmd ?? "-"),
pdmtVl: typeof pdmtVl === "number" ? pdmtVl : String(pdmtVl),
prlgVl: typeof prlgVl === "number" ? prlgVl : String(prlgVl),
usgCd: useMap.get(trimmedUse) ?? String(formatted.usgCd ?? source.usgCd ?? source.usg_cd ?? "-"),
mqltCd:
materialMap.get(trimmedMaterial) ??
String(formatted.mqltCd ?? source.mqltCd ?? source.mqlt_cd ?? "-"),
elapsedYears: getElapsedYears(source.instlYmd ?? source.instl_ymd) ?? "확인불가",
ctpvNm: resolveRegionName(
codePrefix,
formatted.ctpvNm ?? source.ctpvNm ?? source.ctpv_nm,
ctpvOptions,
2,
),
sggNm: resolveRegionName(
codePrefix,
formatted.sggNm ?? source.sggNm ?? source.sgg_nm,
sggOptions,
5,
),
};
};
export const isAgedPipeLayerNode = (node: any) => {
const data = node?.data ?? node;
const layerName = String(data?.lyrNm ?? node?.text ?? "");
const layerTypeName = String(data?.lyrTyNm ?? "");
return (
layerTypeName === "tb_old_pipe" ||
layerName.includes("노후관로")
);
};
@@ -3,6 +3,7 @@ export const MsSwagePplCol = [
{ id: 'fclt_mng_no', title: '시설관리번호' },
{ id: 'inst_mng_no', title: '기관관리번호' },
{ id: 'instl_ymd', title: '설치일자' },
{ id: 'elapsedYears', title: '경과년도' },
{ id: 'mapsht_no', title: '도엽번호' },
{ id: 'pdmt_vl', title: '관경(mm)' },
{ id: 'prlg_vl', title: '연장(m)' },
@@ -18,7 +18,7 @@ import { Button, TableHead, TableRow } from '@common/components/ui';
import React, { useState, useEffect } from 'react';
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
import { VerticalTable } from '@/components/ui/verticalTable';
import { getData } from "@/components/lib/api";
import { getRawJson } from "@/components/lib/api";
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import { Label } from '@radix-ui/react-label';
@@ -65,22 +65,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
size: '3',
});
let result;
const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') result = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') result = null;
// JSON 파싱 시도
result = JSON.parse(text);
}
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`);
// helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM"
const formatNoYearNoSec = (val: any) => {
@@ -138,22 +123,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
size: '3',
});
let result;
const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') result = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') result = null;
// JSON 파싱 시도
result = JSON.parse(text);
}
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`);
// 각 아이템 전처리
const processedDataArray = result.content.map((data: any) => {
@@ -355,4 +325,4 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
)
}
export default Ssc
export default Ssc
@@ -41,6 +41,7 @@ import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import { codeMapper } from "../common/codeMapper";
import LoadingOverlay from '@/components/ui/LoadingBar';
import { searchUpperPpls } from "./smh";
import { getElapsedYearsDisplayText, isInvalidInstallYmd } from "../agedPipe/agedPipeUtils";
const linkedFacilityHighlightStyle = new Style({
image: new CircleStyle({
@@ -63,7 +64,6 @@ const linkedFacilityHighlightStyle = new Style({
zIndex: 9999,
});
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; };
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
@@ -88,7 +88,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
const linkedFacilityOriginalStyleRef = useRef<any>(null);
// 🎯 관로관리번호 추출
const pplMngNo = featureData?.values_?.fclt_mng_no || "";
const pplMngNo = featureData?.values_?.fclt_mng_no || featureData?.fcltMngNo || "";
const featureId = typeof featureData.getId === 'function'
? featureData.getId()
@@ -258,7 +258,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
case 'tb_ms_swage_ppl': {
result = '하수관로';
setHeadlist(MsSwagePplCol2)
setLayerSpecificNm(featureData.swtrmtFcltCd);
setLayerSpecificNm(featureData.fcltMngNo ?? featureData.swtrmtFcltCd);
break;
}
case 'tb_ms_swage_mhl': {
@@ -341,6 +341,14 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
// 🎯 기본 데이터 포맷팅
const formattedData = formatDisplayData(data);
if (selectedLayerName === '하수관로') {
const installYmd = data.instl_ymd ?? data.instlYmd;
formattedData.elapsedYears = getElapsedYearsDisplayText(installYmd);
if (isInvalidInstallYmd(installYmd)) {
formattedData.instl_ymd = "확인불가";
formattedData.instlYmd = "확인불가";
}
}
setFetchData(formattedData);
setTableData(formattedData);
@@ -407,8 +415,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
// 자체입력정보 API 엔드포인트 선택
let apiEndpoint = '';
const fcltMngNo = featureData?.values_?.fclt_mng_no || null;
const mngInstCd = featureData?.values_?.mng_inst_cd || null;
const fcltMngNo = featureData?.values_?.fclt_mng_no || featureData?.fcltMngNo || null;
const mngInstCd = featureData?.values_?.mng_inst_cd || featureData?.mngInstCd || null;
if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${fcltMngNo}/${mngInstCd}`;
else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${fcltMngNo}/${mngInstCd}`;
else if (selectedLayerName === '하수펌프장') apiEndpoint = `v1/fcltpump/${fcltMngNo}/${mngInstCd}`;
@@ -598,7 +606,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
</div>
<div className="overflow-y-auto overflow-x-hidden flex-1" ref={contentRef}>
<VerticalTable
headName='기본정보'
headName={
layerNm === '하수관로' && tableData?.elapsedYears
? <>기본정보 <span className="ml-2 rounded-sm bg-red-50 px-2 py-0.5 text-[1.1rem] font-semibold text-red-700">경과년도 {tableData.elapsedYears}</span></>
: '기본정보'
}
tableName1={layerNm === '중점우수토실' ? "측정지점" : undefined}
tableData={[tableData]}
headlist={headlist}
@@ -15,40 +15,20 @@
import { VerticalTable } from '@/components/ui/verticalTable';
import { useEffect, useState } from 'react';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { getData } from '@/components/lib/api';
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
const BASE_URL = "/sewerage-service/api";
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
method: "GET",
});
let data: any = null;
if (res.ok) {
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get("content-length");
if (contentLength === "0") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
try {
data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`);
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === "") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// JSON 파싱 시도
const response = JSON.parse(text);
data = response.data;
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
} else {
if (!data) {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
@@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: ()
)
}
export default Smh;
export default Smh;
+396 -24
View File
@@ -18,48 +18,130 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { getData } from "../../components/lib/api";
import { useCallback, useEffect, useRef, useState } from "react";
import { getData, getRawJson } from "../../components/lib/api";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import SearchModal from "./searchModal/searchModal";
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth";
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
import { Button } from "@common/components";
import useSearchModalStore from "@/store/useSearchModalStore";
import useStore from "@/store/useStore";
import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
import AgedPipeLayerSettingPanel from "./components/agedPipe/AgedPipeLayerSettingPanel";
import {
AGED_PIPE_EXCEL_HEADER,
AGED_PIPE_MIN_ZOOM,
AGED_PIPE_LAYER_TYPE,
AGED_PIPE_PAGE_SIZE,
buildAgedPipeCqlFilters,
isAgedPipeLayerNode,
matchesAgedPipeFilters,
normalizeAgedPipeFilters,
toDisplayPipeRow,
validateElapsedYears,
type AgedPipeFilters,
type AgedPipeRow,
AGED_PIPE_DEFAULT_FILTERS,
} from "./components/agedPipe/agedPipeUtils";
const AGED_PIPE_CONTROL_NODE_ID = "SN_AGED_PIPE";
const AGED_PIPE_HIGHLIGHT_LAYER_ID = "SN_AGED_PIPE_HIGHLIGHT";
const AGED_PIPE_ROW_SELECT_MESSAGE = "SN_AGED_PIPE_ROW_SELECT";
const AGED_PIPE_POPUP_MINIMIZE_MESSAGE = "SN_AGED_PIPE_POPUP_MINIMIZE";
const AGED_PIPE_FILTER_SYNC_MESSAGE = "SN_AGED_PIPE_FILTER_SYNC";
const AGED_PIPE_FOCUS_PADDING: [number, number, number, number] = [120, 120, 120, 120];
const DEFAULT_FEATURE_FIT_PADDING: [number, number, number, number] = [50, 50, 50, 50];
const appendAgedPipeControlNode = (data: any[]) => {
if (!Array.isArray(data)) return data;
if (data.some((item) => String(item.lyrSn) === AGED_PIPE_CONTROL_NODE_ID || item.lyrNm === "노후관로")) {
return data;
}
return [
...data,
{
lyrSn: AGED_PIPE_CONTROL_NODE_ID,
id: AGED_PIPE_CONTROL_NODE_ID,
lyrNm: "노후관로",
upperLyrSn: null,
upId: null,
sortOrdr: "4",
drawOrdr: null,
flterSql: null,
gobjTy: "",
lyrTyNm: "tb_old_pipe",
useYn: "Y",
minViewSize: null,
iconNm: null,
iconSize: null,
lblAtrbNm: null,
fontNm: null,
fontIntnColor: null,
fontExtnColor: null,
fontExtnWdth: null,
lnWdth: null,
lnColor: null,
polgnColor: null,
initEprssYn: "Y",
clickPsblYn: "N",
exprsYn: "Y",
cmncNm: "all",
srvcNm: "wfs",
actlLyrYn: "N",
clckZoom: null,
children: null,
},
];
};
const Map = () => {
const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore();
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const [treeData, setTreeData] = useState<any[]>([]); // 초기값을 객체 또는 null로 설정
const gisMaps = useRef<any | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const [featureData, setFeatureData] = useState<any>(null); // 선택된 피쳐 데이터
const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부
const [isPlaying, setIsPlaying] = useState(false);
const [mapReady, setMapReady] = useState(false);
const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false);
const [draftAgedPipeFilters, setDraftAgedPipeFilters] =
useState<AgedPipeFilters>(AGED_PIPE_DEFAULT_FILTERS);
const [appliedAgedPipeFilters, setAppliedAgedPipeFilters] =
useState<AgedPipeFilters>(AGED_PIPE_DEFAULT_FILTERS);
const [isAgedPipeListOpen, setIsAgedPipeListOpen] = useState(false);
const [isAgedPipeListMinimized, setIsAgedPipeListMinimized] = useState(false);
const [agedPipeRows, setAgedPipeRows] = useState<AgedPipeRow[]>([]);
const [agedPipeTotalCount, setAgedPipeTotalCount] = useState(0);
const [agedPipeCurrentPage, setAgedPipeCurrentPage] = useState(1);
const [isAgedPipeListLoading, setIsAgedPipeListLoading] = useState(false);
const { ctpv, sgg } = useStore();
const shouldFocusAgedPipeOnNextSelectRef = useRef(false);
const agedPipePopupWindowRef = useRef<Window | null>(null);
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
const timer = window.setInterval(() => {
if (gisMaps.current) {
setMapReady(true);
window.clearInterval(timer);
}
}, 300);
return () => window.clearInterval(timer);
}, []);
const fetchData = async () => {
try {
let data;
const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') data = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') data = null;
// JSON 파싱 시도
data = JSON.parse(text);
}
setTreeData(data);
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
setTreeData(appendAgedPipeControlNode(data));
} catch (error) {
console.error('JSON parsing error:', error);
// JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환
@@ -112,13 +194,23 @@ const Map = () => {
if (geometry) {
let extent = geometry.getExtent();
const layerTypeName = typeof foundFeature.getId === "function"
? String(foundFeature.getId()).split(".")[0]
: data?.lyrTypeNm;
const shouldUseAgedPipeFocus =
shouldFocusAgedPipeOnNextSelectRef.current && layerTypeName === AGED_PIPE_LAYER_TYPE;
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
padding: shouldUseAgedPipeFocus ? AGED_PIPE_FOCUS_PADDING : DEFAULT_FEATURE_FIT_PADDING,
maxZoom: shouldUseAgedPipeFocus ? 18 : 16,
constrainResolution: true,
nearest: true,
});
if (shouldUseAgedPipeFocus) {
shouldFocusAgedPipeOnNextSelectRef.current = false;
}
}
select.selectFeature(foundFeature, true);
} else {
@@ -131,6 +223,181 @@ const Map = () => {
};
const facilityTypes = [{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" }];
const fetchAgedPipeRows = useCallback(
async (page: number, filters: AgedPipeFilters, size: number) => {
const normalized = normalizeAgedPipeFilters(filters);
const queryParams = new URLSearchParams({
lyrTypeNm: AGED_PIPE_LAYER_TYPE,
page: String(page - 1),
size: String(size),
sort: "string",
});
const cqlFilters = buildAgedPipeCqlFilters(normalized);
if (cqlFilters.length > 0) {
queryParams.append("cqlFiter", cqlFilters.join(" and "));
}
const response = await getData(`v1/fclts?${queryParams.toString()}`);
const rows = Array.isArray(response?.data) ? response.data : [];
return {
rows: rows.map((item: Record<string, unknown>, index: number) =>
toDisplayPipeRow(item, index, page, ctpv, sgg),
),
total: Number(response?.count ?? 0),
};
},
[ctpv, sgg],
);
const loadAgedPipeList = useCallback(
async (page = 1, filters = appliedAgedPipeFilters) => {
setIsAgedPipeListLoading(true);
try {
const response = await fetchAgedPipeRows(page, filters, AGED_PIPE_PAGE_SIZE);
setAgedPipeRows(response.rows);
setAgedPipeTotalCount(response.total);
setAgedPipeCurrentPage(page);
} catch (error) {
console.error("노후관로 목록 조회 중 오류:", error);
setAgedPipeRows([]);
setAgedPipeTotalCount(0);
} finally {
setIsAgedPipeListLoading(false);
}
},
[appliedAgedPipeFilters, fetchAgedPipeRows],
);
const syncAgedPipePopupFilters = useCallback((filters: AgedPipeFilters) => {
const popup = agedPipePopupWindowRef.current;
if (!popup || popup.closed) {
agedPipePopupWindowRef.current = null;
return;
}
popup.postMessage(
{
type: AGED_PIPE_FILTER_SYNC_MESSAGE,
filters: normalizeAgedPipeFilters(filters),
},
window.location.origin,
);
}, []);
const handleApplyAgedPipeFilters = useCallback((filters = draftAgedPipeFilters) => {
if (!validateElapsedYears(Number(filters.elapsedYears))) {
alert("경과년도는 1년~100년 범위 내에서 입력해주세요.");
return;
}
const normalized = normalizeAgedPipeFilters(filters);
setDraftAgedPipeFilters(normalized);
setAppliedAgedPipeFilters(normalized);
syncAgedPipePopupFilters(normalized);
if (isAgedPipeListOpen) {
loadAgedPipeList(1, normalized);
}
}, [draftAgedPipeFilters, isAgedPipeListOpen, loadAgedPipeList, syncAgedPipePopupFilters]);
const openAgedPipePopupWindow = useCallback(() => {
const popupParams = new URLSearchParams({
elapsedYears: String(appliedAgedPipeFilters.elapsedYears),
ctpvCd: appliedAgedPipeFilters.ctpvCd,
sggCd: appliedAgedPipeFilters.sggCd,
});
const popup = window.open(
`/map/aged-pipe-popup?${popupParams.toString()}`,
"sn-aged-pipe-list",
"popup=yes,width=1180,height=760,left=120,top=80,resizable=yes,scrollbars=no",
);
if (!popup) {
alert("브라우저 팝업이 차단되었습니다. 팝업 허용 후 다시 시도해주세요.");
return null;
}
agedPipePopupWindowRef.current = popup;
setIsAgedPipeListOpen(true);
setIsAgedPipeListMinimized(false);
return popup;
}, [appliedAgedPipeFilters]);
const openAgedPipeList = useCallback(() => {
openAgedPipePopupWindow();
}, [openAgedPipePopupWindow]);
const handleAgedPipeRowDoubleClick = useCallback(
async (row: AgedPipeRow) => {
try {
shouldFocusAgedPipeOnNextSelectRef.current = true;
const core = GisApp.Module.layer as any;
await core.showSearchResultsFromGeoJSON(AGED_PIPE_LAYER_TYPE, row.fid);
} catch (error) {
shouldFocusAgedPipeOnNextSelectRef.current = false;
console.error("노후관로 위치 이동 중 오류:", error);
}
setFeatureData(row as any);
setIsOpen(true);
setIsSearchModalOpen(false);
},
[setIsSearchModalOpen],
);
const handleAgedPipeDownload = useCallback(async () => {
if (agedPipeTotalCount === 0) {
alert("다운로드할 데이터가 없습니다.");
return;
}
try {
setIsAgedPipeListLoading(true);
const response = await fetchAgedPipeRows(1, appliedAgedPipeFilters, 10000);
await downloadExcelWithKoreanHeaders({
data: response.rows as JSON[],
fileName: `노후관로_현황_${new Date().toISOString().slice(0, 10)}`,
sheetNameArray: ["노후관로 현황"],
headerRange: "A1:J1",
headerMapping: AGED_PIPE_EXCEL_HEADER,
});
} catch (error) {
console.error("노후관로 목록 다운로드 중 오류:", error);
alert("노후관로 목록 다운로드 중 오류가 발생했습니다.");
} finally {
setIsAgedPipeListLoading(false);
}
}, [agedPipeTotalCount, appliedAgedPipeFilters, fetchAgedPipeRows]);
useEffect(() => {
const handleAgedPipePopupMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type === AGED_PIPE_ROW_SELECT_MESSAGE) {
if (!event.data?.row) return;
handleAgedPipeRowDoubleClick(event.data.row as AgedPipeRow);
return;
}
if (event.data?.type === AGED_PIPE_POPUP_MINIMIZE_MESSAGE) {
agedPipePopupWindowRef.current = null;
if (event.data?.filters) {
const normalized = normalizeAgedPipeFilters(event.data.filters as AgedPipeFilters);
setDraftAgedPipeFilters(normalized);
setAppliedAgedPipeFilters(normalized);
}
setAgedPipeTotalCount(Number(event.data?.total ?? agedPipeTotalCount));
setIsAgedPipeListMinimized(true);
}
};
window.addEventListener("message", handleAgedPipePopupMessage);
return () => window.removeEventListener("message", handleAgedPipePopupMessage);
}, [agedPipeTotalCount, handleAgedPipeRowDoubleClick]);
// WaterDepth 표시 여부를 결정하는 함수
const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
const waterDepthLayers = checkedNodesData.filter(node =>
@@ -144,12 +411,83 @@ const Map = () => {
// 체크된 레이어 변경 처리 함수
const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
handleWaterDepthVisibility(checkedIds, checkedNodesData);
setIsAgedPipeChecked(checkedNodesData.some((node) => isAgedPipeLayerNode(node)));
}, [handleWaterDepthVisibility]);
const handleOpenLayerPopup = useCallback(
(_layerNm: string, node?: any) => {
if (isAgedPipeLayerNode(node)) {
openAgedPipeList();
return false;
}
return true;
},
[openAgedPipeList],
);
const inlineLayerSettingView = useCallback(
(node: any) => {
if (!isAgedPipeChecked || !isAgedPipeLayerNode(node)) return null;
return (
<AgedPipeLayerSettingPanel
filters={draftAgedPipeFilters}
onFiltersChange={setDraftAgedPipeFilters}
onApply={handleApplyAgedPipeFilters}
/>
);
},
[draftAgedPipeFilters, handleApplyAgedPipeFilters, isAgedPipeChecked],
);
useEffect(() => {
if (!mapReady || !gisMaps.current) return;
const core = GisApp.Module.core as any;
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
if (!isAgedPipeChecked) return;
const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find(
(item: any) => item?.lyrTyNm === AGED_PIPE_LAYER_TYPE && item?.actlLyrYn === "Y",
) as any;
if (!baseLayerCode) return;
const layerCode = {
...baseLayerCode,
id: AGED_PIPE_HIGHLIGHT_LAYER_ID,
lyrSn: AGED_PIPE_HIGHLIGHT_LAYER_ID,
lyrNm: "노후관로",
lyrTyNm: AGED_PIPE_LAYER_TYPE,
gobjTy: "line",
actlLyrYn: "Y",
exprsYn: "Y",
clickPsblYn: "Y",
cmncNm: "bbox",
srvcNm: "wfs",
flterSql: buildAgedPipeCqlFilters(appliedAgedPipeFilters).join(" and "),
lnWdth: "4",
lnColor: "rgba(235, 40, 40, 0.82)",
drawOrdr: "9999",
minViewSize: String(AGED_PIPE_MIN_ZOOM),
};
const layer = core?.addWfsLayer?.(layerCode);
layer?.set?.("id", AGED_PIPE_HIGHLIGHT_LAYER_ID);
layer?.set?.("lyrNm", "노후관로");
layer?.set?.("lyrTyNm", AGED_PIPE_LAYER_TYPE);
return () => {
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
};
}, [appliedAgedPipeFilters, isAgedPipeChecked, mapReady]);
return (
<>
<div className="bg-container">
<div className="bg-container relative h-[calc(100dvh-5rem)] overflow-hidden">
<Gis
treeData={treeData}
isSearch={true}
@@ -165,8 +503,11 @@ const Map = () => {
isSearchModalOpen={isSearchModalOpen}
setIsSearchModalOpen={setIsSearchModalOpen}
onCheckedLayersChange={handleCheckedLayersChange}
onOpenPopup={handleOpenLayerPopup}
searchBar={<ProcSearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} />}
waterDepth={showWaterDepth ? WaterDepth : undefined}
inlineLayerSettingView={inlineLayerSettingView}
layerIconVisible={isAgedPipeLayerNode}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying as any}
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
@@ -176,6 +517,37 @@ const Map = () => {
defaultZoom={7.5}
defaultLonLat={[127.75, 36]}
/>
{isAgedPipeListMinimized && (
<div className="fixed left-[480px] top-[68px] z-[90] flex min-w-[250px] items-center gap-3 rounded-sm border border-red-200 bg-white/95 px-3 py-2 shadow-[0_8px_22px_rgba(15,23,42,0.22)] backdrop-blur">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm bg-red-50">
<span className="block h-4 w-4 rounded-sm border-2 border-red-500 bg-red-500/20" />
</div>
<div className="min-w-0 flex-1">
<div className="text-[1.25rem] font-semibold leading-tight text-red-700">노후관로 현황</div>
<div className="text-[1rem] leading-tight text-slate-500">
{appliedAgedPipeFilters.elapsedYears}년 이상 · {agedPipeTotalCount.toLocaleString()}건
</div>
</div>
<Button
type="button"
color="primary"
size="xs"
className="h-7 px-3"
onClick={openAgedPipeList}
>
열기
</Button>
<Button
type="button"
color="white"
size="xs"
className="h-7 px-3"
onClick={() => setIsAgedPipeListMinimized(false)}
>
닫기
</Button>
</div>
)}
</div>
</>
);
@@ -21,6 +21,7 @@
// FileUpload.js
import React, { useState } from 'react';
import axios from 'axios';
import { SITE_ID } from '@/shared/config';
const SEWERAGE_URL = "/sewerage-service/api";
function FileUpload() {
@@ -49,6 +50,7 @@ function FileUpload() {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Site-Id": SITE_ID,
},
body: JSON.stringify({authToken: auth})
}).then((data) => {
@@ -58,7 +60,11 @@ function FileUpload() {
const handleFindUserOna = () => {
const auth = getCookie("Authorization");
if (auth) console.log("쿠키 auth : " + auth);
fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b").then((data) => {
fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b", {
headers: {
"X-Site-Id": SITE_ID,
},
}).then((data) => {
console.log(data);
});
};
@@ -86,7 +92,11 @@ function FileUpload() {
formData.append('inpFile', file);
try {
const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData);
const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData, {
headers: {
"X-Site-Id": SITE_ID,
},
});
alert('업로드 성공: ' + response.data.message);
} catch (err) {
@@ -17,6 +17,7 @@ import { useState, useEffect } from 'react'
import LoadingOverlay from '@/components/ui/LoadingBar'
import { UITab, UiTable } from '@common/business_components/ui'
import * as XLSX from 'xlsx';
import { getBlobData } from '@/components/lib/api';
// import { Table, TableBody, TableHeader } from "@common/components/ui";
// import { HeadMakeFloodPre } from './headMakeFloodPre';
// import { InputTableBody } from './inputTableBody';
@@ -54,12 +55,7 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
return false
}
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
if (!response.ok) {
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
}
const blob = await response.blob();
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
const file = new File([blob], fileNm, {
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
@@ -267,4 +263,4 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
)
};
export default TableModal;
export default TableModal;
@@ -19,6 +19,7 @@ import { UITab, UiTable } from '@common/business_components/ui'
import * as XLSX from 'xlsx';
import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan';
import { oldPpTable2HeadList, oldPpTable2NodeList, oldPpTable6HeadList, oldPpTable6NodeList } from './headList';
import { getBlobData } from '@/components/lib/api';
const TABS = [
{ val: "1", text: "raw data" },
@@ -63,12 +64,7 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
return false;
}
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
if (!response.ok) {
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
}
const blob = await response.blob();
const blob = await getBlobData(`/attach-service/api/v1/download/${atflId}`);
const file = new File([blob], fileNm, {
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
@@ -476,4 +472,4 @@ const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
)
};
export default TableModal;
export default TableModal;
+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);
}
}
@@ -1,6 +1,7 @@
"use client";
import { Button } from "@common/components";
import { VerticalTable } from "@/components/ui/verticalTable";
import {
AGED_PIPE_DETAIL_FIELDS,
getElapsedYears,
@@ -16,39 +17,48 @@ export default function AgedPipeDetailPanel({ featureData, onClose }: Props) {
const source = featureData?.values_ ?? featureData ?? {};
const facilityNo = String(getPipeDisplayValue(source, "fclt_mng_no") ?? "-");
const elapsedYears = getElapsedYears(source.instl_ymd ?? source.instlYmd);
const detailFields = [
...AGED_PIPE_DETAIL_FIELDS,
{ id: "elapsedYears", title: "경과년도" },
];
const tableData = detailFields.reduce<Record<string, string>>((acc, field) => {
acc[field.id] =
field.id === "elapsedYears"
? elapsedYears === null
? "확인불가"
: `${elapsedYears}년`
: String(getPipeDisplayValue(source, field.id) ?? "-");
return acc;
}, {});
return (
<div className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[320px] flex-col overflow-hidden border-l border-slate-200 bg-white shadow-[-12px_0_24px_rgba(15,23,42,0.1)]">
<div className="flex items-start justify-between border-b border-slate-200 bg-slate-900 px-4 py-3">
<div className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[350px] flex-col overflow-hidden border-l border-gray-300 bg-white">
<div className="card-header relative flex items-center justify-between bg-primary p-4">
<div>
<div className="text-[1.45rem] font-semibold text-white">노후관로 상세</div>
<div className="mt-1 text-[1.05rem] text-slate-300">시설관리번호 {facilityNo}</div>
<div className="mt-1 text-[1.05rem] text-amber-300">
경과년도 {elapsedYears ?? "-"}년
<div className="text-2xl font-medium text-white text-label">
하수관로 정보 <br />관리번호 : {facilityNo}
</div>
</div>
<Button
type="button"
color="white"
size="xs"
className="border border-white/30 px-3 py-1 text-[1.05rem] text-white"
className="text-white hover:text-gray-300"
onClick={onClose}
>
닫기
✕
</Button>
</div>
<div className="overflow-y-auto px-4 py-3">
<div className="space-y-2">
{AGED_PIPE_DETAIL_FIELDS.map((field) => (
<div key={field.id} className="rounded-lg border border-slate-200 px-3 py-2.5">
<div className="text-[1rem] font-medium text-slate-500">{field.title}</div>
<div className="mt-1 break-words text-[1.15rem] leading-6 text-slate-900">
{String(getPipeDisplayValue(source, field.id) ?? "-")}
</div>
</div>
))}
</div>
<div className="flex-1 overflow-y-auto overflow-x-hidden">
<VerticalTable
headName="기본정보"
tableData={[tableData]}
headlist={detailFields}
pageSize={20}
total={0}
cellClick={undefined}
onPageChange={() => undefined}
/>
</div>
</div>
);
@@ -1,7 +1,6 @@
"use client";
import { Button, Input } from "@common/components";
import useStore from "@/store/useStore";
import { useState } from "react";
import type { AgedPipeFilters } from "./agedPipeUtils";
type OverlayProps = {
@@ -13,89 +12,62 @@ type OverlayProps = {
};
type Props = OverlayProps & {
title?: string;
filters: AgedPipeFilters;
onFiltersChange: (filters: AgedPipeFilters) => void;
onApply: () => void;
};
export default function AgedPipeFilterPanel({
title = "지반침하정보",
filters,
onFiltersChange,
onApply,
}: Props) {
const { ctpv, sgg } = useStore();
const sggOptions = filters.ctpvCd === "0"
? sgg
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)));
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<div className="w-[280px] rounded-xl border border-slate-200 bg-white/95 p-3 shadow-[0_14px_30px_rgba(15,23,42,0.16)] backdrop-blur">
<div className="mb-2">
<div className="text-[1.35rem] font-semibold text-slate-900">노후관로 기준</div>
<div className="mt-1 text-[1.05rem] leading-5 text-slate-500">
경과년수 이상 관로만 지도와 목록에 반영합니다.
<div className="overflow-hidden rounded-sm border border-[#bdbdbd] bg-white">
<div className="relative flex min-h-[34px] items-center justify-center border-b border-[#eeeeee] bg-white px-8">
<button
type="button"
className="absolute left-2 top-1/2 h-[18px] w-[18px] -translate-y-1/2 bg-transparent text-[1.2rem] leading-none text-[#555]"
onClick={() => setIsCollapsed((value) => !value)}
title={isCollapsed ? "펼치기" : "접기"}
>
{isCollapsed ? "›" : "⌄"}
</button>
<span className="text-[1.3rem] font-semibold text-[#4b5563]">{title}</span>
</div>
{!isCollapsed && (
<div className="px-3 py-2">
<div className="flex min-h-[30px] items-center gap-1 whitespace-nowrap text-[1.2rem] text-[#333]">
<label htmlFor="aged-pipe-elapsed-years">경과년도(년)</label>
<input
id="aged-pipe-elapsed-years"
className="h-[22px] w-10 border border-[#ccd6df] px-1 text-center text-[1.2rem]"
inputMode="numeric"
value={String(filters.elapsedYears)}
onChange={(event) => {
const next = Number(event.target.value.replace(/[^0-9]/g, "") || "0");
onFiltersChange({ ...filters, elapsedYears: next });
}}
onKeyDown={(event) => {
if (event.key === "Enter") onApply();
}}
/>
<button
type="button"
className="grid h-[22px] w-[22px] place-items-center rounded-full bg-[#31a83b] text-[1.4rem] font-bold leading-none text-white"
onClick={onApply}
title="적용"
>
✓
</button>
</div>
</div>
</div>
<div className="space-y-2.5">
<label className="block">
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년수</div>
<Input
className="h-10 !text-[1.15rem]"
inputMode="numeric"
value={String(filters.elapsedYears)}
onChange={(event) => {
const next = Number(event.target.value.replace(/[^0-9]/g, "") || "0");
onFiltersChange({ ...filters, elapsedYears: next });
}}
onKeyDown={(event) => {
if (event.key === "Enter") onApply();
}}
/>
</label>
<label className="block">
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시도</div>
<select
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
value={filters.ctpvCd}
onChange={(event) =>
onFiltersChange({
...filters,
ctpvCd: event.target.value,
sggCd: "0",
})
}
>
<option value="0">전체</option>
{ctpv.map((item) => (
<option key={item.val} value={String(item.val)}>
{item.text}
</option>
))}
</select>
</label>
<label className="block">
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시군구</div>
<select
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
value={filters.sggCd}
onChange={(event) => onFiltersChange({ ...filters, sggCd: event.target.value })}
>
<option value="0">전체</option>
{sggOptions.map((item) => (
<option key={item.val} value={String(item.val)}>
{item.text}
</option>
))}
</select>
</label>
</div>
<Button className="mt-3 h-10 w-full !text-[1.15rem]" onClick={onApply}>
GIS 반영
</Button>
)}
</div>
);
}
@@ -64,14 +64,11 @@ export default function AgedPipeListModal({
return (
<div className="pointer-events-none absolute inset-0 z-[70] flex items-center justify-center px-6 py-8">
<div className="pointer-events-auto relative flex max-h-[82vh] w-[min(1120px,94vw)] flex-col overflow-hidden rounded-2xl border border-slate-300 bg-white shadow-[0_28px_80px_rgba(15,23,42,0.28)]">
<div className="pointer-events-auto relative flex max-h-[82vh] w-[min(1120px,94vw)] flex-col overflow-hidden border border-[#dbe2e6] bg-white shadow-[0_0_12px_rgba(0,0,0,0.25)]">
<LoadingOverlay isLoading={isLoading} />
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-900 px-5 py-3">
<div className="flex items-center justify-between border-b border-[#dbe2e6] bg-primary px-5 py-3">
<div>
<div className="text-[1.45rem] font-semibold text-white">노후관로 목록</div>
<div className="mt-1 text-[1rem] text-slate-300">
지도 위에 띄워지는 윈도우형 팝업 미리보기
</div>
</div>
<Button
type="button"
@@ -85,7 +82,7 @@ export default function AgedPipeListModal({
</div>
<div className="relative overflow-y-auto p-4">
<div className="grid grid-cols-1 gap-3 pb-4 md:grid-cols-[110px_minmax(0,1fr)_110px_minmax(0,1fr)_auto_auto]">
<div className="grid grid-cols-1 gap-3 bg-slate-50 pb-4 md:grid-cols-[110px_minmax(0,1fr)_110px_minmax(0,1fr)_auto_auto]">
<label className="block">
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년수</div>
<Input
@@ -141,7 +138,7 @@ export default function AgedPipeListModal({
</label>
<div className="flex flex-wrap gap-2 md:col-span-3 md:justify-end">
<Button type="button" size="sm" className="h-10 px-5 !text-[1.1rem]" onClick={() => onSearch(1)}>
<Button type="button" size="sm" color="primary" className="h-10 px-5 !text-[1.1rem]" onClick={() => onSearch(1)}>
조회
</Button>
<Button
@@ -7,7 +7,7 @@ import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
import Stroke from "ol/style/Stroke.js";
import Style from "ol/style/Style.js";
import useStore from "@/store/useStore";
import { getData } from "@/components/lib/api";
import { getData, getRawJson } from "@/components/lib/api";
import AgedPipeFilterPanel from "./AgedPipeFilterPanel";
import AgedPipeDetailPanel from "./AgedPipeDetailPanel";
import AgedPipeListModal from "./AgedPipeListModal";
@@ -102,19 +102,7 @@ const Map = () => {
useEffect(() => {
const fetchData = async () => {
try {
let data;
const response = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
method: "GET",
});
if (response.ok) {
const contentLength = response.headers.get("content-length");
if (contentLength === "0") data = null;
const text = await response.text();
if (!text || text.trim() === "") data = null;
else data = JSON.parse(text);
}
const data = await getRawJson("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK");
setTreeData(data);
} catch (error) {
console.error("노후관로 레이어 설정 조회 중 오류:", error);
@@ -333,12 +321,12 @@ const Map = () => {
};
}, [appliedFilters, mapReady]);
const FilterOverlay = useMemo(
const LayerSettingView = useMemo(
() =>
function FilterOverlayComponent(props: any) {
function LayerSettingComponent() {
return (
<AgedPipeFilterPanel
{...props}
title="지반침하정보"
filters={draftFilters}
onFiltersChange={setDraftFilters}
onApply={handleApplyFilters}
@@ -368,7 +356,7 @@ const Map = () => {
setIsSearchModalOpen={setIsSearchModalOpen}
onCheckedLayersChange={() => undefined}
searchBar={<></>}
waterDepth={FilterOverlay}
layerSettingView={LayerSettingView}
showZoomLevel={true}
defaultZoom={7.5}
defaultLonLat={[127.75, 36]}
@@ -18,7 +18,7 @@ import { Button, TableHead, TableRow } from '@common/components/ui';
import React, { useState, useEffect } from 'react';
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
import { VerticalTable } from '@/components/ui/verticalTable';
import { getData } from "@/components/lib/api";
import { getRawJson } from "@/components/lib/api";
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import { Label } from '@radix-ui/react-label';
@@ -65,22 +65,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
size: '3',
});
let result;
const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') result = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') result = null;
// JSON 파싱 시도
result = JSON.parse(text);
}
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`);
// helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM"
const formatNoYearNoSec = (val: any) => {
@@ -138,22 +123,7 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
size: '3',
});
let result;
const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, {
method: "GET",
});
if (res.ok) {
// 응답이 비어있는지 확인
const contentLength = res.headers.get('content-length');
if (contentLength === '0') result = null;
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === '') result = null;
// JSON 파싱 시도
result = JSON.parse(text);
}
const result = await getRawJson(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`);
// 각 아이템 전처리
const processedDataArray = result.content.map((data: any) => {
@@ -355,4 +325,4 @@ const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
)
}
export default Ssc
export default Ssc
@@ -15,40 +15,20 @@
import { VerticalTable } from '@/components/ui/verticalTable';
import { useEffect, useState } from 'react';
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { getData } from '@/components/lib/api';
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
const BASE_URL = "/sewerage-service/api";
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
method: "GET",
});
let data: any = null;
if (res.ok) {
try {
// 응답이 비어있는지 확인
const contentLength = res.headers.get("content-length");
if (contentLength === "0") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
try {
data = await getData(`v1/fcltmhl/upperPpls/${fcltMngNo}`);
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
const text = await res.text();
if (!text || text.trim() === "") {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
// JSON 파싱 시도
const response = JSON.parse(text);
data = response.data;
} catch (error) {
console.error("JSON parsing error:", error);
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
} else {
if (!data) {
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
return;
}
@@ -143,4 +123,4 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: ()
)
}
export default Smh;
export default Smh;
+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);
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+4
View File
@@ -10,3 +10,7 @@ body {
font-family: "Noto Sans KR", sans-serif;
font-size: 10px;
}
body.sn-map-page-lock {
overflow: hidden;
}
+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();
+54 -27
View File
@@ -107,6 +107,7 @@ export const chRowspanTable = (
// rowspan된 셀 렌더링 여부 추적용
const renderedMap = new Map<string, Set<number>>();
// console.log(makeTableData);
return (
<>
{/* {makeTableData.map((row, rowIndex) => ( */}
@@ -322,6 +323,26 @@ const handleChange = (
);
};
const handleTextChange = (
rowIndex: number,
field: any,
value: string,
setInputData: (data: any) => void
) => {
setInputData((prevData: any) =>
prevData?.map((row: any, index: number) => {
if (index === rowIndex) {
return {
...row,
[field]: value,
};
}
return row;
})
);
};
// 재이용실적등록 > 지도점검결과 등록 - 상세 modal의 body
export const InputTableBody = (
chgRes: any[] | [],
@@ -357,19 +378,8 @@ export const InputTableBody = (
return (
<TableRow key={dataIndex + "row"}>
{headList.map((head, headIndex) => {
//냄새면 계산 못하게 - 로 수정
// if (["smll"].includes(head.id)) {
// return (
// <TableCell
// key={head.id + headIndex}
// className={`w-36 ${bgClass} text-center !text-xl`}
// >
// <span>-</span>
// </TableCell>
// );
// }
const isTextColumn =
head.id === "구분" || head.id === "utztnUsgCdText";
const isTextColumn = head.id === "구분" || head.id === "utztnUsgCdText";
const textAlignClass = isTextColumn
? "text-center"
: "text-right";
@@ -433,22 +443,39 @@ export const InputTableBody = (
key={head.id + headIndex}
className={`w-36 ${bgClass}`}
>
<Input
// className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`}
className={`${textAlignClass} !text-xl h-10 w-full`}
onChange={(e) =>
handleCommaNumberChange(e, (val) =>
handleChange(
{head.id === "smll" ? (
<Input
className="text-center !text-xl h-10 w-full"
type="text"
onChange={(e) =>
handleTextChange(
dataIndex,
head.id,
String(val),
setInputData,
),
)
}
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
maxLength={numSize}
/>
e.target.value,
setInputData
)
}
value={String(data[head.id as keyof any] ?? "")}
maxLength={numSize}
/>
) : (
<Input
// className={`${head.id !== "utztnUsgCd" ? "text-right" : "text-center"} !text-xl h-10 w-full`}
className={`${textAlignClass} !text-xl h-10 w-full`}
onChange={(e) =>
handleCommaNumberChange(e, (val) =>
handleChange(
dataIndex,
head.id,
String(val),
setInputData
)
)
}
value={formatCommaNumber(data[head.id as keyof any]) ?? ""}
maxLength={numSize}
/>
)}
</TableCell>
) : (
<TableCell
@@ -192,7 +192,7 @@ export default function ChkgdExcel({
}
})
.finally(() => setRswtChkLoading(false));
console.log("업로드파일 점검 완료");
// console.log("업로드파일 점검 완료");
}
}
}, [file]);
+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 {
@@ -32,6 +32,9 @@ export const GisContext = createContext({
});
export const useGisContext = () => useContext(GisContext);
/**
* @param {any} props
*/
function Gis({
isOpen, //우측 상세 레이어 팝업
gisMaps,
@@ -49,7 +52,10 @@ function Gis({
setIsSearchModalOpen,
onCheckedLayersChange,
searchBar: SearchBar,
waterDepth: WaterDepth,
waterDepth: WaterDepth = null,
layerSettingView: LayerSettingView = null,
inlineLayerSettingView,
layerIconVisible,
isPlaying = false,
setIsPlaying = () => { },
targetLayerTypes = [],
@@ -281,18 +287,21 @@ function Gis({
visible={tabVisible}
setVisible={setTabVisible}
onCheckedChange={handleCheckedLayersChange}
onLayerIconClick={(layerNm) => {
onLayerIconClick={(layerNm, node) => {
layerNmRef.current = layerNm;
setLayerNm(layerNm);
setIsOpen(true);
onOpenPopup(layerNm)
const shouldOpenDetail = onOpenPopup(layerNm, node)
if (shouldOpenDetail !== false) setIsOpen(true);
}}
imageVisible={imgVisble}
layerIconVisible={layerIconVisible}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
targetLayerTypes={targetLayerTypes}
checkInterval={checkInterval}
onProgressChange={handleProgressChange}
layerSettingView={LayerSettingView}
inlineLayerSettingView={inlineLayerSettingView}
/>
{typeof WaterDepth === "function" && (
@@ -27,6 +27,8 @@ import GisApp from "./lib/gis.core";
* - orderedLayers: 레이어 재생 순서를 지정하는 id 배열 (lyrSn)
* - checkInterval: 자동 애니메이션 토글 간격(ms)
* - onProgressChange(total, current): 애니메이션 진행상태 콜백
* - layerSettingView: (선택) 좌측 레이어 트리 하단에 표시할 설정 영역
* - layerIconVisible: (선택) 레이어 행 우측 액션 아이콘 노출 여부를 결정하는 함수
*/
export default React.memo(function GisTree({
checkedNodes = [],
@@ -41,6 +43,9 @@ export default React.memo(function GisTree({
targetLayerTypes = [],
checkInterval = 3000,
onProgressChange = () => { },
layerSettingView: LayerSettingView = null,
layerIconVisible,
inlineLayerSettingView,
}) {
const { gisCore, treeData } = useGisContext();
@@ -494,9 +499,9 @@ export default React.memo(function GisTree({
/**
* 각 레이어 행의 아이콘 클릭 시 호출. 선택되지 않은 레이어면 경고, 선택된 경우 콜백 전달.
*/
const handleLayerImgClick = (nodeNm, nodeId) => {
const handleLayerImgClick = (nodeNm, nodeId, node) => {
if (!checked.includes(nodeId)) alert("시설을 선택해주세요");
else onLayerIconClick(nodeNm);
else onLayerIconClick(nodeNm, node);
};
// =====================
@@ -534,48 +539,59 @@ export default React.memo(function GisTree({
child.parent === node.id && child.data.exprsYn === "Y"
);
const inlineLayerSetting = inlineLayerSettingView?.(node);
return (
<div
className="flex items-center gap-2 rounded px-2 py-1 hover:bg-gray-100"
style={{
marginInlineStart: depth * 16,
display: isVisible ? "flex" : "none",
}}
>
{hasVisibleChildren ? (
<i
onClick={onToggle}
className={`diveicon di-arrow-forward mr-2 text-base text-label ${isOpen ? "rotate-90" : ""
}`}
title={isOpen ? "닫기" : "열기"}
/>
) : (
<span style={{ display: "inline-block", width: 16 }} />
)}
<input
type="checkbox"
checked={checked.includes(node.id)}
onChange={() => handleCheck(node.id)}
className="cursor-pointer"
/>
{node.icon && <span>{node.icon}</span>}
<span className="text-[1.3rem] font-medium text-label">
{node.text}
</span>
{imageVisible && depth > 0 && (
<span>
<img
className="h-5 w-5 cursor-pointer"
src="/images/map/layericon/Layer_Select_BTN.png"
onClick={() =>
handleLayerImgClick(
node.data.lyrTyNm,
node.id
)
}
alt={node.text}
<div style={{ display: isVisible ? "block" : "none" }}>
<div
className="flex items-center gap-2 rounded px-2 py-1 hover:bg-gray-100"
style={{
marginInlineStart: depth * 16,
}}
>
{hasVisibleChildren ? (
<i
onClick={onToggle}
className={`diveicon di-arrow-forward mr-2 text-base text-label ${isOpen ? "rotate-90" : ""
}`}
title={isOpen ? "닫기" : "열기"}
/>
) : (
<span style={{ display: "inline-block", width: 16 }} />
)}
<input
type="checkbox"
checked={checked.includes(node.id)}
onChange={() => handleCheck(node.id)}
className="cursor-pointer"
/>
{node.icon && <span>{node.icon}</span>}
<span className="text-[1.3rem] font-medium text-label">
{node.text}
</span>
{((depth > 0 && imageVisible) || layerIconVisible?.(node)) && (
<span>
<img
className="h-5 w-5 cursor-pointer"
src="/images/map/layericon/Layer_Select_BTN.png"
onClick={() =>
handleLayerImgClick(
node.data.lyrTyNm,
node.id,
node
)
}
alt={node.text}
/>
</span>
)}
</div>
{inlineLayerSetting && (
<div className="mt-2 pb-3 pr-1" style={{ marginInlineStart: depth * 8 + 12 }}>
{typeof inlineLayerSetting === "function"
? React.createElement(inlineLayerSetting)
: inlineLayerSetting}
</div>
)}
</div>
);
@@ -588,6 +604,11 @@ export default React.memo(function GisTree({
/>
</DndProvider>
</div>
{LayerSettingView && (
<div className="shrink-0 bg-white px-2 pb-3">
<LayerSettingView />
</div>
)}
<button
onClick={() => setVisible(false)}
className="absolute right-[-3px] top-1/2 z-50 -translate-y-1/2 transform rounded-l-none rounded-r border border-l border-gray-300 bg-white px-1 py-2 shadow-sm"
@@ -57,6 +57,40 @@ GisOlSelect.prototype = {
width : 10,
})
});
let focusedLineStyle = [
new Style({
stroke : new Stroke({
color : 'rgba(88, 28, 135, 0.26)',
width : 18,
lineCap : 'round',
lineJoin : 'round',
}),
}),
new Style({
stroke : new Stroke({
color : 'rgba(255, 255, 255, 0.96)',
width : 12,
lineCap : 'round',
lineJoin : 'round',
}),
}),
new Style({
stroke : new Stroke({
color : 'rgba(109, 40, 217, 0.98)',
width : 8,
lineCap : 'round',
lineJoin : 'round',
}),
}),
new Style({
stroke : new Stroke({
color : 'rgba(216, 180, 254, 0.92)',
width : 3,
lineCap : 'round',
lineJoin : 'round',
}),
}),
];
this.selectInteraction = new Select({
style: function (feature, resolution) {
@@ -72,7 +106,12 @@ GisOlSelect.prototype = {
styles.push(style);
}
}
styles.push(baseStyle);
const geometryType = feature?.getGeometry?.()?.getType?.();
if (geometryType === 'LineString' || geometryType === 'MultiLineString') {
styles.push(...focusedLineStyle);
} else {
styles.push(baseStyle);
}
return styles;
},
layers: function (layer) {
@@ -26,6 +26,7 @@ type SearchBaseType = {
leftSelectClassName?: string;
buttonOrder?: ("query" | "init" | "excel")[];
initSidoValue?: string | '';
initSigunValue?: string | '';
excelDisabled?: boolean;
}
export type ReturnType = {
@@ -61,11 +62,12 @@ function SearchBaseForm ({
leftSelectClassName,
buttonOrder,
initSidoValue='',
initSigunValue='',
excelDisabled=false,
}: SearchBaseType) {
const [partValue, setPartValue] = useState<string>("A");
const [sidoValue, setSidoValue] = useState<string>(initSidoValue?initSidoValue:"0");
const [sigunValue, setSigunValue] = useState<string>("0");
const [sigunValue, setSigunValue] = useState<string>(initSigunValue?initSigunValue:"0");
const [sidoText, setSidoText] = useState<string>("");
const [sigunText, setSigunText] = useState<string>("");
const [regionValue, setRegionValue] = useState<string>("0");
@@ -115,18 +117,24 @@ function SearchBaseForm ({
if(initSidoValue){
setSidoValue(initSidoValue);
}
if(initSigunValue){
setSigunValue(initSigunValue);
}
setResetTrigger(true);
initClick();
}, [initSidoValue, setResetTrigger, initClick]);
}, [initSidoValue, initSigunValue, setResetTrigger, initClick]);
useEffect(() => {
if (resetTrigger) {
if(initSidoValue!=""){
setSidoValue(initSidoValue);
}
if(initSigunValue!=""){
setSigunValue(initSigunValue);
}
setResetTrigger(false);
}
}, [resetTrigger]);
}, [resetTrigger, initSidoValue, initSigunValue]);
const excelDownload = () => {
excelClick({
partValue: partValue,
@@ -179,6 +187,7 @@ function SearchBaseForm ({
leftClassName={leftClassName}
leftSelectClassName={leftSelectClassName}
initSidoValue={initSidoValue}
initSigunValue={initSigunValue}
>
{leftChildren}
</SearchFormLeft>
@@ -47,6 +47,7 @@ type SearchFormLeftProps = {
leftClassName?: string;
leftSelectClassName?: string;
initSidoValue?: string;
initSigunValue?: string;
}
export type EnvPartSelectDataType = SelectDataType & {
@@ -59,12 +60,12 @@ const selectPartData:SelectDataType[] = [
{ text: "환경청별", val: 'G' },
];
function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false, leftClassName, leftSelectClassName, initSidoValue }: SearchFormLeftProps) {
function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false, leftClassName, leftSelectClassName, initSidoValue, initSigunValue }: SearchFormLeftProps) {
/******************선택된 데이터******************/
const [partValue, setPartValue] = useState<string>('A');
//행정별 시도 시군구
const [sidoValue, setSidoValue] = useState<string>(initSidoValue?initSidoValue : '0');
const [sigunValue, setSigunValue] = useState<string>('0');
const [sigunValue, setSigunValue] = useState<string>(initSigunValue?initSigunValue : '0');
const [sidoText, setSidoText] = useState<string>('');
const [sigunText, setSigunText] = useState<string>('');
//유역별
@@ -87,6 +88,39 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
const [filteredEnvPartSigunData, setFilteredEnvPartSigunData] = useState<SelectDataType[]>([{ text: "전체", val: '0' }]);
const [initTrigger, setInitTrigger] = useState<boolean>(false);
useEffect(() => {
if (!initSidoValue) return;
if (!selectSidoData.some((item) => item.val === initSidoValue)) return;
const selectedSido = selectSidoData.find((item) => item.val === initSidoValue);
setSidoValue(initSidoValue);
setSidoText(selectedSido?.text || "");
if (setRtnSidoValue) setRtnSidoValue(initSidoValue);
if (setRtnSidoText) setRtnSidoText(selectedSido?.text || "");
initializeSigunData(initSidoValue);
}, [initSidoValue, selectSidoData]);
useEffect(() => {
if (!initSidoValue || !initSigunValue) return;
const initializedSigunData = [
{ text: "전체", val: '0' },
...selectSigunData.filter(
(item: SelectDataType) => item.val.substring(0, 2) === initSidoValue
)
];
const selectedSigun = initializedSigunData.find((item) => item.val === initSigunValue);
if (!selectedSigun) return;
setFilteredSigunData(initializedSigunData);
setSigunValue(initSigunValue);
setSigunText(selectedSigun.text || "");
if (setRtnSigunValue) setRtnSigunValue(initSigunValue);
if (setRtnSigunText) setRtnSigunText(selectedSigun.text || "");
}, [initSidoValue, initSigunValue, selectSigunData, setRtnSigunText, setRtnSigunValue]);
// 시군구 세팅
const initializeSigunData = (sidoCd: string) => {
if (sidoCd === '0') {
@@ -204,7 +238,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
setRtnSidoText(selectedSido?.text || '');
}
initializeSigunData(val);
onSelectValue('0', 'sigun');
onSelectValue(val === initSidoValue && initSigunValue ? initSigunValue : '0', 'sigun');
break;
case 'sigun':
if (setRtnSigunValue) {
@@ -276,7 +310,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
const resetAllValues = () => {
setPartValue('A');
setSidoValue(initSidoValue?initSidoValue:'0');
setSigunValue('0');
setSigunValue(initSigunValue?initSigunValue:'0');
setRegionValue('0');
setUnitWaterShedValue('0');
setMiddleRegionValue('0');
@@ -291,7 +325,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
if (setRtnPartValue) setRtnPartValue('A');
if (setRtnSidoValue) setRtnSidoValue(initSidoValue?initSidoValue:'0');
if (setRtnSigunValue) setRtnSigunValue('0');
if (setRtnSigunValue) setRtnSigunValue(initSigunValue?initSigunValue:'0');
if (setRtnRegionValue) setRtnRegionValue('0');
if (setRtnUnitWaterShedValue) setRtnUnitWaterShedValue('0');
if (setRtnMiddleRegionValue) setRtnMiddleRegionValue('0');
@@ -22,7 +22,7 @@ const SearchInput = React.forwardRef<HTMLInputElement, {className:string, label:
return (
<div className={className+" flex items-center"} >
{label && <Label className="w-20 text-xl text-gray-600">{label}</Label>}
<Input className="!text-lg flex-1 w-3/4 h-14 border rounded" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input>
<Input className="!text-lg flex-1 w-3/4 h-13 border rounded" value={textValue} onChange={changeText} onKeyUp={keyUp}></Input>
</div>
)
});
+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 반환
}
}