Skip to content

Commit a413acd

Browse files
committed
2 parents bc00acd + 7e08051 commit a413acd

10 files changed

Lines changed: 122 additions & 24 deletions

File tree

apps/ticket/src/app/(pages)/event/[eventId]/_clientBoundray/SelectTicketBottomSheet/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ const SelectTicketBottomSheetContent = ({
250250
// TODO: 토스트나 커스텀 모달로 변경
251251
alert(error.response?.data.message);
252252
}
253-
253+
} finally {
254254
setIsLoading(false);
255255
}
256256
});

apps/ticket/src/app/(pages)/order/[orderId]/_clientBoundary/TossPaymentWidget/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ export const TossPaymentWidget = ({ orderId }: Props) => {
116116
<Typography type="body14" color="gray300">
117117
* 환불 정책은 페이지 하단을 참고해주세요.
118118
</Typography>
119+
<Typography type="body14" color="gray300">
120+
* 결제창이 계속해서 보이지 않는다면 새로고침 해주세요.
121+
</Typography>
119122
</div>
120123
</>
121124
);

apps/ticket/src/app/(pages)/staff/ticket-authorization/_clientBoundary/TicketAuthorizationClient/index.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import classNames from "classnames/bind";
77
import { Flex, Typography } from "@permit/design-system";
88
import { useGuestTicketCameraConfirmMutation } from "@/data/tickets/postStaffGuestTicketDoorConfirm/mutation";
99
import { useUserTicketCameraConfirmMutation } from "@/data/tickets/postStaffTicketDoorConfirm/mutation";
10+
import { useUserInfoSuspenseQuery } from "@/data/users/getUserInfo/queries";
11+
import { ERROR_CODE } from "@/lib/axios/utils/errorCode";
1012
import { isAxiosErrorResponse } from "@/shared/types/axioxError";
1113

1214
import styles from "./index.module.scss";
@@ -137,6 +139,9 @@ const showToast = (message: string, type: "success" | "error" = "success") => {
137139

138140
export const TicketAuthorizationClient = () => {
139141
const qc = useQueryClient();
142+
143+
const { data: userInfoData } = useUserInfoSuspenseQuery({ refetchOnWindowFocus: true });
144+
140145
const [scannedTicketCode, setScannedTicketCode] = useState<string | null>(null);
141146
const [isGuest, setIsGuest] = useState(false);
142147
const [lastScannedCode, setLastScannedCode] = useState<string | null>(null);
@@ -303,6 +308,8 @@ export const TicketAuthorizationClient = () => {
303308
message = "이미 사용한 티켓입니다.";
304309
} else if (error.response?.data.code === CANCELED_TICKET) {
305310
message = "취소된 티켓입니다.";
311+
} else if (error.response?.data.code === ERROR_CODE.SECURITY_ENTRY) {
312+
message = "권한 업데이트를 위해 로그아웃 후 재 로그인 해주세요.";
306313
} else if (error.response?.data.message) {
307314
message = error.response?.data.message;
308315
}
@@ -322,6 +329,18 @@ export const TicketAuthorizationClient = () => {
322329
verifyTicket();
323330
}, [scannedTicketCode, isGuest, guestTicketCameraMutate, userTicketCameraMutate]);
324331

332+
if (userInfoData.role === "USER") {
333+
return (
334+
<div className={cx("container")}>
335+
<Flex className={cx("header")} direction="column" align="center" gap={16}>
336+
<Typography type="title18" weight="bold" color="white">
337+
접근 권한이 없습니다.
338+
</Typography>
339+
</Flex>
340+
</div>
341+
);
342+
}
343+
325344
return (
326345
<div className={cx("container")}>
327346
<Flex className={cx("header")} direction="column" align="center" gap={16}>
Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,62 @@
1+
import { ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
12
import { cookies } from "next/headers";
23
import { NextResponse } from "next/server";
34

45
import { API_URL } from "@/data/constants";
6+
import { ERROR_CODE } from "@/lib/axios/utils/errorCode";
57

6-
/**
7-
* 로그아웃 요청 API
8-
*/
9-
export async function POST(req: Request) {
10-
const cookiesStore = await cookies();
11-
12-
const apiRes = await fetch(process.env.NEXT_PUBLIC_TICKET_API_BASE_URL + API_URL.USER.LOGOUT, {
8+
async function requestLogout(cookiesStore: ReadonlyRequestCookies) {
9+
return fetch(process.env.NEXT_PUBLIC_TICKET_API_BASE_URL + API_URL.USER.LOGOUT, {
1310
method: "POST",
1411
headers: {
1512
"Content-Type": "application/json",
1613
Cookie: `accessToken=${cookiesStore.get("accessToken")?.value}; refreshToken=${cookiesStore.get("refreshToken")?.value}`,
1714
},
1815
credentials: "include",
1916
});
17+
}
2018

21-
const setCookies = apiRes.headers.getSetCookie();
19+
export async function POST() {
20+
let apiRes: Response | null = null;
2221

23-
const res = NextResponse.json(await apiRes.json(), {
24-
status: apiRes.status,
25-
});
22+
try {
23+
const cookiesStore = await cookies();
24+
25+
apiRes = await requestLogout(cookiesStore);
26+
27+
const data = await apiRes.clone().json();
2628

27-
// API 서버가 내려준 모든 Set-Cookie를 그대로 전달
28-
for (const cookie of setCookies) {
29-
// 기존 쿠키 그대로 전달
30-
// SSR 환경에서 브라우저 쿠키 사용할 수 있도록 하기 위함
31-
res.headers.append("Set-Cookie", cookie);
29+
if (data?.code === ERROR_CODE.ACCESS_TOKEN_EXPIRED) {
30+
await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/reissue`, {
31+
method: "POST",
32+
credentials: "include",
33+
});
3234

33-
// Domain 추가한 쿠키 한 번 더 전달
34-
// 브라우저 단에서 자동으로 쿠키가 포함한 요청이 갈 수 있도록 하기 위함
35-
res.headers.append("Set-Cookie", `${cookie}; Domain=.permitseoul.com`);
35+
apiRes = await requestLogout(await cookies());
36+
}
37+
} catch (e) {
38+
// ❗ 실패해도 무시
3639
}
3740

41+
const res = NextResponse.json({ success: true }, { status: 200 });
42+
43+
// 성공/실패 무관하게 쿠키 제거
44+
clearAuthCookies(res);
45+
3846
return res;
3947
}
48+
49+
function clearAuthCookies(res: NextResponse) {
50+
for (const name of ["accessToken", "refreshToken"]) {
51+
res.headers.append(
52+
"Set-Cookie",
53+
`${name}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=None`,
54+
);
55+
56+
// 도메인 쿠키까지 같이 제거
57+
res.headers.append(
58+
"Set-Cookie",
59+
`${name}=; Path=/; Domain=.permitseoul.com; Max-Age=0; HttpOnly; Secure; SameSite=None`,
60+
);
61+
}
62+
}

apps/ticket/src/app/api/reissue/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export async function POST(req: Request) {
1818
},
1919
);
2020

21+
if (!apiRes.ok) {
22+
throw new Error("Token reissue failed");
23+
}
24+
2125
const setCookies = apiRes.headers.getSetCookie();
2226

2327
const res = NextResponse.json(await apiRes.json(), {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { NextResponse } from "next/server";
2+
3+
import { API_URL } from "@/data/constants";
4+
5+
/**
6+
* 회원가입 요청 API
7+
* NOTE: API 서버에서 Set-Cookie 설정해주는 것을 브라우저에서 동일하게 쿠키 설정하기 위함
8+
*/
9+
export async function POST(req: Request) {
10+
const body = await req.json();
11+
12+
const apiRes = await fetch(process.env.NEXT_PUBLIC_TICKET_API_BASE_URL + API_URL.USER.SIGNUP, {
13+
method: "POST",
14+
headers: { "Content-Type": "application/json" },
15+
body: JSON.stringify(body),
16+
credentials: "include",
17+
});
18+
19+
const setCookies = apiRes.headers.getSetCookie();
20+
21+
const res = NextResponse.json(await apiRes.json(), {
22+
status: apiRes.status,
23+
});
24+
25+
// API 서버가 내려준 모든 Set-Cookie를 그대로 전달
26+
for (const cookie of setCookies) {
27+
// 기존 쿠키 그대로 전달
28+
// SSR 환경에서 브라우저 쿠키 사용할 수 있도록 하기 위함
29+
res.headers.append("Set-Cookie", cookie);
30+
31+
// Domain 추가한 쿠키 한 번 더 전달
32+
// 브라우저 단에서 자동으로 쿠키가 포함한 요청이 갈 수 있도록 하기 위함
33+
res.headers.append("Set-Cookie", `${cookie}; Domain=.permitseoul.com`);
34+
}
35+
36+
return res;
37+
}

apps/ticket/src/data/users/getUserInfo/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ export type UserInfo = {
77
gender: string;
88
/** 회원 이메일 */
99
email: string;
10+
/** 회원 role */
11+
role: "USER" | "ADMIN" | "STAFF";
1012
};

apps/ticket/src/data/users/postUserLogout/mutation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export const useLogoutMutation = (options?: LogoutMutationOptions<LogoutResponse
2222
credentials: "include",
2323
});
2424

25+
if (!res.ok) {
26+
throw new Error("Logout failed");
27+
}
28+
2529
return res.json();
2630
},
2731
...options,

apps/ticket/src/data/users/postUserSignup/mutation.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
22

3-
import { API_URL } from "@/data/constants";
4-
import { instance } from "@/lib/axios";
53
import { SocialLoginType } from "@/shared/hooks/useOAuth/types";
64

75
type SignupRequest = {
@@ -27,9 +25,16 @@ export type SignupMutationOptions<TData> = Omit<
2725
export const useSignupMutation = (options?: SignupMutationOptions<SignupResponse>) => {
2826
return useMutation({
2927
mutationFn: async (params: SignupRequest) => {
30-
const { data } = await instance.post<SignupResponse>(API_URL.USER.SIGNUP, params);
28+
const res = await fetch("/api/sign-up", {
29+
method: "POST",
30+
headers: {
31+
"Content-Type": "application/json",
32+
},
33+
credentials: "include",
34+
body: JSON.stringify(params),
35+
});
3136

32-
return data;
37+
return res.json();
3338
},
3439
...options,
3540
});

apps/ticket/src/lib/axios/utils/errorCode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const ERROR_CODE = {
22
LOGIN_REQUIRED: 40402,
33
ACCESS_TOKEN_EXPIRED: 40103,
44
REFRESH_TOKEN_EXPIRED: 40104,
5+
SECURITY_ENTRY: 40106,
56
NO_ACCESS_TOKEN: 40403,
67
PAYMENT: 50004,
78
} as const;

0 commit comments

Comments
 (0)