diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d51f8ed..eb01e53 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import Header from '@/components/common/Header'; import { ToastProvider } from '@/components/common/ToastContext'; import ReactQueryProviders from '@/hooks/useReactQuery'; import axiosInstance from '@/lib/axios/axiosInstance'; +import { AxiosError } from 'axios'; import type { Metadata } from 'next'; import localFont from 'next/font/local'; @@ -24,26 +25,31 @@ const pretendard = localFont({ async function getUserInfo() { try { - const { data } = await axiosInstance.get('/api/v1/mypage/banner'); - return data.data; + const res = await axiosInstance.get( + 'https://deving.shop/api/v1/mypage/banner', + ); + return res.data.data; } catch (error) { + if (error instanceof AxiosError) { + console.log('error: ', error.status); + } return null; } } + export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { const userInfo = await getUserInfo(); - console.log('[layout] userInfo', userInfo); return ( -
+
{children}
diff --git a/src/app/meeting/[category]/page.tsx b/src/app/meeting/[category]/page.tsx index 3cacd73..0b6d5d5 100644 --- a/src/app/meeting/[category]/page.tsx +++ b/src/app/meeting/[category]/page.tsx @@ -27,7 +27,7 @@ async function MeetingListPage({ params }: { params: { category: string } }) { const initialSearchQueryObj = { keyword: '', skillArray: [], - sortField: 'NEW', + sortField: 'CREATED', lastMeetingId: 0, size: 4, }; diff --git a/src/app/meeting/_features/MeetingList.tsx b/src/app/meeting/_features/MeetingList.tsx index 1be1229..c7db50a 100644 --- a/src/app/meeting/_features/MeetingList.tsx +++ b/src/app/meeting/_features/MeetingList.tsx @@ -26,7 +26,7 @@ const MeetingList = () => { const [searchQuery, setSearchQuery] = useState({ keyword: '', skillArray: [], - sortField: 'NEW', + sortField: 'CREATED', lastMeetingId: 0, size: 4, }); @@ -132,7 +132,7 @@ const MeetingList = () => { className="w-full md:w-[122px] lg:w-[122px]" options={filterOptions} onChange={(value) => handleSearchOption({ sortField: value })} - trigger="최신순" + trigger="생성순" variant="doubleArrow" sideOffset={8} /> diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index 222a46c..711d3b1 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -1,7 +1,7 @@ 'use client'; import Logo from '@/assets/icon/logo.svg'; -import { QUERY_KEYS, useBannerQueries } from '@/hooks/queries/useMyPageQueries'; +import { QUERY_KEYS } from '@/hooks/queries/useMyPageQueries'; import { removeAccessToken } from '@/lib/serverActions'; import { translateCategoryNameToKor } from '@/util/searchFilter'; import { useQueryClient } from '@tanstack/react-query'; @@ -11,6 +11,7 @@ import Image from 'next/image'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { IBanner } from 'types/myMeeting'; import Dropdown from './Dropdown'; import { useToast } from './ToastContext'; @@ -192,17 +193,17 @@ const NavLinks = ({ isMobile }: { isMobile?: boolean }) => { ); }; -const Header = () => { +const Header = ({ userInfo }: { userInfo: IBanner }) => { const [isOpen, setIsOpen] = useState(false); - const { data: userInfo, isLoading } = useBannerQueries(); const userId = undefined; const isLogIn = !!userInfo; - console.log('[Header] userInfo: ', userInfo, 'isLogIn: ', isLogIn); - // 유저 정보 꺼내기 - // const queryClient = useQueryClient(); - - // const userInfo = queryClient.getQueryData(QUERY_KEYS.banner()) + const queryClient = useQueryClient(); + useEffect(() => { + if (userInfo) { + queryClient.setQueryData(QUERY_KEYS.banner(), userInfo); + } + }, [userInfo]); return (
diff --git a/src/hooks/mutations/useUserMutation.ts b/src/hooks/mutations/useUserMutation.ts index 52bb981..31fcd31 100644 --- a/src/hooks/mutations/useUserMutation.ts +++ b/src/hooks/mutations/useUserMutation.ts @@ -27,9 +27,8 @@ const useLoginMutation = ({ mutationFn: ({ email, password }: { email: string; password: string }) => postLogin({ email, password }), onSuccess: async (res) => { - // 유저 정보 불러오기 - console.log('유저 정보 invalidate'); - // queryClient.invalidateQueries({ queryKey: QUERY_KEYS.banner() }); + // accessToken 저장 + await setAccessToken(res.accessToken); // refreshToken 저장 await setRefreshToken(res.refreshToken); diff --git a/src/lib/axios/axiosInstance.ts b/src/lib/axios/axiosInstance.ts index a519cc8..6f42328 100644 --- a/src/lib/axios/axiosInstance.ts +++ b/src/lib/axios/axiosInstance.ts @@ -1,9 +1,11 @@ import axios from 'axios'; import { + getAccessToken, getRefreshToken, removeAccessToken, removeRefreshToken, + setAccessToken, } from '../serverActions'; export const baseURL = process.env.NEXT_PUBLIC_API_URL; @@ -26,9 +28,19 @@ const onAccessTokenFetched = () => { refreshSubscribers.forEach((callback) => callback()); refreshSubscribers = []; // 모든 요청이 처리되었기에 배열 초기화 }; + +// 해당 요청이 서버 사이드인지 클라이언트 사이드인지 판별 +const isServer = typeof window === 'undefined'; + axiosInstance.interceptors.request.use( async (config) => { - console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`); + if (isServer) { + const accessToken = await getAccessToken(); + if (accessToken) { + config.headers.Cookie = `access_token= ${accessToken}`; + } + } + return config; }, async (error) => { @@ -39,21 +51,15 @@ axiosInstance.interceptors.request.use( axiosInstance.interceptors.response.use( (response) => response, async (error) => { - /** - * TODO:(refresh 토큰 발급 이후) - * - 토근 재발급 로직 - */ - - /** - * - 401 에러로 실패하면, 로그인 페이지로 리다이렉트하는 로직 - * - 리다이렉트 전에 사용자에게 경고 메시지 - */ + // 서버 사이드 환경이면 바로 리턴 + if (isServer) { + return Promise.reject(error); + } + if (error.response?.status === 401) { - // await removeAccessToken(); console.log('401 Unauthorized - 토큰 재발급 시도'); const refreshToken = await getRefreshToken(); - console.log('refreshToken: ', refreshToken); if (!refreshToken) { console.log('Refresh Token 없음 -> 강제 로그아웃'); @@ -68,7 +74,7 @@ axiosInstance.interceptors.response.use( try { // Refresh Token으로 Access Token 재발급 시도 - await axios.post( + const res = await axios.post( 'https://deving.shop/api/v1/auths/refresh', { refreshToken, @@ -78,6 +84,10 @@ axiosInstance.interceptors.response.use( isRefreshing = false; + // accessToken 서버 쿠키로 다시 저장 + const accessToken = res.data.data.accessToken; + await setAccessToken(accessToken); + // 대기중인 요청들을 새로운 access token으로 실행 onAccessTokenFetched(); // ✅ 기존 요청 다시 실행 (Access Token 갱신 후) @@ -93,7 +103,6 @@ axiosInstance.interceptors.response.use( } } else { // refresh token 요청이 진행 중이라면 대기 (Promise를 반환) - return new Promise((resolve) => { refreshSubscribers.push(() => { resolve(axiosInstance(error.config)); // 기존의 요청을 새로운 토큰으로 재시도 diff --git a/src/lib/serverActions.ts b/src/lib/serverActions.ts index 122b01f..9b947bc 100644 --- a/src/lib/serverActions.ts +++ b/src/lib/serverActions.ts @@ -4,7 +4,12 @@ import { cookies } from 'next/headers'; export async function getAccessToken() { const cookieStore = cookies(); - return cookieStore.get('token')?.value || null; + return cookieStore.get('accessToken')?.value || null; +} + +export async function getAllToken() { + const cookieStore = cookies(); + return cookieStore.getAll(); } export async function getRefreshToken() { @@ -14,7 +19,17 @@ export async function getRefreshToken() { export async function removeAccessToken() { const cookieStore = cookies(); - cookieStore.delete('access_token'); + // cookieStore.delete('access_token'); + + // ✅ Set-Cookie를 통해 access_token을 삭제 (Max-Age=0) + cookieStore.set('access_token', '', { + path: '/', + maxAge: 0, // 쿠키 만료 + httpOnly: true, // 백엔드와 일관성 유지 + secure: true, + domain: 'deving.shop', + sameSite: 'none', + }); } export async function removeRefreshToken() { @@ -25,7 +40,7 @@ export async function removeRefreshToken() { export async function setAccessToken(token: string) { const cookieStore = cookies(); const isProd = process.env.NODE_ENV === 'production'; - cookieStore.set('token', token, { + cookieStore.set('accessToken', token, { httpOnly: true, sameSite: 'none', secure: true, diff --git a/src/util/searchFilter.ts b/src/util/searchFilter.ts index ad0767d..b3d1a07 100644 --- a/src/util/searchFilter.ts +++ b/src/util/searchFilter.ts @@ -2,8 +2,8 @@ import { CategoryTitle } from 'types/meeting'; export const filterOptions = [ { - value: 'NEW', - label: '최신순', + value: 'CREATED', + label: '생성순', }, { value: 'OLD',