Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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';

Expand All @@ -24,26 +25,31 @@

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);

Check warning on line 34 in src/app/layout.tsx

View workflow job for this annotation

GitHub Actions / check

Unexpected console statement
}
return null;
}
}

export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const userInfo = await getUserInfo();
console.log('[layout] userInfo', userInfo);

return (
<html lang="ko" className={pretendard.variable}>
<body className="bg-BG">
<ReactQueryProviders>
<ToastProvider>
<Header />
<Header userInfo={userInfo} />
<div className="m-auto max-w-[1340px]">{children}</div>
</ToastProvider>
</ReactQueryProviders>
Expand Down
2 changes: 1 addition & 1 deletion src/app/meeting/[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function MeetingListPage({ params }: { params: { category: string } }) {
const initialSearchQueryObj = {
keyword: '',
skillArray: [],
sortField: 'NEW',
sortField: 'CREATED',
lastMeetingId: 0,
size: 4,
};
Expand Down
4 changes: 2 additions & 2 deletions src/app/meeting/_features/MeetingList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const MeetingList = () => {
const [searchQuery, setSearchQuery] = useState<IMeetingSearchCondition>({
keyword: '',
skillArray: [],
sortField: 'NEW',
sortField: 'CREATED',
lastMeetingId: 0,
size: 4,
});
Expand Down Expand Up @@ -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}
/>
Expand Down
17 changes: 9 additions & 8 deletions src/components/common/Header.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<IUserInfo>(QUERY_KEYS.banner())
const queryClient = useQueryClient();
useEffect(() => {
if (userInfo) {
queryClient.setQueryData(QUERY_KEYS.banner(), userInfo);
}
}, [userInfo]);

return (
<div>
Expand Down
5 changes: 2 additions & 3 deletions src/hooks/mutations/useUserMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
37 changes: 23 additions & 14 deletions src/lib/axios/axiosInstance.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

access_token= ${accessToken}

위에서 = 우측에 공백은 일부러 넣으신걸까요 ?!
아니라면 제거되는게 좋을 것 같습니당

}
}

return config;
},
async (error) => {
Expand All @@ -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 없음 -> 강제 로그아웃');
Expand All @@ -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,
Expand All @@ -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 갱신 후)
Expand All @@ -93,7 +103,6 @@ axiosInstance.interceptors.response.use(
}
} else {
// refresh token 요청이 진행 중이라면 대기 (Promise를 반환)

return new Promise((resolve) => {
refreshSubscribers.push(() => {
resolve(axiosInstance(error.config)); // 기존의 요청을 새로운 토큰으로 재시도
Expand Down
21 changes: 18 additions & 3 deletions src/lib/serverActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/util/searchFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { CategoryTitle } from 'types/meeting';

export const filterOptions = [
{
value: 'NEW',
label: '최신순',
value: 'CREATED',
label: '생성순',
},
{
value: 'OLD',
Expand Down
Loading