Merge branch 'iipbs/wr/dev'

This commit is contained in:
2026-06-04 11:06:27 +09:00
17 changed files with 284 additions and 155 deletions
@@ -1,6 +1,7 @@
"use client"
import { BaseLayout } from "@common/components";
import { useQueryClient } from "@tanstack/react-query";
import type { MenuItem } from "@/app/store/common";
import { useFoStore } from "@/app/store";
import { usePathname, useRouter } from "next/navigation";
@@ -16,6 +17,7 @@ export default function RootLayout({
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
const pathname = usePathname();
const router = useRouter();
const queryClient = useQueryClient();
const handleClick = (id: MenuItem) => {
setCurrentMenuInfo(id);
@@ -25,6 +27,7 @@ export default function RootLayout({
return (
<BaseLayout
queryClient={queryClient}
currentPath={pathname ?? ""}
handleMenuClick={handleClick}
>
+12 -1
View File
@@ -459,7 +459,18 @@ const fetchFacilityList = async (pageNo: number, pageSize: number, partValue?: s
}
};
// 시설정보 리스트 데이터 호출 (대시보드용)
/**
* 시설정보 리스트 데이터 호출 (대시보드용)
* @param pageNo 페이지번호
* @param pageSize 페이지크기
* @param partValue 조회구분값(0 전체,A: 행정, R: 유역, G: 환경청)
* @param nameCd 조회구분에 따른 지역코드(0 전체)
* @param fcltCpct 시설용량 필터값(0 전체)
* @param oprtngSttsCd 운영상태 코드(0 전체,1: 가동중, 2:재가동, 3:가동중지, 4:시설폐쇄)
* @param facilityPartValue 시설구분 코드(0 전체,ET:기타기초시설,SW:공공하수처리시설,SS:오수처리장,NS:분뇨처리장)
* @param facilityName 시설명 검색어
* @returns 시설목록과 전체건수
*/
const fetchFacilityListSearching = async (pageNo: number, pageSize: number, partValue?: string, nameCd?: string
, fcltCpct?: string, oprtngSttsCd?: string, facilityPartValue?: string, facilityName?: string): Promise<{ data: facilityListProps[], count: number }> => {
try {
+1
View File
@@ -8,6 +8,7 @@ NEXT_PUBLIC_PROXY_HOST = http://localhost:3001
NEXT_PUBLIC_SUB_DOMAIN = http://localhost:13000
NEXT_PUBLIC_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_FO_SERVER_API_URL = http://con.mind-one.co.kr:11083/operation-service/api/v1
# NEXT_PUBLIC_FO_SERVER_API_URL = http://localhost:8883/operation-service/api/v1
NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver
+52 -4
View File
@@ -5,7 +5,8 @@
*/
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { LoginApi } from "@/src/entities/auth/api/login-service";
import { CLAIM_NAME } from "@/src/shared/config";
import { getCookie } from "@/src/shared/lib";
@@ -112,14 +113,51 @@ function readJwtState(): JwtState {
export default function Page() {
const [jwtState, setJwtState] = useState<JwtState>(() => readJwtState());
const isRefreshingRef = useRef(false);
const handleSilentRefresh = async () => {
try {
await LoginApi.silentRefresh();
} catch (error) {
console.error("Manual silent refresh failed:", error);
} finally {
setJwtState(readJwtState());
}
};
useEffect(() => {
const syncJwtState = () => {
const syncJwtState = async () => {
const nextState = readJwtState();
if (
nextState.remainingMs !== null &&
nextState.remainingMs > 0 &&
nextState.remainingMs < 5 * 60 * 1000 &&
!isRefreshingRef.current
) {
isRefreshingRef.current = true;
try {
await LoginApi.silentRefresh();
} catch (error) {
console.error("JWT test silent refresh failed:", error);
}
}
setJwtState(readJwtState());
if (
nextState.remainingMs === null ||
nextState.remainingMs >= 5 * 60 * 1000 ||
nextState.remainingMs <= 0
) {
isRefreshingRef.current = false;
}
};
syncJwtState();
const timer = window.setInterval(syncJwtState, 1000);
void syncJwtState();
const timer = window.setInterval(() => {
void syncJwtState();
}, 1000);
return () => window.clearInterval(timer);
}, []);
@@ -128,6 +166,16 @@ export default function Page() {
<div className="space-y-4 p-8 text-base">
<h1 className="text-2xl font-bold">JWT Exp Test</h1>
<button
type="button"
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
onClick={() => {
void handleSilentRefresh();
}}
>
Silent Refresh 호출
</button>
<div className="rounded border p-4">
<div className="font-semibold">쿠키명</div>
<div>{CLAIM_NAME}</div>
+3 -1
View File
@@ -19,6 +19,7 @@
"use client";
import { BaseLayout } from "@common/components/ui";
import { useQueryClient } from "@tanstack/react-query";
import { useWrStore } from "../store";
import { MainDiv } from "@common/business_components/ui";
import type { MenuItem } from "../store/common/type/menu";
@@ -35,6 +36,7 @@ export default function RootLayout({
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
const router = useRouter();
const pathname = usePathname();
const queryClient = useQueryClient();
const changeMenu = (path: MenuItem) => {
setCurrentMenuInfo(path);
@@ -46,7 +48,7 @@ export default function RootLayout({
}
};
return (
<BaseLayout currentPath={pathname ?? ""} handleMenuClick={changeMenu}>
<BaseLayout queryClient={queryClient} currentPath={pathname ?? ""} handleMenuClick={changeMenu}>
<PageLayout>
<MainDiv>{children}</MainDiv>
</PageLayout>
@@ -10,7 +10,7 @@
"use client";
import { useWrStore } from "@/app/store";
import { btnClass } from "@/app/store/common/code";
import { prfRegPageSize } from "@/app/store/prfmncReg/pbnstPrfmncReg";
import { DEFAULT_LIST_SIZE } from "@/src/shared/config";
import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type";
import type { WaterReuseFacility } from "@/app/store/ruseFcltMng/fcltSttn/pbnstSttn/type";
import { excelDownloadNumStyle, yearOptions } from "@/components/lib/func/common";
@@ -42,13 +42,13 @@ export default function PbnstSttn() {
pbnstSttnSearch,
pbnSttnInit,
getPbnStnDetail,
getFlcts, // 하수재이용시설 조회
getFlcts, // 하수재이용시설(tb_wr_pbnst_ruse_fclt) 조회
resetPbnstDetail,
getLaeSeCdCheck,
} = useWrStore((state) => state);
const { pbnstSttnRes, fclts, pbnstSttnResCount } = pbnstSttn;
const { laeSeCd } = auth;
const { laeSeCd } = auth; // 지자체구분코드
const [isReset, setIsReset] = useState(false);
const ref = useRef<ReturnType>({} as ReturnType);
@@ -66,7 +66,7 @@ export default function PbnstSttn() {
const [pbnRuseNm, setPbnRuseNm] = useState<string>("");
const { t } = useTranslation("base");
const alert = useAlertStore((state) => state.setMessage);
const isAllAreaUser = laeSeCd === "0";
const isAllAreaUser = laeSeCd === "0"; // 모든 지자체 권한이 있는 경우
const fixedSidoValue =
!isAllAreaUser && laeSeCd?.length ? laeSeCd.slice(0, 2) : "";
const fixedSigunValue =
@@ -77,7 +77,8 @@ export default function PbnstSttn() {
// 유역(R) 탭 선택 시, 이전 행정구역 탭에서 선택된 시군구 값이 남아있어
// 검색 결과가 시군구로 필터링되는 현상을 방지하기 위해 sigunValue를 '0'으로 초기화합니다.
if (cleanedValue.partValue === "R"|| cleanedValue.partValue === "G") {
// if (cleanedValue.partValue === "R"|| cleanedValue.partValue === "G") {
if (cleanedValue.partValue === "R") {
cleanedValue.sigunValue = "0";
cleanedValue.sigunText = "";
}
@@ -143,7 +144,7 @@ export default function PbnstSttn() {
useEffect(() => {
if (useInitRef.current === true && laeSeCd !== undefined && fclts.length > 0) {
pbnSttnInit("1", "15", getFixedSearchValue(ref.current), searchYear, fclts);
pbnSttnInit("1", String(DEFAULT_LIST_SIZE), getFixedSearchValue(ref.current), searchYear, fclts);
}
}, [fclts, laeSeCd, searchYear, pbnSttnInit, getFixedSearchValue]);
@@ -253,7 +254,7 @@ export default function PbnstSttn() {
pbnstSttnSearch(
false,
"1",
String(prfRegPageSize),
String(DEFAULT_LIST_SIZE),
fixedSearchValue,
pbnRuseNm,
searchYear,
@@ -276,33 +277,33 @@ export default function PbnstSttn() {
<div className="grid grid-cols-6 gap-4 ">
<div className="col-span-6">
<ListDiv>
<div className="h-full w-full overflow-x-scroll min-[1900px]:overflow-x-hidden">
<div className="w-[160%] min-[1900px]:w-[100%]">
<UiTable
headName={undefined}
tableData={bdstTable || []}
headlist={pbnstSttn.pbnstSttHeadList}
pageSize={15}
total={pbnstSttnResCount}
cellClick={(index: number | undefined) => {
cellCLick(index);
}}
onPageChange={(page: number) => {
const fixedSearchValue = getFixedSearchValue(ref.current);
pbnstSttnSearch(
false,
String(page),
String(prfRegPageSize),
fixedSearchValue,
pbnRuseNm,
searchYear,
fclts
);
setIsReset(false);
}}
resetPageNo={isReset}
widthFixed={true}
>
<div className="h-full w-full overflow-x-scroll min-[2100px]:overflow-x-hidden">
<div className="w-[160%] min-[2100px]:w-[100%]">
<UiTable
headName={undefined}
tableData={bdstTable || []}
headlist={pbnstSttn.pbnstSttHeadList}
pageSize={DEFAULT_LIST_SIZE}
total={pbnstSttnResCount}
cellClick={(index: number | undefined) => {
cellCLick(index);
}}
onPageChange={(page: number) => {
const fixedSearchValue = getFixedSearchValue(ref.current);
pbnstSttnSearch(
false,
String(page),
String(DEFAULT_LIST_SIZE),
fixedSearchValue,
pbnRuseNm,
searchYear,
fclts
);
setIsReset(false);
}}
resetPageNo={isReset}
widthFixed={true}
>
<HeadMakeColSpan upHeadList={pbnstSttn.pbnstSttNodeList} />
</UiTable>
</div>
+21 -7
View File
@@ -15,9 +15,10 @@ import { NextResponse } from "next/server";
const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token";
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN;
const REFRESH_TOKEN = process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token";
const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || "";
const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "SN";
const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "WR";
async function handler(
request: Request,
@@ -35,6 +36,7 @@ async function handler(
// silent refresh
if (isRefreshHandler) {
const refreshToken = cookieStore.get(REFRESH_TOKEN);
console.log("refreshToken", refreshToken?.value);
headers[CLAIM_NAME] = refreshToken?.value ?? "";
if (!headers[CLAIM_NAME]) {
@@ -52,12 +54,11 @@ async function handler(
headers,
body: JSON.stringify(body),
});
// console.log(result);
if (!result.ok) {
console.log("Invalid Credentials ");
return NextResponse.json(
{ message: "Invalid Credentials" },
{ status: 401 },
{ message: "first Invalid Credentials" },
{ status: result.status },
);
}
@@ -72,11 +73,13 @@ async function handler(
});
if (refreshToken) {
cookieStore.set(REFRESH_TOKEN, refreshToken, {
response.cookies.set(REFRESH_TOKEN, refreshToken, {
httpOnly: true,
sameSite: "lax",
sameSite: "strict", // 다른 사이트에서 들어오는 모든 요청에 대해 쿠키를 보내지 않는 가장 강력한 설정
domain: getCookieDomain(),
path: "/",
});
console.log("refresh cookie setting : " + refreshToken);
}
return response;
@@ -109,3 +112,14 @@ export async function PUT(
const params = await props.params;
return handler(request, params, "PUT");
}
function getCookieDomain(): string | undefined {
if (typeof window === "undefined") return undefined;
if (COOKIE_DOMAIN) return COOKIE_DOMAIN;
const { hostname } = window.location;
if (hostname === "localhost" || hostname === "127.0.0.1") return undefined;
return hostname;
}
-2
View File
@@ -1,8 +1,6 @@
import type { Metadata } from "next";
import { App } from "../src/app";
import { setCookie } from "../src/shared/lib/cookie";
setCookie("site-id-key", process.env.NEXT_PUBLIC_SITE_ID || "WR");
export const metadata: Metadata = {
title: "물재이용매칭시스템",
description: "물재이용매칭 시스템입니다",
@@ -120,77 +120,62 @@ export const hasuListFetch = async (
let url = ``;
let filteredFclt = flcts;
const filterData =
reurnValue.sigunValue !== "0"
? reurnValue.sigunValue
: reurnValue.sidoValue;
if (filterData !== "0")
filteredFclt = flcts.filter((fclt: string) => {
return filterData.length == 2
? fclt.slice(0, 2) === filterData
: fclt.slice(0, 5) === filterData;
});
if (reurnValue.partValue === "A") {
url += `${reurnValue.partValue}/${
// reurnValue.sidoValue ? reurnValue.sidoValue : reurnValue.sigunValue
reurnValue.sigunValue !== "0"
? reurnValue.sigunValue
: reurnValue.sidoValue
}/0/1/SW`;
if (filterData !== "0")
filteredFclt = flcts.filter((fclt: string) => {
return filterData.length == 2
? fclt.slice(0, 2) === filterData
: fclt.slice(0, 5) === filterData;
});
} else if (reurnValue.partValue === "R") {
url += `${reurnValue.partValue}/${
reurnValue.regionValue
? reurnValue.regionValue
: reurnValue.unitWaterShedValue
? reurnValue.unitWaterShedValue
: reurnValue.middleRegionValue
}/0/1/SW`;
} else if (reurnValue.partValue === "G") {
url += `${reurnValue.envPartValue}/${reurnValue.sigunValue !== "0" ? reurnValue.sigunValue : reurnValue.sidoValue}/0/1/SW`;
if (filterData !== "0")
filteredFclt = flcts.filter((fclt: string) => {
return filterData.length == 2
? fclt.slice(0, 2) === filterData
: fclt.slice(0, 5) === filterData;
});
} else {
url += `A/0/0/1/SW`;
let nameCd="0", partStr="A", sidogunValue="0";
switch (reurnValue.partValue) {
case 'R': // 유역별
if (reurnValue.regionValue !== '0') {
if(reurnValue.unitWaterShedValue==='0') nameCd = reurnValue.regionValue;
else if (reurnValue.middleRegionValue==='0') nameCd = reurnValue.unitWaterShedValue;
else nameCd = reurnValue.middleRegionValue;
}
partStr = reurnValue.partValue;
break;
default:
if(reurnValue.sidoValue && reurnValue.sidoValue !== '0') nameCd = reurnValue.sidoValue;
if(reurnValue.sigunValue &&reurnValue.sigunValue !== '0') nameCd = reurnValue.sigunValue;
sidogunValue = nameCd;
partStr = reurnValue.envPartValue !== '0' ? reurnValue.envPartValue:'A';
break;
}
if (sidogunValue && sidogunValue !== "0") { // 시도, 시군구 조회 조건이 있다면
filteredFclt = flcts.filter((fclt: string) => {
return sidogunValue.length == 2
? fclt.slice(0, 2) === sidogunValue
: fclt.slice(0, 5) === sidogunValue;
});
}
url = `${partStr}/${nameCd}/0/1/SW`;
if (searchNm && searchNm?.length > 0) {
url += "/" + searchNm;
}
// console.log(url);
// 시설 코드가 없으면 FO 조회를 호출하지 않고 빈 결과 반환
if (filteredFclt.length === 0) {
return { data: [], count: 0 };
}
// fo의 하수처리시설을 조회
const pbnList = await postFectch(
`/prcsFclt/ruseInfo/1/${filteredFclt.length}/` + url,
filteredFclt,
fetchType.COMMON // FO
);
const checkRuseFcltList =
searchNm && searchNm?.length > 0
? pbnList.data.map((item) => item.bscFctCd)
: filteredFclt;
const checkRuseFcltList = pbnList.data.map((item) => item.bscFctCd);
// console.log(checkRuseFcltList);
const getRuseList = await postFectch(
`/reuseFc/pubDetailList/${year}/${filterData}/${page}/${total}`,
`/reuseFc/pubDetailList/${year}/${sidogunValue}/${page}/${total}`,
checkRuseFcltList,
fetchType.WR
);
const checkWrRegListRes = getRuseList.data;
// console.log(getRuseList);
const data: WaterReuseFacility[] = checkWrRegListRes
.map((item: FcPrfType) => {
@@ -292,13 +277,13 @@ export const pbnstSttnActionsExport: (set: any) => pbnstSttnActions = (
isExcel: boolean,
page: string,
total: string,
reurnValue: ReturnType,
reurnValue: ReturnType, // 검색조건
searchNm: string | undefined,
year: string,
fclts: string[]
) => {
try {
console.log(reurnValue);
// console.log(reurnValue);
const pbnstSttnRes = await hasuListFetch(
page,
total,
+4 -1
View File
@@ -30,6 +30,7 @@ function buildAuthHeaders(req: NextRequest): Headers {
const requestHeaders = new Headers(req.headers);
const token = req.cookies.get(CLAIM_NAME)?.value;
const userId = req.cookies.get(AUTH_USER_ID)?.value;
if (token) requestHeaders.set(CLAIM_NAME, token);
if (userId) requestHeaders.set(AUTH_USER_ID, userId);
return requestHeaders;
@@ -38,7 +39,9 @@ function buildAuthHeaders(req: NextRequest): Headers {
/** 공통: rewrite 응답 생성 (헤더 주입 여부 선택) */
function rewriteWith(url: string, req: NextRequest, withAuth = false): NextResponse {
return withAuth
? NextResponse.rewrite(new URL(url), { headers: buildAuthHeaders(req) })
? NextResponse.rewrite(new URL(url), {
headers: buildAuthHeaders(req),
})
: NextResponse.rewrite(new URL(url));
}
@@ -19,7 +19,9 @@ import koBase from "@/public/locales/ko/message.json";
import { StoreProvider as WrStoreProvider } from "@/app/store";
//import { LoginApi } from "@/src/entities/auth";
//import { useEffect } from "react";
import { setCookie } from "@/src/shared/lib/cookie";
setCookie("site-id-key", process.env.NEXT_PUBLIC_SITE_ID || "WR");
const i18n = i18next.createInstance();
i18n.init({
+23 -18
View File
@@ -1,7 +1,7 @@
import type { LoginService, ILogin } from "../model/login-interface";
import { tokenManager } from "../lib/token-manager";
import {PROXY_HOST, ACCESS_TOKEN, AUTH_USER_ID,SITE_ID, CLAIM_NAME, ENDPOINT, JWT_EXPIRATION_TIME } from "../../../shared/config";
import { ACCESS_TOKEN, AUTH_USER_ID, SITE_ID, CLAIM_NAME, ENDPOINT } from "../../../shared/config";
export class LoginApiService implements LoginService {
private static instance: LoginApiService;
@@ -18,31 +18,33 @@ export class LoginApiService implements LoginService {
public async login(payload: ILogin): Promise<void> {
try {
tokenManager.clearAuthToken();
const response = await fetch( PROXY_HOST+ ENDPOINT.USER_SERVICE.LOGIN, {
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,
}),
});
// const data = response.headers.get(ACCESS_TOKEN);
const accessToken = response.headers.get(ACCESS_TOKEN);
const tokenId = response.headers.get(AUTH_USER_ID);
const data = await response.json();
if (!response.ok) {
throw new Error(data?.message ?? "Login failed");
}
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
if (!accessToken || !tokenId) {
throw new Error("Missing required authentication tokens");
}
tokenManager.setToken(CLAIM_NAME, accessToken)
tokenManager.setToken(AUTH_USER_ID, tokenId)
tokenManager.setToken("site-id-key", SITE_ID)
tokenManager.setupRefreshTimer(
() => this.silentRefresh(),
JWT_EXPIRATION_TIME,
);
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
tokenManager.setToken("site-id-key", SITE_ID);
tokenManager.setupRefreshTimer(() => this.silentRefresh());
} catch (error) {
throw new Error(`Login failed: ${error}`);
@@ -54,17 +56,19 @@ export class LoginApiService implements LoginService {
console.log(' silentRefresh ')
try {
const response = await fetch(PROXY_HOST+
`${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
const response = await fetch(
`/api/proxy${ENDPOINT.USER_SERVICE.SILENT_REFRESH}`,
{
method: "PUT",
headers: {
"X-Site-Id": SITE_ID,
},
},
);
const data = await response.json();
if (!response.ok) {
throw new Error(data?.message ?? "Silent refresh failed");
}
const accessToken = data[ACCESS_TOKEN];
const tokenId = data[AUTH_USER_ID];
@@ -74,6 +78,7 @@ export class LoginApiService implements LoginService {
tokenManager.setToken(CLAIM_NAME, accessToken);
tokenManager.setToken(AUTH_USER_ID, tokenId);
tokenManager.setupRefreshTimer(() => this.silentRefresh());
} catch (error) {
tokenManager.clearAuthToken();
throw new Error(`Silent refresh failed: ${error}`);
+21 -4
View File
@@ -2,10 +2,12 @@
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { LoginApi } from "../api/login-service";
import { tokenManager } from "./token-manager";
import { client } from "../../../shared/api";
import type { IUserInfo } from "../../../shared/store";
import { useUserStore } from "../../../shared/lib";
import { ENDPOINT, AUTH_USER_ID } from "../../../shared/config";
import { ENDPOINT, AUTH_USER_ID, CLAIM_NAME } from "../../../shared/config";
import { isAtLoginRedirectUrl } from "@common/network";
import { getCookie } from "../../../shared/lib";
@@ -17,22 +19,37 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
const { userInfo, setUserInfo } = useUserStore((state) => state);
async function authCheck() {
// 현재 페이지가 로그인 페이지이면 authCheck(쿠키점검) 안함.
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) {
return;
}
const userId = getCookie(AUTH_USER_ID);
if (!userId) {
if (!userId) { // token_id 가 없으면 로그인 페이지로 이동
throw new Error("Missing required authentication tokens");
}
if (userInfo) return;
const response = await client.get<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
);
if (!userInfo) setUserInfo(response.data);
setUserInfo(response.data);
}
useEffect(() => {
const token = getCookie(CLAIM_NAME);
const userId = getCookie(AUTH_USER_ID);
if (!token || !userId) {
return;
}
tokenManager.setupRefreshTimer(() => LoginApi.silentRefresh());
}, []);
useEffect(() => {
const isJwtTestPage = pathname?.startsWith("/jwttest");
(async () => {
try {
await authCheck();
@@ -40,7 +57,7 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => {
console.error(error);
setUserInfo(null);
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return;
if (isAtLoginRedirectUrl(LOGIN_PAGE_URL) || isJwtTestPage) return;
// const hostname = window.location.hostname;
+27 -8
View File
@@ -1,4 +1,4 @@
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
import { AUTH_USER_ID, CLAIM_NAME, REFRESH_TOKEN } from "@/src/shared/config";
import { getCookie, removeCookie, setCookie } from "@/src/shared/lib/cookie";
type JwtPayload = {
@@ -11,6 +11,7 @@ type JwtPayload = {
export class TokenManager {
private static instance: TokenManager;
private refreshTimer: NodeJS.Timeout | null = null;
private static readonly REFRESH_BUFFER_MS = 60000; // 60초
private constructor() {}
@@ -79,20 +80,37 @@ export class TokenManager {
removeCookie(key);
}
public setupRefreshTimer(
callback: () => Promise<void>,
expirationTime: number,
): void {
private handleRefreshFailure(error: unknown, message: string): void {
console.error(message, error);
this.clearAuthToken();
}
public setupRefreshTimer(callback: () => Promise<void>): void {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
const remainingMs = this.getRemainingMsFromToken(CLAIM_NAME);
if (remainingMs === null) {
return;
}
if (remainingMs <= 0) {
this.clearAuthToken();
return;
}
const refreshDelay = Math.max(
remainingMs - TokenManager.REFRESH_BUFFER_MS,
0,
);
this.refreshTimer = setTimeout(() => {
callback().catch((error) => {
console.error("Silent refresh failed:", error);
this.clearAuthToken();
this.handleRefreshFailure(error, "Silent refresh failed:");
});
}, expirationTime - 60000);
}, refreshDelay);
}
public clearAuthToken(): void {
@@ -102,6 +120,7 @@ export class TokenManager {
}
this.removeToken(CLAIM_NAME);
this.removeToken(REFRESH_TOKEN);
this.removeToken(AUTH_USER_ID);
}
}
+1 -1
View File
@@ -82,4 +82,4 @@ export const ENDPOINT = {
};
export const JWT_EXPIRATION_TIME = 1800000;
export const JWT_EXPIRATION_TIME = 3600000;
@@ -30,15 +30,15 @@ type SearchBaseType = {
excelDisabled?: boolean;
}
export type ReturnType = {
partValue: string;
sidoValue: string;
sigunValue: string;
regionValue: string;
unitWaterShedValue: string;
middleRegionValue: string;
envPartValue: string;
sidoText: string;
sigunText: string;
partValue: string; // 조회 구분값(A: 행정, R: 유역, G: 환경청)
sidoValue: string; // 선택된 시도 코드
sigunValue: string; // 선택된 시군구 코드
regionValue: string; // 선택된 권역(유역) 코드
unitWaterShedValue: string; // 선택된 단위유역 코드
middleRegionValue: string; // 선택된 중권역 코드
envPartValue: string; // 선택된 환경청 코드
sidoText: string; // 선택된 시도명
sigunText: string; // 선택된 시군구명
}
function SearchBaseForm ({
@@ -201,31 +201,51 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
let selectedEnvPartSido;
let selectedEnvPartSigun;
const resetSelectStateByPartChange = () => {
setSidoValue('0');
setSigunValue('0');
setSidoText('');
setSigunText('');
setRegionValue('0');
setUnitWaterShedValue('0');
setMiddleRegionValue('0');
setEnvPartValue('0');
setFilteredSigunData([{ text: "전체", val: '0' }]);
setFilteredUnitWaterShedData([{ text: "전체", val: '0' }]);
setFilteredMiddleRegionData([{ text: "전체", val: '0' }]);
setFilteredEnvPartData([{ text: "전체", val: '0', adstDliStrCd:'' }]);
setFilteredEnvPartSidoData([{ text: "전체", val: '0', adstDliStrCd:'' }]);
setFilteredEnvPartSigunData([{ text: "전체", val: '0' }]);
setFilteredEnvPartSigunDataSelected([]);
if (setRtnSidoValue) setRtnSidoValue('0');
if (setRtnSigunValue) setRtnSigunValue('0');
if (setRtnSidoText) setRtnSidoText('');
if (setRtnSigunText) setRtnSigunText('');
if (setRtnRegionValue) setRtnRegionValue('0');
if (setRtnUnitWaterShedValue) setRtnUnitWaterShedValue('0');
if (setRtnMiddleRegionValue) setRtnMiddleRegionValue('0');
if (setRtnEnvPartValue) setRtnEnvPartValue('0');
if (setRtnEnvPartSidoValue) setRtnEnvPartSidoValue('0');
if (setRtnEnvPartSigunValue) setRtnEnvPartSigunValue('0');
};
switch(type) {
case 'part':
setPartValue(val);
if (setRtnPartValue) {
setRtnPartValue(val);
}
// 구분
if(val == 'R') {
initializeMiddleRegionData('0');
initializeUnitWaterShedData('0');
} else if(val == 'G') {
resetSelectStateByPartChange();
if (val == 'G') {
setFilteredEnvPartData([
{ text: "전체", val: '0', adstDliStrCd:'' },
...selectEnvPartData.filter((item: EnvPartSelectDataType) => item.val !== null && item.adstDliStrCd !== '')
]);
setEnvPartValue('0');
initializeCityDataByEnvPartSido('0');
} else {
initializeSigunData('0');
//onSelectValue(selectSigunData[0]?.val || '0', 'sigun');
}
// 다른 selectbox 전체로 초기화
//onSelectValue(selectSidoData[0]?.val || '0', 'sido');
//onSelectValue(selectSigunData[0]?.val || '0', 'sigun');
break;
case 'sido':
setSidoValue(val);