Skip to content

Commit 0d0d30b

Browse files
author
twtwkim
committed
feat: use refresh token
1 parent d0e2392 commit 0d0d30b

File tree

6 files changed

+109
-12
lines changed

6 files changed

+109
-12
lines changed

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@types/react": "^18.3.12",
1313
"@types/react-dom": "^18.3.1",
1414
"axios": "^1.7.9",
15+
"jwt-decode": "^4.0.0",
1516
"react": "^18.3.1",
1617
"react-dom": "^18.3.1",
1718
"react-hook-form": "^7.53.2",

src/App.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Navigate, Route, Routes } from "react-router-dom";
2-
import { useEffect } from "react";
32
import "./App.css";
43
import MainPage from "./page/MainPage";
54
import AddItemPage from "./page/AddItemPage";
@@ -13,11 +12,7 @@ import RegisterPage from "./page/RegisterPage";
1312
import { useSelector } from "react-redux";
1413

1514
function App() {
16-
// const isAccessToken = localStorage.getItem("access_token");
1715
const count = useSelector((state: any) => state.counter.value);
18-
useEffect(() => {
19-
console.log("Count changed:", count);
20-
}, [count]);
2116
return (
2217
<Routes>
2318
<Route path={ROUTES.LANDING} element={<LandingPage />} />

src/api/api.ts

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export const API_BASE_URL = "https://panda-market-api.vercel.app";
22

3+
// 기본 함수
34
async function fetchApi(url: string, options = {}) {
45
try {
56
const response = await fetch(url, options);
@@ -15,6 +16,7 @@ async function fetchApi(url: string, options = {}) {
1516
}
1617
}
1718

19+
// 상품 가져오기 함수
1820
export async function getProducts({
1921
page = "",
2022
pageSize = "",
@@ -24,29 +26,32 @@ export async function getProducts({
2426
const params = new URLSearchParams({ page, pageSize, orderBy, keyword });
2527
const url = `${API_BASE_URL}/products?${params}`;
2628

27-
return fetchApi(url);
29+
return fetchWithAuth(url);
2830
}
2931

32+
// 상품 id별 가져오기 함수
3033
export async function getProductsById(productId: string | undefined) {
3134
const url = `${API_BASE_URL}/products/${productId}`;
3235

33-
return fetchApi(url);
36+
return fetchWithAuth(url);
3437
}
3538

39+
// 댓글 가져오기 함수
3640
export async function getCommentsById(
3741
productId: string | undefined,
3842
{ limit = "" }
3943
) {
4044
const params = new URLSearchParams({ limit });
4145
const url = `${API_BASE_URL}/products/${productId}/comments?${params}`;
4246

43-
return fetchApi(url);
47+
return fetchWithAuth(url);
4448
}
4549

4650
interface UpdateCommentParams {
4751
content: string;
4852
}
4953

54+
// 댓글 수정 함수
5055
export async function updateCommentsById(
5156
commentId: number,
5257
{ content }: UpdateCommentParams
@@ -60,7 +65,7 @@ export async function updateCommentsById(
6065
body: JSON.stringify({ content }),
6166
};
6267

63-
return fetchApi(url, options);
68+
return fetchWithAuth(url, options);
6469
}
6570

6671
interface SignupParams {
@@ -70,6 +75,7 @@ interface SignupParams {
7075
passwordConfirmation: string;
7176
}
7277

78+
// 회원가입 함수
7379
export async function signup(data: SignupParams) {
7480
const url = `${API_BASE_URL}/auth/signUp`;
7581
const options = {
@@ -92,6 +98,7 @@ interface LoginParams {
9298
password: string;
9399
}
94100

101+
// 로그인 함수
95102
export async function login(data: LoginParams) {
96103
const url = `${API_BASE_URL}/auth/signIn`;
97104
const options = {
@@ -106,3 +113,90 @@ export async function login(data: LoginParams) {
106113
};
107114
return fetchApi(url, options);
108115
}
116+
117+
// 토큰을 디코딩하는 함수
118+
const parseJwt = (token: string) => {
119+
try {
120+
const base64Url = token.split(".")[1];
121+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
122+
const decodedPayload = JSON.parse(atob(base64));
123+
return decodedPayload;
124+
} catch (error) {
125+
console.error("JWT 파싱 오류 : ", error);
126+
return null;
127+
}
128+
};
129+
130+
// 토큰이 만료되었는지 비교하는 함수
131+
const isTokenExpired = (token: string) => {
132+
if (!token) return true;
133+
134+
const decodedToken = parseJwt(token);
135+
if (!decodedToken) return true;
136+
137+
const expirationTime = decodedToken.exp * 1000;
138+
return Date.now() > expirationTime;
139+
};
140+
141+
// 리프레시 토큰을 통해 새로운 엑세스 토큰을 받아오는 함수
142+
const refreshAccessToken = async (refreshToken: string) => {
143+
const url = `${API_BASE_URL}/auth/refresh-token`;
144+
const options = {
145+
method: "POST",
146+
headers: {
147+
"Content-Type": "application/json",
148+
},
149+
body: JSON.stringify({ refreshToken }),
150+
};
151+
152+
try {
153+
const response = await fetch(url, options);
154+
if (!response.ok) {
155+
throw new Error("리프레시 토큰 요청 실패");
156+
}
157+
158+
const data = await response.json();
159+
const newAccessToken = data.accessToken;
160+
if (newAccessToken) {
161+
localStorage.setItem("access_token", newAccessToken);
162+
}
163+
return newAccessToken;
164+
} catch (error) {
165+
console.error("엑세스 토큰 갱신 오류:", error);
166+
throw new Error("엑세스 토큰을 갱신할 수 없습니다.");
167+
}
168+
};
169+
170+
// 로컬 스토리지에서 엑세스 토큰을 가져오고 만료되었으면 리프레시 토큰으로 갱신하는 함수
171+
export const getAccessToken = async () => {
172+
let accessToken = localStorage.getItem("access_token");
173+
const refreshToken = localStorage.getItem("refresh_token");
174+
175+
if (accessToken && !isTokenExpired(accessToken)) {
176+
return accessToken;
177+
}
178+
179+
if (refreshToken) {
180+
return await refreshAccessToken(refreshToken);
181+
}
182+
183+
return null;
184+
};
185+
186+
// 모든 API 보낼때 토큰 담아서 보내는 함수
187+
async function fetchWithAuth(url: string, options: RequestInit = {}) {
188+
const accessToken = await getAccessToken();
189+
190+
if (!accessToken) {
191+
throw new Error("로그인이 필요합니다.");
192+
}
193+
194+
const authOptions = {
195+
...options,
196+
headers: {
197+
...(options.headers || {}),
198+
Authorization: `Bearer ${accessToken}`,
199+
},
200+
};
201+
return fetchApi(url, authOptions);
202+
}

src/components/LoginPage/Login.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ const Login = () => {
3333
dispatch(check());
3434
alert("로그인이 정상적으로 완료되었습니다.");
3535
navigate("/");
36-
console.log(res);
3736
} catch (error: any) {
3837
console.error(
3938
"회원가입 실패:",

src/redux/counterAccessToken.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ const counterSlice = createSlice({
88
reducers: {
99
check: (state) => {
1010
state.value = true;
11-
console.log("check 이후:", state.value);
1211
},
1312
reset: (state) => {
1413
state.value = false;
15-
console.log("reset 이후:", state.value);
1614
},
1715
},
1816
});

0 commit comments

Comments
 (0)