Skip to content

Commit 665f7b4

Browse files
committed
Merge branch 'develop' of https://github.com/prgrms-web-devcourse-final-project/WEB2_3_9crops_FE into 45-feat-notification
2 parents f14d139 + 01e9bbb commit 665f7b4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1953
-635
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"gsap": "^3.12.7",
2222
"react": "^18.3.1",
2323
"react-dom": "^18.3.1",
24+
"react-intersection-observer": "^9.15.1",
2425
"react-router": "^7.1.5",
2526
"swiper": "^11.2.4",
2627
"tailwind-merge": "^3.0.1",

pnpm-lock.yaml

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

src/App.tsx

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { Route, Routes } from 'react-router';
33
import useViewport from './hooks/useViewport';
44
import Layout from './layouts/Layout';
55
import MobileLayout from './layouts/MobileLayout';
6+
import PrivateRoute from './layouts/PrivateRoute';
67
import AdminPage from './pages/Admin';
78
import FilteredLetterManage from './pages/Admin/FilteredLetter';
89
import FilteringManage from './pages/Admin/Filtering';
910
import ReportManage from './pages/Admin/Report';
11+
import AuthCallbackPage from './pages/Auth';
1012
import Home from './pages/Home';
1113
import Landing from './pages/Landing';
1214
import LetterBoardPage from './pages/LetterBoard';
@@ -29,38 +31,44 @@ const App = () => {
2931
return (
3032
<Routes>
3133
<Route element={<MobileLayout />}>
32-
<Route index element={<Home />} />
3334
<Route path="login" element={<LoginPage />} />
3435
<Route path="landing" element={<Landing />} />
36+
<Route path="*" element={<NotFoundPage />} />
37+
<Route path="auth-callback" element={<AuthCallbackPage />} />
38+
<Route index element={<Home />} />
3539
<Route path="onboarding" element={<OnboardingPage />} />
36-
<Route path="letter">
37-
<Route element={<Layout />}>
38-
<Route path="random" element={<RandomLettersPage />} />
39-
<Route path="box" element={<LetterBoxPage />} />
40-
<Route path="box/:id" element={<LetterBoxDetailPage />} />
40+
41+
<Route element={<PrivateRoute />}>
42+
<Route path="letter">
43+
<Route element={<Layout />}>
44+
<Route path="random" element={<RandomLettersPage />} />
45+
<Route path="box" element={<LetterBoxPage />} />
46+
<Route path="box/:id" element={<LetterBoxDetailPage />} />
47+
</Route>
48+
<Route path="write" element={<WritePage />} />
49+
<Route path=":id" element={<LetterDetailPage />} />
4150
</Route>
42-
<Route path="write" element={<WritePage />} />
43-
<Route path=":id" element={<LetterDetailPage />} />
44-
</Route>
45-
<Route path="board">
46-
<Route element={<Layout />}>
47-
<Route path="rolling/:id" element={<RollingPaperPage />} />
48-
<Route path="letter" element={<LetterBoardPage />} />
51+
<Route path="board">
52+
<Route element={<Layout />}>
53+
<Route path="rolling/:id" element={<RollingPaperPage />} />
54+
<Route path="letter" element={<LetterBoardPage />} />
55+
</Route>
56+
<Route path="letter/:id" element={<LetterBoardDetailPage />} />
57+
</Route>
58+
<Route path="mypage" element={<Layout />}>
59+
<Route index element={<MyPage />} />
60+
<Route path="board" element={<LetterBoardPage />} />
61+
<Route path="notifications" element={<NotificationsPage />} />
4962
</Route>
50-
<Route path="letter/:id" element={<LetterBoardDetailPage />} />
51-
</Route>
52-
<Route path="mypage" element={<Layout />}>
53-
<Route index element={<MyPage />} />
54-
<Route path="board" element={<LetterBoardPage />} />
55-
<Route path="notifications" element={<NotificationsPage />} />
5663
</Route>
57-
<Route path="*" element={<NotFoundPage />}></Route>
5864
</Route>
5965

60-
<Route path="admin" element={<AdminPage />}>
61-
<Route path="report" element={<ReportManage />} />
62-
<Route path="badwords" element={<FilteringManage />} />
63-
<Route path="filtered-letter" element={<FilteredLetterManage />} />
66+
<Route element={<PrivateRoute />}>
67+
<Route path="admin" element={<AdminPage />}>
68+
<Route path="report" element={<ReportManage />} />
69+
<Route path="badwords" element={<FilteringManage />} />
70+
<Route path="filtered-letter" element={<FilteredLetterManage />} />
71+
</Route>
6472
</Route>
6573
</Routes>
6674
);

src/apis/auth.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import client from './client';
2+
3+
export const socialLogin = (loginType: LoginType) => {
4+
window.location.href = `${import.meta.env.VITE_API_URL}/oauth2/authorization/${loginType}`;
5+
};
6+
7+
export const getUserToken = async (stateToken: string) => {
8+
try {
9+
const response = await client.get(`/api/auth/token?state=${stateToken}`);
10+
if (!response) throw new Error('getUserToken: Error while fetching user token');
11+
const userInfo = response.data;
12+
if (userInfo) {
13+
return userInfo;
14+
}
15+
return response;
16+
} catch (error) {
17+
console.error(error);
18+
throw error;
19+
}
20+
};
21+
22+
export const postZipCode = async () => {
23+
try {
24+
const response = await client.post(`/api/members/zipCode`);
25+
if (!response) throw new Error('fail to post ZipCode');
26+
return response;
27+
} catch (error) {
28+
console.error(error);
29+
}
30+
};
31+
32+
export const getNewToken = async () => {
33+
try {
34+
const response = await client.post('/api/reissue', {}, { withCredentials: true });
35+
if (!response) throw new Error('getNewToken: no response data');
36+
return response;
37+
} catch (error) {
38+
console.error(error);
39+
}
40+
};
41+
42+
export const getMydata = async () => {
43+
try {
44+
const response = await client.get('/api/members/me');
45+
if (!response) throw new Error('getNewToken: no response data');
46+
return response;
47+
} catch (error) {
48+
console.error(error);
49+
}
50+
};
51+
52+
export const deleteUserInfo = async () => {
53+
try {
54+
const response = await client.delete('/api/members/me', {
55+
withCredentials: true,
56+
});
57+
if (!response) throw new Error('deleteUserInfo: no response');
58+
return response;
59+
} catch (error) {
60+
console.error(error);
61+
}
62+
};
63+
64+
export const postLogout = async () => {
65+
try {
66+
const response = await client.post('/api/logout', { withCredentials: true });
67+
if (!response) throw new Error('postLogout: failed to logout');
68+
return response;
69+
} catch (error) {
70+
console.error(error);
71+
}
72+
};

src/apis/client.ts

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,109 @@
11
import axios from 'axios';
22

3+
import useAuthStore from '@/stores/authStore';
4+
5+
import { getNewToken } from './auth';
6+
37
const client = axios.create({
48
baseURL: import.meta.env.VITE_API_URL,
9+
headers: { 'Content-Type': 'application/json' },
510
});
611

7-
// client.interceptors.request.use(
8-
// (config) => {
9-
// const token = localStorage.getItem('authToken');
10-
// if (token) {
11-
// config.headers['Authorization'] = `Bearer ${token}`;
12-
// }
13-
// return config;
14-
// },
15-
// (error) => {
16-
// //TODO: 에러처리
17-
// return Promise.reject(error);
18-
// },
19-
// );
12+
type FailedRequest = {
13+
resolve: (token: string) => void;
14+
reject: (error: unknown) => void;
15+
};
16+
17+
let isRefreshing = false;
18+
let failedQueue: FailedRequest[] = [];
19+
20+
const processQueue = (error: unknown, token: string | null = null) => {
21+
failedQueue.forEach((prom) => {
22+
if (error) {
23+
prom.reject(error);
24+
} else {
25+
if (token) {
26+
prom.resolve(token);
27+
}
28+
}
29+
});
30+
31+
failedQueue = [];
32+
};
33+
34+
client.interceptors.request.use(
35+
(config) => {
36+
const accessToken = useAuthStore.getState().accessToken;
37+
if (config.url !== '/auth/reissue' && accessToken) {
38+
config.headers.Authorization = `Bearer ${accessToken}`;
39+
}
40+
41+
return config;
42+
},
43+
(error) => Promise.reject(error),
44+
);
45+
46+
client.interceptors.response.use(
47+
(response) => response,
48+
async (error) => {
49+
const setAccessToken = useAuthStore.getState().setAccessToken;
50+
const logout = useAuthStore.getState().logout;
51+
const originalRequest = error.config;
52+
53+
if (!originalRequest) return Promise.reject(error);
54+
55+
if (
56+
originalRequest.url === '/auth/reissue' ||
57+
originalRequest.url.includes('/api/auth/token?state=')
58+
) {
59+
return Promise.reject(error);
60+
}
61+
62+
if (
63+
(error.response?.status === 401 || error.response?.status === 403) &&
64+
!originalRequest._retry
65+
) {
66+
originalRequest._retry = true;
67+
68+
if (isRefreshing) {
69+
try {
70+
return new Promise((resolve, reject) => {
71+
failedQueue.push({
72+
resolve: (token: string) => {
73+
originalRequest.headers.Authorization = `Bearer ${token}`;
74+
resolve(client(originalRequest));
75+
},
76+
reject: (err: unknown) => reject(err),
77+
});
78+
});
79+
} catch (e) {
80+
return Promise.reject(e);
81+
}
82+
}
83+
84+
isRefreshing = true;
85+
86+
try {
87+
const response = await getNewToken();
88+
const newToken = response?.data.accessToken;
89+
90+
if (!newToken) throw new Error('Failed to refresh token');
91+
92+
setAccessToken(newToken);
93+
processQueue(null, newToken);
94+
95+
isRefreshing = false;
96+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
97+
return client(originalRequest);
98+
} catch (e) {
99+
processQueue(e, null);
100+
isRefreshing = false;
101+
logout();
102+
return Promise.reject(e);
103+
}
104+
}
105+
return Promise.reject(error);
106+
},
107+
);
20108

21109
export default client;

src/apis/letterDetail.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import client from './client';
22

3-
const getLetter = async (
4-
letterId: string,
5-
setLetterState: React.Dispatch<React.SetStateAction<LetterDetail | null>>,
6-
) => {
3+
const getLetter = async (letterId: string) => {
74
try {
85
const res = await client.get(`/api/letters/${letterId}`);
9-
setLetterState(res.data.data);
6+
if (!res) throw new Error('편지 데이터를 가져오는 도중 에러가 발생했습니다.');
107
console.log(res);
8+
return res;
119
} catch (error) {
1210
console.error(error);
1311
}
@@ -17,7 +15,9 @@ const deleteLetter = async (letterId: string) => {
1715
try {
1816
console.log(`/api/letters/${letterId}`);
1917
const res = await client.delete(`/api/letters/${letterId}`);
18+
if (!res) throw new Error('편지 삭제 요청 도중 에러가 발생했습니다.');
2019
console.log(res);
20+
return res;
2121
} catch (error) {
2222
console.error(error);
2323
}

0 commit comments

Comments
 (0)