diff --git a/apps/_template/.env.example b/apps/_template/.env.example index b2d706024..f9794a6f8 100644 --- a/apps/_template/.env.example +++ b/apps/_template/.env.example @@ -3,3 +3,6 @@ NEXT_PUBLIC_PORT= NEXT_PUBLIC_PROXY_HOST= NEXT_PUBLIC_SITE_ID= NEXT_PUBLIC_SERVER_API_URL= + +# 401 시 브라우저 리다이렉트 대상 +NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL=/auth/login diff --git a/apps/cm/src/entities/auth/lib/auth-wrapper.tsx b/apps/cm/src/entities/auth/lib/auth-wrapper.tsx index 1aa4f8e61..5366696f7 100644 --- a/apps/cm/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/cm/src/entities/auth/lib/auth-wrapper.tsx @@ -5,16 +5,15 @@ import { useRouter } from "next/navigation"; import { client } from "@/shared/api"; import type { IUserInfo } from "@/shared/store"; import { getCookie, useUserStore } from "@/shared/lib"; +import { isAtLoginRedirectUrl } from "@common/network"; import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; -import { LoginApi } from "../api/login-service"; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { const router = useRouter(); const { userInfo, setUserInfo } = useUserStore((state) => state); async function authCheck() { - // 로그인 페이지에서는 인증 체크를 건너뜀 - if (window.location.pathname === '/auth/login') { + if (isAtLoginRedirectUrl("/auth/login")) { return; } @@ -35,13 +34,8 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { await authCheck(); } catch (error) { console.error(error); - - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); + setUserInfo(null); + if (!isAtLoginRedirectUrl("/auth/login")) { router.push("/auth/login"); } } diff --git a/apps/common/features/src/entities/auth/lib/auth-wrapper.tsx b/apps/common/features/src/entities/auth/lib/auth-wrapper.tsx index e518f938a..d514569e0 100644 --- a/apps/common/features/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/common/features/src/entities/auth/lib/auth-wrapper.tsx @@ -4,13 +4,17 @@ import { useEffect } from "react"; import { client } from "@/shared/api"; import type { IUserInfo } from "@/shared/store"; import { getCookie, useUserStore } from "@/shared/lib"; +import { getAuthLoginRedirectUrl, isAtLoginRedirectUrl } from "@common/network"; import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; -import { LoginApi } from "../api/login-service"; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { const { userInfo, setUserInfo } = useUserStore((state) => state); async function authCheck() { + if (isAtLoginRedirectUrl(getAuthLoginRedirectUrl())) { + return; + } + const userId = getCookie(AUTH_USER_ID); if (!userId) { throw new Error("Missing required authentication tokens"); @@ -28,8 +32,6 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { await authCheck(); } catch (error) { console.error(error); - await LoginApi.silentRefresh(); - await authCheck(); } })(); }, []); diff --git a/apps/cs/src/entities/auth/lib/auth-wrapper.tsx b/apps/cs/src/entities/auth/lib/auth-wrapper.tsx index c0be4c2dd..57ae75c34 100644 --- a/apps/cs/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/cs/src/entities/auth/lib/auth-wrapper.tsx @@ -1,18 +1,24 @@ "use client"; import { useEffect } from "react"; -import { useRouter } from "next/navigation"; import { client } from "@/shared/api"; import type { IUserInfo } from "@/shared/store"; import { getCookie, useUserStore } from "@/shared/lib"; -import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; -import { LoginApi } from "../api/login-service"; +import { isAtLoginRedirectUrl } from "@common/network"; +import { + ENDPOINT, + AUTH_USER_ID, + AUTH_LOGIN_REDIRECT_URL, +} from "@/shared/config"; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); const { userInfo, setUserInfo } = useUserStore((state) => state); async function authCheck() { + if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { + return; + } + const userId = getCookie(AUTH_USER_ID); if (!userId) { throw new Error("Missing required authentication tokens"); @@ -30,14 +36,9 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { await authCheck(); } catch (error) { console.error(error); - - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); - router.push("/auth/login"); + setUserInfo(null); + if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { + window.location.assign(AUTH_LOGIN_REDIRECT_URL); } } })(); diff --git a/apps/cs/src/shared/config/env.ts b/apps/cs/src/shared/config/env.ts index c6373724f..95d1a4477 100644 --- a/apps/cs/src/shared/config/env.ts +++ b/apps/cs/src/shared/config/env.ts @@ -2,6 +2,8 @@ export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL; +export const AUTH_LOGIN_REDIRECT_URL = + process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login"; export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "CS"; export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; export const ACCESS_TOKEN = diff --git a/apps/dc/src/entities/auth/lib/auth-wrapper.tsx b/apps/dc/src/entities/auth/lib/auth-wrapper.tsx index 1f196ed52..57ae75c34 100644 --- a/apps/dc/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/dc/src/entities/auth/lib/auth-wrapper.tsx @@ -1,20 +1,24 @@ "use client"; import { useEffect } from "react"; -import { useRouter } from "next/navigation"; import { client } from "@/shared/api"; import type { IUserInfo } from "@/shared/store"; import { getCookie, useUserStore } from "@/shared/lib"; -import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; -import { LoginApi } from "../api/login-service"; - -const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; +import { isAtLoginRedirectUrl } from "@common/network"; +import { + ENDPOINT, + AUTH_USER_ID, + AUTH_LOGIN_REDIRECT_URL, +} from "@/shared/config"; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); const { userInfo, setUserInfo } = useUserStore((state) => state); async function authCheck() { + if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { + return; + } + const userId = getCookie(AUTH_USER_ID); if (!userId) { throw new Error("Missing required authentication tokens"); @@ -32,14 +36,9 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { await authCheck(); } catch (error) { console.error(error); - - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); - router.push("/auth/login"); + setUserInfo(null); + if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { + window.location.assign(AUTH_LOGIN_REDIRECT_URL); } } })(); diff --git a/apps/dc/src/shared/config/env.ts b/apps/dc/src/shared/config/env.ts index 89d2c039c..9edea0e2d 100644 --- a/apps/dc/src/shared/config/env.ts +++ b/apps/dc/src/shared/config/env.ts @@ -3,10 +3,14 @@ export const PROXY_HOST = process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL; -export const NEXT_PUBLIC_DC_SERVICE_API_URL = process.env.NEXT_PUBLIC_DC_SERVICE_API_URL || `http://localhost:${PORT}`; +export const NEXT_PUBLIC_DC_SERVICE_API_URL = + process.env.NEXT_PUBLIC_DC_SERVICE_API_URL || `http://localhost:${PORT}`; +export const AUTH_LOGIN_REDIRECT_URL = + process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login"; // Flask API URL -export const FLASK_API_URL = process.env.NEXT_PUBLIC_FLASK_API_URL || "http://localhost:5000"; +export const FLASK_API_URL = + process.env.NEXT_PUBLIC_FLASK_API_URL || "http://localhost:5000"; //export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "DC"; export const SITE_ID = "DC"; diff --git a/apps/fo/.env.local b/apps/fo/.env.local index 1b80d302e..646d80c2c 100644 --- a/apps/fo/.env.local +++ b/apps/fo/.env.local @@ -16,8 +16,8 @@ NEXT_PUBLIC_DC_URL = https://apigateway.apps.rsonesoft.com/dc-service NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service # 개발 서버 API NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y} -NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login - +# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login +NEXT_PUBLIC_CM_LOGIN_PAGE_URL = / #NEXT_PUBLIC_SERVER_API_URL = http://localhost:8883/operation-service diff --git a/apps/fo/src/entities/auth/lib/auth-wrapper.tsx b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx index 864b54ce0..bf159756c 100644 --- a/apps/fo/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/fo/src/entities/auth/lib/auth-wrapper.tsx @@ -4,18 +4,22 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { client } from "@/src/shared/api"; import type { IUserInfo } from "@/src/shared/store"; +import { isAtLoginRedirectUrl } from "@common/network"; import { useUserStore, getCookie } from "@/src/shared/lib"; import { ENDPOINT, AUTH_USER_ID } from "@/src/shared/config"; -import { LoginApi } from "../api/login-service"; -const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "http://localhost:3001/auth/login"; // 개발 환경 URL +const LOGIN_PAGE_URL = + process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login"; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { const router = useRouter(); - // const pathname = usePathname(); const { userInfo, setUserInfo } = useUserStore((state) => state); async function authCheck() { + if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) { + return; + } + const userId = getCookie(AUTH_USER_ID); if (!userId) { throw new Error("Missing required authentication tokens"); @@ -33,24 +37,29 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { await authCheck(); } catch (error) { console.error(error); - - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); - - const hostname = window.location.hostname; - - // 개발 환경에서는 localhost 도메인 사용 - if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "con.mind-one.co.kr") - router.push("/"); // 개발시 홈으로 돌아가기 - - // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용 - // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 - if (hostname.includes("hasuon.or.kr")) window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트 + setUserInfo(null); + if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) { + return; } + + // const hostname = window.location.hostname; + + // // 개발 환경에서는 localhost 도메인 사용 + // if ( + // hostname === "localhost" || + // hostname === "127.0.0.1" || + // hostname === "con.mind-one.co.kr" + // ) { + // router.push("/"); // 개발시 홈으로 돌아가기 + // } + + // // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용 + // // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 + // if (hostname.includes("hasuon.or.kr")) + // window.location.assign(LOGIN_PAGE_URL); // 세션 만료 시 CM 로그인 페이지로 리다이렉트 + + // 개발/운영 환경에서 로그인 URL 분기는 환경 변수로 처리 (.env.*) + window.location.assign(LOGIN_PAGE_URL); } })(); }, []); @@ -58,4 +67,4 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { return <>{children}; }; -export { AuthWrapper }; \ No newline at end of file +export { AuthWrapper }; diff --git a/apps/sn/.env.local b/apps/sn/.env.local index 997f50518..a47c409a7 100644 --- a/apps/sn/.env.local +++ b/apps/sn/.env.local @@ -14,7 +14,8 @@ 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_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 = http://dev.onasys.co.kr:3000/auth/login +# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login +NEXT_PUBLIC_CM_LOGIN_PAGE_URL = / #NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083 #NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883 # NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883 diff --git a/apps/sn/src/entities/auth/lib/auth-wrapper.tsx b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx index 1c27e6fe3..bc4a82aab 100644 --- a/apps/sn/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/sn/src/entities/auth/lib/auth-wrapper.tsx @@ -1,23 +1,26 @@ "use client"; import { useEffect } from "react"; -import { useRouter, usePathname } from "next/navigation"; +import { usePathname } from "next/navigation"; 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 { isAtLoginRedirectUrl } from "@common/network"; import { getCookie } from "@/shared/lib"; -import { LoginApi } from "../api/login-service"; - -const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "http://dev.onasys.co.kr:3000/auth/login"; // 로그인 페이지 URL +const LOGIN_PAGE_URL = + process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login"; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); const pathname = usePathname(); const { userInfo, setUserInfo } = useUserStore((state) => state); async function authCheck() { + if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) { + return; + } + const userId = getCookie(AUTH_USER_ID); if (!userId) { throw new Error("Missing required authentication tokens"); @@ -35,24 +38,27 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { await authCheck(); } catch (error) { console.error(error); + setUserInfo(null); - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); + if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return; - const hostname = window.location.hostname; + // const hostname = window.location.hostname; - // 개발 환경에서는 localhost 도메인 사용 - if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "con.mind-one.co.kr") - window.location.href = "/"; + // // 개발 환경에서는 localhost 도메인 사용 + // if ( + // hostname === "localhost" || + // hostname === "127.0.0.1" || + // hostname === "con.mind-one.co.kr" + // ) + // window.location.href = "/"; - // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용 - // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 - if (hostname.includes("hasuon.or.kr")) window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트 - } + // // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용 + // // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 + // if (hostname.includes("hasuon.or.kr")) + // window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트 + + // 개발/운영 환경에서 로그인 URL 분기는 환경 변수로 처리 (.env.*) + window.location.assign(LOGIN_PAGE_URL); } })(); }, [pathname]); diff --git a/apps/st/app/api/proxy/[...url]/route.ts b/apps/st/app/api/proxy/[...url]/route.ts index 7442c702f..c9f0d3730 100644 --- a/apps/st/app/api/proxy/[...url]/route.ts +++ b/apps/st/app/api/proxy/[...url]/route.ts @@ -1,104 +1,106 @@ import { - ACCESS_TOKEN, - AUTH_USER_ID, - CLAIM_NAME, - REFRESH_TOKEN, - SERVER_API_URL, - SITE_ID, + ACCESS_TOKEN, + AUTH_USER_ID, + CLAIM_NAME, + REFRESH_TOKEN, + SERVER_API_URL, + SITE_ID, } from "@/shared/config"; import { cookies } from "next/headers"; import { NextResponse } from "next/server"; async function handler( - request: Request, - params: { url: string[] }, - method: "GET" | "POST" | "PUT" | "DELETE", + request: Request, + params: { url: string[] }, + method: "GET" | "POST" | "PUT" | "DELETE", ) { - const cookieStore = await cookies(); - const headers: Record = { - "Content-Type": "application/json", - "X-Site-Id": SITE_ID, - }; - let body = {}; - const isRefreshHandler = params.url.includes("refresh"); + const cookieStore = await cookies(); + const headers: Record = { + "Content-Type": "application/json", + "X-Site-Id": SITE_ID, + }; + let body = {}; + const isRefreshHandler = params.url.includes("refresh"); - // silent refresh - if (isRefreshHandler) { - const refreshToken = cookieStore.get(REFRESH_TOKEN); - headers[CLAIM_NAME] = refreshToken?.value ?? ""; + // silent refresh + if (isRefreshHandler) { + const refreshToken = cookieStore.get(REFRESH_TOKEN); + headers[CLAIM_NAME] = refreshToken?.value ?? ""; - if (!headers[CLAIM_NAME]) { - return NextResponse.json({ message: "Can't refresh" }, { status: 401 }); - } - } - - try { - if (!isRefreshHandler) { - body = await request.json(); + if (!headers[CLAIM_NAME]) { + return NextResponse.json( + { message: "Can't refresh" }, + { status: 401 }, + ); + } } - const result = await fetch(`${SERVER_API_URL}/${params.url.join("/")}`, { - method, - headers, - body: JSON.stringify(body), - }); + try { + if (!isRefreshHandler) { + body = await request.json(); + } - if (!result.ok) { - return NextResponse.json( - { message: "Invalid Credentials" }, - { status: 401 }, - ); + const result = await fetch( + `${SERVER_API_URL}/${params.url.join("/")}`, + { + method, + headers, + body: JSON.stringify(body), + }, + ); + + if (!result.ok) { + return NextResponse.json( + { message: "Invalid Credentials" }, + { status: 401 }, + ); + } + + const accessToken = result.headers.get(ACCESS_TOKEN); + const refreshToken = result.headers.get(REFRESH_TOKEN); + const userId = result.headers.get(AUTH_USER_ID); + + if (accessToken && userId) { + const response = NextResponse.json({ + [ACCESS_TOKEN]: accessToken, + [AUTH_USER_ID]: userId, + }); + + if (refreshToken) { + cookieStore.set(REFRESH_TOKEN, refreshToken, { + httpOnly: true, + sameSite: "lax", + path: "/", + }); + } + + return response; + } + + return NextResponse.json( + { message: "Invalid Credentials" }, + { status: 401 }, + ); + } catch (error) { + return NextResponse.json( + { message: "Server Error", error }, + { status: 500 }, + ); } - - const accessToken = result.headers.get(ACCESS_TOKEN); - const refreshToken = result.headers.get(REFRESH_TOKEN); - const userId = result.headers.get(AUTH_USER_ID); - - if (accessToken && userId) { - const response = NextResponse.json({ - [ACCESS_TOKEN]: accessToken, - [AUTH_USER_ID]: userId, - }); - - - - if (refreshToken) { - cookieStore.set(REFRESH_TOKEN, refreshToken, { - httpOnly: true, - sameSite: "lax", -// domain: domain, - path: "/", - }); - } - - return response; - } - - return NextResponse.json( - { message: "Invalid Credentials" }, - { status: 401 }, - ); - } catch (error) { - return NextResponse.json( - { message: "Server Error", error }, - { status: 500 }, - ); - } } - export async function POST( - request: Request, - props: { params: Promise<{ url: string[] }> }, + request: Request, + props: { params: Promise<{ url: string[] }> }, ) { - const params = await props.params; - return handler(request, params, "POST"); + const params = await props.params; + return handler(request, params, "POST"); } export async function PUT( - request: Request, - props: { params: Promise<{ url: string[] }> }, + request: Request, + props: { params: Promise<{ url: string[] }> }, ) { - const params = await props.params; - return handler(request, params, "PUT"); + const params = await props.params; + return handler(request, params, "PUT"); } diff --git a/apps/st/src/entities/auth/lib/auth-wrapper.tsx b/apps/st/src/entities/auth/lib/auth-wrapper.tsx index c0be4c2dd..c58581c2b 100644 --- a/apps/st/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/st/src/entities/auth/lib/auth-wrapper.tsx @@ -1,49 +1,50 @@ "use client"; import { useEffect } from "react"; -import { useRouter } from "next/navigation"; import { client } from "@/shared/api"; import type { IUserInfo } from "@/shared/store"; import { getCookie, useUserStore } from "@/shared/lib"; -import { ENDPOINT, AUTH_USER_ID } from "@/shared/config"; -import { LoginApi } from "../api/login-service"; +import { isAtLoginRedirectUrl } from "@common/network"; +import { + ENDPOINT, + AUTH_USER_ID, + AUTH_LOGIN_REDIRECT_URL, +} from "@/shared/config"; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); - const { userInfo, setUserInfo } = useUserStore((state) => state); + const { userInfo, setUserInfo } = useUserStore((state) => state); - async function authCheck() { - const userId = getCookie(AUTH_USER_ID); - if (!userId) { - throw new Error("Missing required authentication tokens"); + async function authCheck() { + if (isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { + return; + } + + const userId = getCookie(AUTH_USER_ID); + if (!userId) { + throw new Error("Missing required authentication tokens"); + } + + const response = await client.get( + `${ENDPOINT.USER_SERVICE.USERS}/info/me`, + ); + if (!userInfo) setUserInfo(response.data); } - const response = await client.get( - `${ENDPOINT.USER_SERVICE.USERS}/info/me`, - ); - if (!userInfo) setUserInfo(response.data); - } + useEffect(() => { + (async () => { + try { + await authCheck(); + } catch (error) { + console.error(error); + setUserInfo(null); + if (!isAtLoginRedirectUrl(AUTH_LOGIN_REDIRECT_URL)) { + window.location.assign(AUTH_LOGIN_REDIRECT_URL); + } + } + })(); + }, []); - useEffect(() => { - (async () => { - try { - await authCheck(); - } catch (error) { - console.error(error); - - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); - router.push("/auth/login"); - } - } - })(); - }, []); - - return <>{children}; + return <>{children}; }; export { AuthWrapper }; diff --git a/apps/st/src/shared/config/env.ts b/apps/st/src/shared/config/env.ts index ded87ac3e..560bf72e1 100644 --- a/apps/st/src/shared/config/env.ts +++ b/apps/st/src/shared/config/env.ts @@ -1,12 +1,15 @@ export const PORT = process.env.NEXT_PUBLIC_PORT || "3000"; export const PROXY_HOST = - process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; + process.env.NEXT_PUBLIC_PROXY_HOST || `http://localhost:${PORT}`; export const SERVER_API_URL = process.env.NEXT_PUBLIC_SERVER_API_URL; +export const AUTH_LOGIN_REDIRECT_URL = + process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || "/auth/login"; export const SITE_ID = process.env.NEXT_PUBLIC_SITE_ID || "ST"; export const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; export const ACCESS_TOKEN = - process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; + process.env.NEXT_PUBLIC_ACCESS_TOKEN || "access-token"; export const REFRESH_TOKEN = - process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; + process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-token"; export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; -export const ST_SERVICE_URL = process.env.NEXT_PUBLIC_ST_SERVICE_URL || "http://localhost:8080"; +export const ST_SERVICE_URL = + process.env.NEXT_PUBLIC_ST_SERVICE_URL || "http://localhost:8080"; diff --git a/apps/wr/.env.local b/apps/wr/.env.local index db59d33c4..3e79ce9d8 100644 --- a/apps/wr/.env.local +++ b/apps/wr/.env.local @@ -18,10 +18,10 @@ NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service # NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y} -NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login +# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login +NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login NEXT_PUBLIC_HASUDOINFO = https://www.hasudoinfo.or.kr NEXT_PUBLIC_HASUDOINFO_IN = https://www.hasudoinfo.or.kr NEXT_PUBLIC_FINDADRESS_URL = https://business.juso.go.kr/addrlink/addrLinkApi.do NEXT_PRIVATE_WR_ADDRESS_KEY = devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM= REACT_EDITOR = atom - diff --git a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx index 9b59774fc..796cd44a9 100644 --- a/apps/wr/src/entities/auth/lib/auth-wrapper.tsx +++ b/apps/wr/src/entities/auth/lib/auth-wrapper.tsx @@ -1,24 +1,26 @@ "use client"; import { useEffect } from "react"; -import { useRouter } from "next/navigation"; import { usePathname } from "next/navigation"; 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 { isAtLoginRedirectUrl } from "@common/network"; import { getCookie } from "../../../shared/lib"; -import { LoginApi } from "../api/login-service"; - -const LOGIN_PAGE_URL = process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "http://localhost:3000/auth/login"; // 로그인 페이지 URL +const LOGIN_PAGE_URL = + process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || "/auth/login"; const AuthWrapper = ({ children }: { children: React.ReactNode }) => { - const router = useRouter(); const pathname = usePathname(); const { userInfo, setUserInfo } = useUserStore((state) => state); async function authCheck() { + if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) { + return; + } + const userId = getCookie(AUTH_USER_ID); if (!userId) { throw new Error("Missing required authentication tokens"); @@ -36,25 +38,27 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { await authCheck(); } catch (error) { console.error(error); + setUserInfo(null); - try { - await LoginApi.silentRefresh(); - await authCheck(); - } catch (refreshError) { - console.error("Token refresh failed:", refreshError); - setUserInfo(null); - - const hostname = window.location.hostname; + if (isAtLoginRedirectUrl(LOGIN_PAGE_URL)) return; - // 개발 환경에서는 localhost 도메인 사용 - if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "con.mind-one.co.kr") - router.push("/auth/login"); + // const hostname = window.location.hostname; - // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용 - // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 - if (hostname.includes("hasuon.or.kr")) - window.location.href = LOGIN_PAGE_URL; // 세션 만료 시 CM 로그인 페이지로 리다이렉트 - } + // // 개발 환경에서는 localhost 도메인 사용 + // if ( + // hostname === "localhost" || + // hostname === "127.0.0.1" || + // hostname === "con.mind-one.co.kr" + // ) + // router.push("/auth/login"); + + // // 운영 환경에서는 서브도메인을 포함한 최상위 도메인 사용 + // // router.push는 같은 Next.js 앱 내에서만 작동하므로 다른 앱으로 이동할 때는 window.location.href 사용 + // if (hostname.includes("hasuon.or.kr")) + // window.location.assign(LOGIN_PAGE_URL); // 세션 만료 시 CM 로그인 페이지로 리다이렉트 + + // 개발/운영 환경에서 로그인 URL 분기는 환경 변수로 처리 (.env.*) + window.location.assign(LOGIN_PAGE_URL); } })(); }, [pathname]); diff --git a/packages/common/network/client.ts b/packages/common/network/client.ts index 587fcf541..6b39a33fa 100644 --- a/packages/common/network/client.ts +++ b/packages/common/network/client.ts @@ -1,7 +1,8 @@ -import axios, { AxiosError } from "axios"; +import axios from "axios"; import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import Cookies from "js-cookie"; import { AUTH_USER_ID, CLAIM_NAME } from "./env"; +import { errorInterceptor } from "./error-interceptor"; interface ClientConfig { baseURL: string; @@ -22,25 +23,12 @@ class Client { withCredentials: true, }); - this.axiosInstance.interceptors.request.use((config) => { const token = Cookies.get(CLAIM_NAME); const userId = Cookies.get(AUTH_USER_ID); if (!token || !userId) { - throw new AxiosError( - "The authentication token is invalid.", - "UNAUTHORIZED", - config, - null, - { - data: { message: "The authentication token is invalid." }, - status: 401, - statusText: "Unauthorized", - headers: this.axiosInstance.defaults.headers, - config: config, - } as AxiosResponse - ); + return config; } config.headers[CLAIM_NAME] = token; @@ -48,6 +36,11 @@ class Client { return config; }); + + this.axiosInstance.interceptors.response.use( + (response) => response, + (error) => errorInterceptor(error), + ); } public static getInstance(config: ClientConfig): Client { @@ -59,7 +52,7 @@ class Client { public async get>( url: string, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise { const response = await this.axiosInstance.get(url, config); return response; @@ -68,7 +61,7 @@ class Client { public async post, D = unknown>( url: string, data?: D, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise { const response = await this.axiosInstance.post(url, data, config); return response; @@ -77,7 +70,7 @@ class Client { public async put, D = unknown>( url: string, data?: D, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise { const response = await this.axiosInstance.put(url, data, config); return response; @@ -85,7 +78,7 @@ class Client { public async delete>( url: string, - config?: AxiosRequestConfig + config?: AxiosRequestConfig, ): Promise { const response = await this.axiosInstance.delete(url, config); return response; diff --git a/packages/common/network/error-interceptor.ts b/packages/common/network/error-interceptor.ts new file mode 100644 index 000000000..1f0eca1a4 --- /dev/null +++ b/packages/common/network/error-interceptor.ts @@ -0,0 +1,31 @@ +import type { AxiosError } from "axios"; + +import { getAuthLoginRedirectUrl, isAtLoginRedirectUrl } from "./utils"; + +let isRedirectingToLogin = false; + +function shouldRedirectToLogin(error: unknown): boolean { + if (!error || typeof error !== "object") return false; + + const axiosError = error as AxiosError; + const status = axiosError.response?.status; + const code = axiosError.code; + + return status === 401 || code === "UNAUTHORIZED"; +} + +function redirectToLogin(): void { + if (typeof window === "undefined") return; + + const loginUrl = getAuthLoginRedirectUrl(); + if (isAtLoginRedirectUrl(loginUrl)) return; + if (isRedirectingToLogin) return; + + isRedirectingToLogin = true; + window.location.assign(loginUrl); +} + +export function errorInterceptor(error: unknown) { + if (shouldRedirectToLogin(error)) redirectToLogin(); + return Promise.reject(error); +} diff --git a/packages/common/network/index.ts b/packages/common/network/index.ts index 5ec76921e..f08438b95 100644 --- a/packages/common/network/index.ts +++ b/packages/common/network/index.ts @@ -1 +1,2 @@ +export * from "./utils"; export * from "./client"; diff --git a/packages/common/network/utils.ts b/packages/common/network/utils.ts new file mode 100644 index 000000000..010bf2fe2 --- /dev/null +++ b/packages/common/network/utils.ts @@ -0,0 +1,20 @@ +export function getAuthLoginRedirectUrl(): string { + return ( + process.env.NEXT_PUBLIC_AUTH_LOGIN_REDIRECT_URL || + process.env.NEXT_PUBLIC_CM_LOGIN_PAGE_URL || + "/auth/login" + ); +} + +export function isAtLoginRedirectUrl(loginRedirectUrl: string): boolean { + if (typeof window === "undefined") return false; + try { + const target = new URL(loginRedirectUrl, window.location.href); + return ( + target.origin === window.location.origin && + target.pathname === window.location.pathname + ); + } catch { + return false; + } +}