Skip to content

Commit 5cd22d1

Browse files
dooohunff1451
andauthored
[공통] 쿠키 도메인 확장으로 인한 SSR 500 에러 수정 (#1197)
Co-authored-by: 이준영 <54898597+ff1451@users.noreply.github.com>
1 parent 7f8a671 commit 5cd22d1

10 files changed

Lines changed: 203 additions & 115 deletions

File tree

src/middleware.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,36 @@ export function middleware(request: NextRequest) {
1010
const token = request.cookies.get(COOKIE_KEY.AUTH_TOKEN)?.value;
1111

1212
if (token && isTokenExpired(token)) {
13-
const response = NextResponse.next();
13+
// 만료된 토큰을 SSR 요청에서도 제거해 getServerSideProps의 500 오류를 방지
14+
const requestHeaders = new Headers(request.headers);
15+
const remainingCookies = request.cookies
16+
.getAll()
17+
.filter((cookie) => cookie.name !== COOKIE_KEY.AUTH_TOKEN && cookie.name !== COOKIE_KEY.AUTH_USER_TYPE)
18+
.map((cookie) => `${cookie.name}=${cookie.value}`)
19+
.join('; ');
20+
21+
if (remainingCookies) {
22+
requestHeaders.set('cookie', remainingCookies);
23+
} else {
24+
requestHeaders.delete('cookie');
25+
}
26+
27+
const response = NextResponse.next({ request: { headers: requestHeaders } });
1428

1529
const hostname = request.nextUrl.hostname;
30+
const baseOptions = `Path=/; Max-Age=0; Expires=${new Date(0).toUTCString()}; SameSite=Lax`;
1631

17-
const options: Parameters<typeof response.cookies.set>[2] = {
18-
expires: new Date(0),
19-
path: '/',
20-
};
32+
// host-only 쿠키와 domain 쿠키를 모두 삭제 (host-only 쿠키가 남아있는 사용자가 있을 수 있어 임시로 둔 후 추후 제거하도록 하겠습니다.)
33+
const cookieStrings = [COOKIE_KEY.AUTH_TOKEN, COOKIE_KEY.AUTH_USER_TYPE].flatMap((name) => {
34+
const hostOnly = `${name}=; ${baseOptions}`;
35+
if (isLocalhost(hostname)) return [hostOnly];
36+
return [hostOnly, `${name}=; Domain=${COOKIE_DOMAIN}; ${baseOptions}`];
37+
});
2138

22-
if (!isLocalhost(hostname)) {
23-
options.domain = COOKIE_DOMAIN;
24-
}
39+
const [first, ...rest] = cookieStrings;
40+
response.headers.set('Set-Cookie', first);
41+
rest.forEach((cookie) => response.headers.append('Set-Cookie', cookie));
2542

26-
response.cookies.set(COOKIE_KEY.AUTH_TOKEN, '', options);
2743
return response;
2844
}
2945

src/pages/articles/index.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,32 @@ import ArticlesHeader from 'components/Articles/components/ArticlesHeader';
99
import Pagination from 'components/Articles/components/Pagination';
1010
import useArticles from 'components/Articles/hooks/useArticles';
1111
import { SSRLayout } from 'components/layout';
12-
import { COOKIE_KEY } from 'static/url';
1312
import useMount from 'utils/hooks/state/useMount';
13+
import { parseServerSideParams } from 'utils/ts/parseServerSideParams';
1414

1515
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
16-
const page = context.query.page;
17-
const pageNumber = typeof page === 'string' ? page : '1';
16+
const { token, query } = parseServerSideParams(context);
17+
const pageNumber = typeof query.page === 'string' ? query.page : '1';
1818

1919
const queryClient = new QueryClient();
20-
const token = context.req.cookies[COOKIE_KEY.AUTH_TOKEN] || '';
2120

22-
await Promise.all([
23-
queryClient.prefetchQuery({
24-
queryKey: ['articles', pageNumber],
25-
queryFn: () => articlesApi.getArticles(token, pageNumber),
26-
}),
21+
const prefetchPromises = [
2722
queryClient.prefetchQuery({
2823
queryKey: ['hotArticles'],
2924
queryFn: articlesApi.getHotArticles,
3025
}),
31-
]);
26+
];
27+
28+
if (token) {
29+
prefetchPromises.push(
30+
queryClient.prefetchQuery({
31+
queryKey: ['articles', pageNumber],
32+
queryFn: () => articlesApi.getArticles(token, pageNumber),
33+
}),
34+
);
35+
}
36+
37+
await Promise.all(prefetchPromises);
3238

3339
return {
3440
props: {

src/pages/clubs/[id]/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ import ConfirmModal from 'components/Club/NewClubRecruitment/components/ConfirmM
3030
import { SSRLayout } from 'components/layout';
3131
import LoginRequiredModal from 'components/modal/LoginRequiredModal';
3232
import ROUTES from 'static/routes';
33-
import { COOKIE_KEY } from 'static/url';
3433
import useLogger from 'utils/hooks/analytics/useLogger';
3534
import { useDebounce } from 'utils/hooks/debounce/useDebounce';
3635
import useMediaQuery from 'utils/hooks/layout/useMediaQuery';
3736
import useBooleanState from 'utils/hooks/state/useBooleanState';
3837
import useTokenState from 'utils/hooks/state/useTokenState';
3938
import { formatPhoneNumber } from 'utils/ts/formatPhoneNumber';
39+
import { parseServerSideParams } from 'utils/ts/parseServerSideParams';
4040
import showToast from 'utils/ts/showToast';
4141
import { useHeaderTitle } from 'utils/zustand/customTitle';
4242
import type { ClubRecruitmentResponse } from 'api/club/entity';
@@ -73,7 +73,8 @@ const EMPTY_RECRUITMENT: ClubRecruitmentResponse = {
7373
};
7474

7575
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
76-
const { params, req, query } = context;
76+
const { params, query } = context;
77+
const { token } = parseServerSideParams(context);
7778
const id = params?.id;
7879

7980
if (!id || Array.isArray(id)) {
@@ -83,7 +84,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
8384
}
8485

8586
const clubId = Number(id);
86-
const token = req.cookies[COOKIE_KEY.AUTH_TOKEN] || '';
8787
const tab = query.tab as TabType | undefined;
8888
const eventId = query.eventId as string | undefined;
8989
const numericEventId = eventId ? Number(eventId) : NO_SELECTED_EVENT_ID;
@@ -98,7 +98,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
9898
await Promise.all([
9999
queryClient.prefetchQuery({
100100
queryKey: ['clubDetail', clubId],
101-
queryFn: () => getClubDetail(token, clubId),
101+
queryFn: () => getClubDetail(token ?? '', clubId),
102102
}),
103103

104104
queryClient.prefetchQuery({

src/pages/index.tsx

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
22
import { isKoinError } from '@bcsdlab/koin';
33
import { dehydrate, QueryClient } from '@tanstack/react-query';
4-
import { club, articles as articlesApi, banner, timetable } from 'api';
5-
import { getLostItemStat } from 'api/articles';
6-
import { getBannerCategoryList } from 'api/banner';
4+
import { getArticles, getLostItemStat } from 'api/articles';
5+
import { getBannerCategoryList, getBanners } from 'api/banner';
6+
import { getHotClub } from 'api/club';
77
import { HotClubResponse } from 'api/club/entity';
88
import { getStoreCategories } from 'api/store';
9+
import { getMySemester, getSemesterInfoList, getTimetableFrame, getTimetableLectureInfo } from 'api/timetable';
910
import IndexArticles from 'components/IndexComponents/IndexArticles';
1011
import IndexBus from 'components/IndexComponents/IndexBus';
1112
import IndexCafeteria from 'components/IndexComponents/IndexCafeteria';
@@ -22,11 +23,13 @@ import Banner from 'components/ui/Banner';
2223
import UserInfoModal from 'components/ui/UserInfoModal';
2324
import { COOKIE_KEY } from 'static/url';
2425
import { getRecentSemester } from 'utils/timetable/semester';
26+
import { parseServerSideParams } from 'utils/ts/parseServerSideParams';
27+
import { clearServerAuthCookies, isServerAuthError } from 'utils/ts/ssrAuth';
2528
import styles from './IndexPage.module.scss';
2629

27-
const getHotClub = async () => {
30+
const getHotClubData = async () => {
2831
try {
29-
return await club.getHotClub();
32+
return await getHotClub();
3033
} catch (e) {
3134
if (isKoinError(e) && e.status === 404) {
3235
return {
@@ -41,28 +44,44 @@ const getHotClub = async () => {
4144

4245
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
4346
const queryClient = new QueryClient();
44-
const token = context.req.cookies[COOKIE_KEY.AUTH_TOKEN] || '';
45-
const userType = context.req.cookies[COOKIE_KEY.AUTH_USER_TYPE] || '';
47+
let token = parseServerSideParams(context).token ?? '';
48+
let userType = context.req.cookies[COOKIE_KEY.AUTH_USER_TYPE] || '';
49+
50+
const resetAuthContext = () => {
51+
token = '';
52+
userType = '';
53+
clearServerAuthCookies(context);
54+
};
55+
56+
const fetchMySemester = async () => {
57+
if (!token || userType !== 'STUDENT') return null;
58+
59+
try {
60+
return await queryClient.fetchQuery({
61+
queryKey: [MY_SEMESTER_INFO_KEY],
62+
queryFn: () => getMySemester(token),
63+
});
64+
} catch (error) {
65+
if (isServerAuthError(error)) {
66+
resetAuthContext();
67+
return null;
68+
}
69+
if (isKoinError(error) && error.status === 403) {
70+
return null;
71+
}
72+
throw error;
73+
}
74+
};
4675

4776
const [[banners, categories, hotClubInfo, mySemester]] = await Promise.all([
48-
Promise.all([
49-
getBannerCategoryList(),
50-
getStoreCategories(),
51-
getHotClub(),
52-
token && userType === 'STUDENT'
53-
? queryClient.fetchQuery({
54-
queryKey: [MY_SEMESTER_INFO_KEY],
55-
queryFn: () => timetable.getMySemester(token),
56-
})
57-
: null,
58-
]),
77+
Promise.all([getBannerCategoryList(), getStoreCategories(), getHotClubData(), fetchMySemester()]),
5978
queryClient.prefetchQuery({
6079
queryKey: ['articles', '1'],
61-
queryFn: () => articlesApi.getArticles(token, '1'),
80+
queryFn: () => getArticles(token, '1'),
6281
}),
6382
queryClient.prefetchQuery({
6483
queryKey: [SEMESTER_INFO_KEY],
65-
queryFn: timetable.getSemesterInfoList,
84+
queryFn: getSemesterInfoList,
6685
}),
6786
queryClient.prefetchQuery({
6887
queryKey: ['lostItemStat'],
@@ -73,23 +92,30 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
7392
const userSemester = mySemester?.semesters?.[0] || getRecentSemester();
7493

7594
const bannerCategoryId = Number(banners.banner_categories[0].id);
76-
const bannersList = await banner.getBanners(bannerCategoryId);
95+
const bannersList = await getBanners(bannerCategoryId);
7796
const isBannerOpen =
7897
context.req.cookies['HIDE_BANNER'] !== `modal_category_${bannerCategoryId}` && bannersList.count !== 0;
7998

80-
let mainFrameId: number | null = null;
8199
if (token && userType === 'STUDENT') {
82-
const timetableFrameList = await queryClient.fetchQuery({
83-
queryKey: [TIMETABLE_FRAME_KEY + userSemester.year + userSemester.term],
84-
queryFn: () => timetable.getTimetableFrame(token, userSemester),
85-
});
86-
const mainFrame = timetableFrameList.find((frame) => frame.is_main);
87-
mainFrameId = mainFrame?.id ?? null;
88-
if (mainFrameId !== null) {
89-
await queryClient.prefetchQuery({
90-
queryKey: [TIMETABLE_INFO_LIST, mainFrameId],
91-
queryFn: () => timetable.getTimetableLectureInfo(token, mainFrameId!),
100+
try {
101+
const timetableFrameList = await queryClient.fetchQuery({
102+
queryKey: [TIMETABLE_FRAME_KEY + userSemester.year + userSemester.term],
103+
queryFn: () => getTimetableFrame(token, userSemester),
92104
});
105+
const mainFrame = timetableFrameList.find((frame) => frame.is_main);
106+
const activeMainFrameId = mainFrame?.id;
107+
if (typeof activeMainFrameId === 'number') {
108+
await queryClient.prefetchQuery({
109+
queryKey: [TIMETABLE_INFO_LIST, activeMainFrameId],
110+
queryFn: () => getTimetableLectureInfo(token, activeMainFrameId),
111+
});
112+
}
113+
} catch (error) {
114+
if (isServerAuthError(error)) {
115+
resetAuthContext();
116+
} else if (!(isKoinError(error) && error.status === 403)) {
117+
throw error;
118+
}
93119
}
94120
}
95121

src/pages/lost-item/[id]/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { GetServerSidePropsContext } from 'next';
22
import { useRouter } from 'next/router';
33
import { dehydrate, QueryClient } from '@tanstack/react-query';
4-
import { articles } from 'api';
4+
import { getSingleLostItemArticle, getLostItemArticles } from 'api/articles';
55
import ChatIcon from 'assets/svg/Articles/chat.svg';
66
import ReportIcon from 'assets/svg/Articles/report.svg';
77
import HotArticles from 'components/Articles/components/HotArticle';
@@ -20,20 +20,20 @@ import useSingleLostItemArticle from 'components/Articles/LostItemDetailPage/hoo
2020
import { SSRLayout } from 'components/layout';
2121
import LoginRequiredModal from 'components/modal/LoginRequiredModal';
2222
import ROUTES from 'static/routes';
23-
import { COOKIE_KEY } from 'static/url';
2423
import useMediaQuery from 'utils/hooks/layout/useMediaQuery';
2524
import useModalPortal from 'utils/hooks/layout/useModalPortal';
2625
import useBooleanState from 'utils/hooks/state/useBooleanState';
2726
import useTokenState from 'utils/hooks/state/useTokenState';
2827
import useScrollToTop from 'utils/hooks/ui/useScrollToTop';
28+
import { parseServerSideParams } from 'utils/ts/parseServerSideParams';
2929
import styles from './LostItemDetailPage.module.scss';
3030

3131
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
32+
const { token } = parseServerSideParams(context);
3233
const id = context.query.id;
3334
if (typeof id !== 'string') {
3435
return { notFound: true };
3536
}
36-
const token = context.req.cookies[COOKIE_KEY.AUTH_TOKEN] || '';
3737
const articleId = Number(id);
3838

3939
const queryClient = new QueryClient();
@@ -43,11 +43,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
4343
await Promise.all([
4444
queryClient.prefetchQuery({
4545
queryKey: ['lostItem', 'detail', articleId],
46-
queryFn: () => articles.getSingleLostItemArticle(token, articleId),
46+
queryFn: () => getSingleLostItemArticle(token ?? '', articleId),
4747
}),
4848
queryClient.prefetchInfiniteQuery({
4949
queryKey: ['lostItem', latestLostItemParams],
50-
queryFn: () => articles.getLostItemArticles(token, { ...latestLostItemParams, page: 1 }),
50+
queryFn: () => getLostItemArticles(token ?? '', { ...latestLostItemParams, page: 1 }),
5151
initialPageParam: 1,
5252
}),
5353
]);

src/pages/lost-item/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import useLostItemPagination from 'components/Articles/hooks/useLostItemPaginati
1111
import { useLostItemSearch } from 'components/Articles/hooks/useLostItemSearch';
1212
import { LostItemParams, parseLostItemQuery } from 'components/Articles/utils/lostItemQuery';
1313
import { SSRLayout } from 'components/layout';
14-
import { COOKIE_KEY } from 'static/url';
1514
import useMount from 'utils/hooks/state/useMount';
15+
import { parseServerSideParams } from 'utils/ts/parseServerSideParams';
1616
import styles from './LostItemArticleListPage.module.scss';
1717

1818
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
1919
const queryClient = new QueryClient();
20-
const token = context.req.cookies[COOKIE_KEY.AUTH_TOKEN] || '';
20+
const { token, query } = parseServerSideParams(context);
2121

2222
const fallback: LostItemParams = {
2323
page: 1,
@@ -28,13 +28,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
2828
author: 'ALL',
2929
};
3030

31-
const params = parseLostItemQuery(context.query, fallback);
31+
const params = parseLostItemQuery(query, fallback);
3232

3333
const apiParams = toLostItemArticlesRequest(params);
3434

3535
await queryClient.prefetchQuery({
3636
queryKey: ['lostItemPagination', apiParams],
37-
queryFn: () => getLostItemArticles(token, apiParams),
37+
queryFn: () => getLostItemArticles(token ?? '', apiParams),
3838
});
3939

4040
return {

0 commit comments

Comments
 (0)