diff --git a/apps/fo/app/pages/(with-layout)/layout.tsx b/apps/fo/app/pages/(with-layout)/layout.tsx index e1f6f664e..38276b541 100644 --- a/apps/fo/app/pages/(with-layout)/layout.tsx +++ b/apps/fo/app/pages/(with-layout)/layout.tsx @@ -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 ( diff --git a/apps/fo/app/store/processFacility/index.tsx b/apps/fo/app/store/processFacility/index.tsx index e825ca4e2..1b6b33d71 100644 --- a/apps/fo/app/store/processFacility/index.tsx +++ b/apps/fo/app/store/processFacility/index.tsx @@ -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 { diff --git a/apps/wr/.env.local b/apps/wr/.env.local index 8f7fb329d..96b66b536 100644 --- a/apps/wr/.env.local +++ b/apps/wr/.env.local @@ -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 diff --git a/apps/wr/app/(with-layout)/jwttest/page.tsx b/apps/wr/app/(with-layout)/jwttest/page.tsx index 25d47c1d2..40baa2034 100644 --- a/apps/wr/app/(with-layout)/jwttest/page.tsx +++ b/apps/wr/app/(with-layout)/jwttest/page.tsx @@ -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(() => 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() {

JWT Exp Test

+ +
쿠키명
{CLAIM_NAME}
diff --git a/apps/wr/app/(with-layout)/layout.tsx b/apps/wr/app/(with-layout)/layout.tsx index 0367c77af..1e43f4c32 100644 --- a/apps/wr/app/(with-layout)/layout.tsx +++ b/apps/wr/app/(with-layout)/layout.tsx @@ -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 ( - + {children} diff --git a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx index cbc19a4d9..a481f66a1 100644 --- a/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx +++ b/apps/wr/app/(with-layout)/ruseFcltMng/fcltSttn/pbnstSttn/page.tsx @@ -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({} as ReturnType); @@ -66,7 +66,7 @@ export default function PbnstSttn() { const [pbnRuseNm, setPbnRuseNm] = useState(""); 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() {
-
-
- { - 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} - > +
+
+ { + 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} + >
diff --git a/apps/wr/app/api/proxy/[...url]/route.ts b/apps/wr/app/api/proxy/[...url]/route.ts index cf6447b1a..e847e8ad2 100644 --- a/apps/wr/app/api/proxy/[...url]/route.ts +++ b/apps/wr/app/api/proxy/[...url]/route.ts @@ -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; +} diff --git a/apps/wr/app/layout.tsx b/apps/wr/app/layout.tsx index e26635d4f..edd57b02b 100644 --- a/apps/wr/app/layout.tsx +++ b/apps/wr/app/layout.tsx @@ -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: "물재이용매칭 시스템입니다", diff --git a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx index 48c1e965d..35a3fb3d7 100644 --- a/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx +++ b/apps/wr/app/store/ruseFcltMng/fcltSttn/pbnstSttn/index.tsx @@ -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, diff --git a/apps/wr/middleware.ts b/apps/wr/middleware.ts index d1e96dacc..498e45fed 100644 --- a/apps/wr/middleware.ts +++ b/apps/wr/middleware.ts @@ -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)); } diff --git a/apps/wr/src/app/providers/app-provider.tsx b/apps/wr/src/app/providers/app-provider.tsx index 88a10e46e..63df8349d 100644 --- a/apps/wr/src/app/providers/app-provider.tsx +++ b/apps/wr/src/app/providers/app-provider.tsx @@ -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({ diff --git a/apps/wr/src/entities/auth/api/login-service.ts b/apps/wr/src/entities/auth/api/login-service.ts index 94be8b53f..1ad5f2cd0 100644 --- a/apps/wr/src/entities/auth/api/login-service.ts +++ b/apps/wr/src/entities/auth/api/login-service.ts @@ -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 { 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}`); diff --git a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx index 796cd44a9..91073c42c 100644 --- a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx @@ -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( `${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; diff --git a/apps/wr/src/entities/auth/lib/token-manager.ts b/apps/wr/src/entities/auth/lib/token-manager.ts index 919386622..af6ecb1c3 100644 --- a/apps/wr/src/entities/auth/lib/token-manager.ts +++ b/apps/wr/src/entities/auth/lib/token-manager.ts @@ -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, - expirationTime: number, - ): void { + private handleRefreshFailure(error: unknown, message: string): void { + console.error(message, error); + this.clearAuthToken(); + } + + public setupRefreshTimer(callback: () => Promise): 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); } } diff --git a/apps/wr/src/shared/config/api.ts b/apps/wr/src/shared/config/api.ts index eebb15c19..73bbab9cf 100644 --- a/apps/wr/src/shared/config/api.ts +++ b/apps/wr/src/shared/config/api.ts @@ -82,4 +82,4 @@ export const ENDPOINT = { }; -export const JWT_EXPIRATION_TIME = 1800000; +export const JWT_EXPIRATION_TIME = 3600000; diff --git a/packages/common/business_components/src/ui/search_base_form/index.tsx b/packages/common/business_components/src/ui/search_base_form/index.tsx index 43276a457..a42e6f548 100644 --- a/packages/common/business_components/src/ui/search_base_form/index.tsx +++ b/packages/common/business_components/src/ui/search_base_form/index.tsx @@ -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 ({ diff --git a/packages/common/business_components/src/ui/search_form_left/index.tsx b/packages/common/business_components/src/ui/search_form_left/index.tsx index 664a3cb5c..451b313b9 100644 --- a/packages/common/business_components/src/ui/search_form_left/index.tsx +++ b/packages/common/business_components/src/ui/search_form_left/index.tsx @@ -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);