refactor: 권한 인증 에러 처리 로직

This commit is contained in:
2026-03-25 13:18:19 +09:00
parent dc8006899e
commit 601e99fbd5
20 changed files with 322 additions and 246 deletions
+3
View File
@@ -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
+4 -10
View File
@@ -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");
}
}
@@ -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();
}
})();
}, []);
+13 -12
View File
@@ -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);
}
}
})();
+2
View File
@@ -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 =
+13 -14
View File
@@ -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);
}
}
})();
+6 -2
View File
@@ -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";
+2 -2
View File
@@ -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
+30 -21
View File
@@ -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 };
export { AuthWrapper };
+2 -1
View File
@@ -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
+25 -19
View File
@@ -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]);
+84 -82
View File
@@ -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<string, string> = {
"Content-Type": "application/json",
"X-Site-Id": SITE_ID,
};
let body = {};
const isRefreshHandler = params.url.includes("refresh");
const cookieStore = await cookies();
const headers: Record<string, string> = {
"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");
}
+35 -34
View File
@@ -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<IUserInfo>(
`${ENDPOINT.USER_SERVICE.USERS}/info/me`,
);
if (!userInfo) setUserInfo(response.data);
}
const response = await client.get<IUserInfo>(
`${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 };
+7 -4
View File
@@ -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";
+2 -2
View File
@@ -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
+25 -21
View File
@@ -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]);
+12 -19
View File
@@ -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<T = unknown, R = AxiosResponse<T>>(
url: string,
config?: AxiosRequestConfig
config?: AxiosRequestConfig,
): Promise<R> {
const response = await this.axiosInstance.get<T, R>(url, config);
return response;
@@ -68,7 +61,7 @@ class Client {
public async post<T = unknown, R = AxiosResponse<T>, D = unknown>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
config?: AxiosRequestConfig<D>,
): Promise<R> {
const response = await this.axiosInstance.post<T, R>(url, data, config);
return response;
@@ -77,7 +70,7 @@ class Client {
public async put<T = unknown, R = AxiosResponse<T>, D = unknown>(
url: string,
data?: D,
config?: AxiosRequestConfig<D>
config?: AxiosRequestConfig<D>,
): Promise<R> {
const response = await this.axiosInstance.put<T, R>(url, data, config);
return response;
@@ -85,7 +78,7 @@ class Client {
public async delete<T = unknown, R = AxiosResponse<T>>(
url: string,
config?: AxiosRequestConfig
config?: AxiosRequestConfig,
): Promise<R> {
const response = await this.axiosInstance.delete<T, R>(url, config);
return response;
@@ -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);
}
+1
View File
@@ -1 +1,2 @@
export * from "./utils";
export * from "./client";
+20
View File
@@ -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;
}
}