-
-
-
type: reservation
-
- 버튼 클릭시 /activities 페이지로 이동합니다.
-
-
-
-
- type: activity
-
-
-
-
type: review
+
+
+
+
+
type: reservation
+
+ 버튼 클릭시 /activities 페이지로 이동합니다.
+
+
+
+
+ type: activity
+
+
+
+ type: review
+
-
+
);
}
diff --git a/src/domain/Auth/utils/setAuthCookies.ts b/src/domain/Auth/utils/setAuthCookies.ts
index 9366976d..943f5834 100644
--- a/src/domain/Auth/utils/setAuthCookies.ts
+++ b/src/domain/Auth/utils/setAuthCookies.ts
@@ -34,8 +34,10 @@ export default function setAuthCookies
(
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
- maxAge: 60 * 60,
- // maxAge: 15,
+
+ // maxAge: 60 * 60, //! 테스트를 위해 15초로
+ maxAge: 10,
+
});
response.cookies.set({
@@ -46,6 +48,7 @@ export default function setAuthCookies(
sameSite: 'lax',
path: '/',
maxAge: 60 * 60 * 24 * 7,
+ // maxAge: 10,
});
return response;
diff --git a/src/middleware.ts b/src/middleware.ts
index 087b6dd4..882a8196 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import setAuthCookies from '@/domain/Auth/utils/setAuthCookies';
-import { ERROR_CODES, ROUTES } from '@/shared/constants/routes';
+import { ROUTES } from '@/shared/constants/routes';
import { BRIDGE_API } from './shared/constants/bridgeEndpoints';
import { API_ENDPOINTS } from './shared/constants/endpoints';
@@ -24,7 +24,6 @@ const protectedPageRoutes = [ROUTES.MYPAGE.ROOT];
* - `/api/`로 시작하지만 `/api/auth/`가 아닌 모든 요청을 가로채 백엔드 서버로 안전하게 전달합니다.
* - 브라우저가 보낸 HttpOnly 쿠키에서 `accessToken`을 추출하여 `Authorization` 헤더에 담아 백엔드로 요청을 보냅니다.
* - 백엔드에서 `401 Unauthorized` 에러를 받으면 `refreshToken`을 사용해 `accessToken`을 자동으로 갱신합니다.
- * - 토큰 갱신마저 실패하면 로그인 페이지로 리다이렉션하여 세션 만료를 사용자에게 알립니다.
*
* ### 토큰 생명주기 (Token Lifecycle):
* - **로그인 시**: `api/auth/signin` 또는 `api/auth/signup`을 통해 완전히 새로운 토큰 세트(Access/Refresh)가 발급됩니다.
@@ -107,11 +106,22 @@ export async function middleware(request: NextRequest) {
signal: AbortSignal.timeout(30000),
});
- //! 여기서 분명히 갱신했는데 개발서버에서는 갱신이 되는데... 배포하면 안됨..
+ // if (response.status === 401) {
+ // console.log('--- 미들웨어 디버깅 ---');
+ // console.log('응답 상태:', response.status);
+ // console.log(
+ // 'refreshToken 쿠키 존재 여부:',
+ // !!request.cookies.get('refreshToken')?.value,
+ // );
+ // console.log('보호된 라우트 여부:', isProtectedRoute);
+ // console.log('-----------------------');
+ // }
+
if (response.status === 401 && request.cookies.get('refreshToken')?.value) {
const refreshToken = request.cookies.get('refreshToken')!.value;
- const refreshResponse = await fetch(
- `${BACKEND_URL}${API_ENDPOINTS.AUTH.REFRESH_TOKEN}`,
+ console.log(refreshToken);
+ const newTokenResponse = await fetch(
+ `${BACKEND_URL}${API_ENDPOINTS.AUTH.NEW_TOKEN}`,
{
method: 'POST',
headers: {
@@ -121,8 +131,8 @@ export async function middleware(request: NextRequest) {
},
);
- if (refreshResponse.ok) {
- const tokens = await refreshResponse.json();
+ if (newTokenResponse.ok) {
+ const tokens = await newTokenResponse.json();
const newAccessToken = tokens.accessToken;
console.log('[Middleware] 새로운 Access Token 발급 성공');
@@ -146,12 +156,7 @@ export async function middleware(request: NextRequest) {
tokens,
);
} else {
- console.log(
- '[Middleware] Refresh Token 만료 또는 갱신 실패. 로그인 페이지로 리다이렉트합니다.',
- );
- const redirectUrl = new URL(ROUTES.SIGNIN, request.url);
- redirectUrl.searchParams.set('error', ERROR_CODES.SESSION_EXPIRED);
- return NextResponse.redirect(redirectUrl);
+ console.log('[Middleware] Refresh Token 만료 또는 갱신 실패');
}
}
@@ -172,5 +177,3 @@ export async function middleware(request: NextRequest) {
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
-
-// 테스트 커밋
diff --git a/src/shared/constants/endpoints.ts b/src/shared/constants/endpoints.ts
index 1be593f5..02f2aff8 100644
--- a/src/shared/constants/endpoints.ts
+++ b/src/shared/constants/endpoints.ts
@@ -30,7 +30,7 @@ export const API_ENDPOINTS = {
AUTH: {
SIGNUP: '/users',
SIGNIN: '/auth/login',
- REFRESH_TOKEN: '/auth/tokens',
+ NEW_TOKEN: '/auth/tokens',
},
OAUTH: {
diff --git a/src/shared/libs/formatErrorResponseHooks.ts b/src/shared/libs/formatErrorResponseHooks.ts
index 75e357af..ead00af6 100644
--- a/src/shared/libs/formatErrorResponseHooks.ts
+++ b/src/shared/libs/formatErrorResponseHooks.ts
@@ -1,9 +1,80 @@
import type { Hooks } from 'ky';
import { HTTPError } from 'ky';
+import { ERROR_CODES, ROUTES } from '@/shared/constants/routes';
+import { queryClient } from '@/shared/libs/queryClient';
import { errorResponseSchema } from '@/shared/schemas/error';
+import { useRoamReadyStore } from '@/shared/store';
import getErrorMessageByStatus from '@/shared/utils/errors/getErrorMessageByStatus';
+const beforeErrorHook = async (error: HTTPError) => {
+ const { response } = error;
+ const protectedPageRoutes = [ROUTES.MYPAGE.ROOT];
+ let formattedMessage = null;
+
+ if (response.status === 401) {
+ console.error('클라이언트 측에서 401 에러 감지. 세션이 만료되었습니다.');
+ const { clearUser } = useRoamReadyStore.getState();
+ clearUser();
+
+ queryClient.invalidateQueries({ queryKey: ['user', 'me'] });
+
+ const currentPathname = window.location.pathname;
+ const isCurrentlyOnProtectedPage = protectedPageRoutes.some((route) =>
+ currentPathname.startsWith(route),
+ );
+
+ if (response.status === 401 && isCurrentlyOnProtectedPage) {
+ console.error(
+ '클라이언트 측에서 401 에러 감지. 보호된 페이지에서 세션이 만료되었습니다.',
+ );
+ const redirectUrl = new URL(
+ ROUTES.ACTIVITIES.ROOT,
+ window.location.origin,
+ );
+ redirectUrl.searchParams.set('error', ERROR_CODES.SESSION_EXPIRED);
+ window.location.href = redirectUrl.toString();
+ }
+ }
+
+ try {
+ const contentType = response.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ const errorData = await response.json();
+ const parsedError = errorResponseSchema.safeParse(errorData);
+
+ if (parsedError.success) {
+ formattedMessage = parsedError.data.message;
+ } else {
+ formattedMessage = `[${response.status}] 서버 응답 형식이 올바르지 않습니다.`;
+ }
+ } else {
+ formattedMessage = `[${response.status}] 서버에서 예상치 않은 응답을 받았습니다.`;
+ }
+ } catch (err: unknown) {
+ const errorName = err instanceof Error ? err.name : 'UnknownError';
+ console.error(`API Response Parsing Error [${errorName}]:`, err);
+
+ if (errorName === 'AbortError' || errorName.includes('Timeout')) {
+ formattedMessage =
+ '요청 시간이 초과되었습니다. 잠시 후 다시 시도해주세요.';
+ } else {
+ const statusPart = response ? `[${response.status}] ` : '';
+ const statusTextPart = response ? response.statusText || '' : '';
+ formattedMessage = `${statusPart}${statusTextPart || '서버와 통신 중 오류가 발생했습니다.'}`;
+ }
+ }
+
+ const commonErrorMessage = getErrorMessageByStatus(error);
+ if (commonErrorMessage) {
+ formattedMessage = commonErrorMessage;
+ }
+
+ error.message = formattedMessage || error.message;
+
+ return error;
+};
+
/**
* @description API 에러 응답을 가로채 사용자 친화적인 메시지로 포맷팅하고 전역적으로 토스트를 표시하는 공용 `ky` 훅입니다.
* 이 훅은 `ky` 요청 실패 시 호출되어 다음과 같은 로직으로 에러를 처리합니다.
@@ -19,51 +90,13 @@ import getErrorMessageByStatus from '@/shared/utils/errors/getErrorMessageByStat
* - `AbortError` 또는 `Timeout`과 같은 네트워크 오류 발생 시: '요청 시간이 초과되었습니다.'라는 사용자 친화적인 메시지를 설정합니다.
* - 그 외의 파싱 오류 또는 네트워크 오류의 경우: 응답 상태를 포함한 일반적인 통신 오류 메시지를 설정합니다.
*
+ * 3. **인증 에러(`401`) 전역 처리**:
+ * - API 요청이 `401 Unauthorized`를 반환하면, 클라이언트의 전역 사용자 상태(`useRoamReadyStore`)를 즉시 초기화하여 헤더 UI가 로그아웃 상태로 변경되도록 합니다.
+ * - 또한, 현재 페이지가 `mypage`와 같은 보호된 라우트일 경우, 사용자를 메인 페이지로 리디렉션하여 세션 만료를 알립니다.
+ *
* @param {HTTPError} error - `ky` 라이브러리에서 발생한 HTTP 에러 객체입니다.
* @returns {Promise} - 처리되고 메시지가 포맷팅된 HTTP 에러 객체를 반환합니다.
*/
export const formatErrorResponseHooks: Hooks = {
- beforeError: [
- async (error: HTTPError) => {
- const { response } = error;
- let formattedMessage = null;
-
- try {
- const contentType = response.headers.get('content-type');
- if (contentType && contentType.includes('application/json')) {
- const errorData = await response.json();
- const parsedError = errorResponseSchema.safeParse(errorData);
-
- if (parsedError.success) {
- formattedMessage = parsedError.data.message;
- } else {
- formattedMessage = `[${response.status}] 서버 응답 형식이 올바르지 않습니다.`;
- }
- } else {
- formattedMessage = `[${response.status}] 서버에서 예상치 않은 응답을 받았습니다.`;
- }
- } catch (err: unknown) {
- const errorName = err instanceof Error ? err.name : 'UnknownError';
- console.error(`API Response Parsing Error [${errorName}]:`, err);
-
- if (errorName === 'AbortError' || errorName.includes('Timeout')) {
- formattedMessage =
- '요청 시간이 초과되었습니다. 잠시 후 다시 시도해주세요.';
- } else {
- const statusPart = response ? `[${response.status}] ` : '';
- const statusTextPart = response ? response.statusText || '' : '';
- formattedMessage = `${statusPart}${statusTextPart || '서버와 통신 중 오류가 발생했습니다.'}`;
- }
- }
-
- const commonErrorMessage = getErrorMessageByStatus(error);
- if (commonErrorMessage) {
- formattedMessage = commonErrorMessage;
- }
-
- error.message = formattedMessage || error.message;
-
- return error;
- },
- ],
+ beforeError: [beforeErrorHook],
};