-
Notifications
You must be signed in to change notification settings - Fork 1
[Init] swagger-typescript-api 및 axios 초기 세팅 #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 23 commits
b1c3e3d
2b26a45
beff28e
041ae11
a5b7c96
0a14386
a6a4e48
e58c220
3ee6729
f0d0595
ec2d168
c5776c8
64c41f7
ceb4899
e3e6396
afa8493
df79bed
b3e2e08
4615c18
e250385
b68840f
cd057ed
ab7973d
7edd120
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,9 @@ | ||
| import { RouterProvider } from "react-router-dom"; | ||
|
|
||
| import ThemeProvider from "./providers"; | ||
| import { router } from "./routes/app-router"; | ||
|
|
||
| const App = () => { | ||
| return ( | ||
| <ThemeProvider> | ||
| <RouterProvider router={router} /> | ||
| </ThemeProvider> | ||
| ); | ||
| return <RouterProvider router={router} />; | ||
| }; | ||
|
|
||
| export default App; |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,22 @@ | ||
| import type { PropsWithChildren } from "react"; | ||
| import { QueryProvider } from "./query-provider"; | ||
| import ThemeProvider from "./theme-provider"; | ||
|
|
||
| import QueryProvider from "./query-provider"; | ||
| import type { ReactNode } from "react"; | ||
|
|
||
| export default function AppProviders({ children }: PropsWithChildren) { | ||
| return <QueryProvider>{children}</QueryProvider>; | ||
| } | ||
| export const AppProviders = ({ | ||
| theme, | ||
| className, | ||
| children, | ||
| }: { | ||
| children: ReactNode; | ||
| theme?: string; | ||
| className?: string; | ||
| }) => { | ||
| return ( | ||
| <QueryProvider> | ||
| <ThemeProvider theme={theme} className={className}> | ||
| {children} | ||
| </ThemeProvider> | ||
| </QueryProvider> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| export { useAuthStore } from '@shared/model/auth'; | ||
| export { useAuthStore } from "@shared/model/auth"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| const BookmarkPage = () => { | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the Bookmark Page</h1> | ||
| </div> | ||
| ); | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the Bookmark Page</h1> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export { BookmarkPage }; | ||
| export { BookmarkPage }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| const LoginPage = () => { | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the Login Page</h1> | ||
| </div> | ||
| ); | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the Login Page</h1> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export { LoginPage }; | ||
| export { LoginPage }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| const MyPage = () => { | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the My Page</h1> | ||
| </div> | ||
| ); | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the My Page</h1> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export { MyPage }; | ||
| export { MyPage }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| const OnboardingPage = () => { | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the Onboarding Page</h1> | ||
| </div> | ||
| ); | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the Onboarding Page</h1> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export { OnboardingPage }; | ||
| export { OnboardingPage }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| const RegisterPage = () => { | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the Register Page</h1> | ||
| </div> | ||
| ); | ||
| return ( | ||
| <div> | ||
| <h1>Welcome to the Register Page</h1> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export { RegisterPage }; | ||
| export { RegisterPage }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import axios from "axios"; | ||
|
|
||
| import { tokenStorage } from "@lib/auth/token-storage"; | ||
|
|
||
| const SERVER_URL = import.meta.env.VITE_API_URL; | ||
|
|
||
| // TODO: 현재는 axios로 instance를 생성하지만, http-client.ts Api 클래스로 instance를 생성하도록 수정할 예정입니다. | ||
| export const api = axios.create({ | ||
| baseURL: `${SERVER_URL}`, | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| }); | ||
|
|
||
| api.interceptors.request.use( | ||
| async (config) => { | ||
| const accessToken = tokenStorage.get() || null; | ||
| if (accessToken) { | ||
| config.headers["Authorization"] = `Bearer ${accessToken}`; | ||
| } else { | ||
| // TODO: 로그인이 필요하다는 Toast Message | ||
| console.error("액세스 토큰이 존재하지 않습니다"); | ||
| window.location.replace("/login"); | ||
| throw new Error("액세스 토큰이 존재하지 않습니다"); | ||
| } | ||
| return config; | ||
| }, | ||
| (error) => Promise.reject(error) | ||
| ); | ||
|
|
||
| api.interceptors.response.use( | ||
| (response) => response, | ||
| async (error) => { | ||
| const originalRequest = error.config; | ||
|
|
||
| if (error.response?.status === 401 && !originalRequest._retry) { | ||
| originalRequest._retry = true; | ||
| try { | ||
| // TODO: refreshToken 관련은 차후 순위로 개발할 예정 | ||
| // 액세스 토큰 재발급 API | ||
| const { data } = await axios.post("토큰 재발급 path", null, { | ||
| withCredentials: true, // (토큰을 cookie에 넣었을 경우 사용됨) | ||
| }); | ||
|
|
||
| // 새로 발급 받은 액세스 토큰 저장 | ||
| const newAccessToken = data.accessToken; | ||
| tokenStorage.set(newAccessToken); | ||
|
|
||
| originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; | ||
| return api(originalRequest); // 이전 요청 재시도 | ||
| } catch (refreshError) { | ||
| // TODO: 재발급 실패시, 로그인 화면으로 이동 | ||
| console.error("리프레쉬 토큰 요청에 실패했습니다.", error); | ||
| localStorage.removeItem("access_token"); // TODO: 토큰, 유저 정보 모두 삭제 | ||
| window.location.replace("/login"); | ||
|
|
||
| return Promise.reject(refreshError); | ||
| } | ||
| } | ||
|
|
||
| return Promise.reject(error); | ||
| } | ||
| ); |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 코드에서
이 부분일까요? 여러 번 읽어보았는데 "query"일 때 에러 페이지로 이동하는 흐름이 어떤 식으로 이루어지는지 잘 이해가 되지 않아서 호옥시 조금 더 자세히 설명해주실 수 있을지 여쭤봅니다. !! 😅
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 생략 //
throwOnError: true, // throwOnError 옵션을 true 설정
},
mutations: {
throwOnError: false,
},
},
queryCache: new QueryCache({
onError: (error: unknown) => {
handleApiError(error, "query");
},
}),
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하~ 정확히 이해했습니다! mutation error의 경우는 사용자 경험 개선을 위해 alert 메시지로 처리해주기로 하였기 때문에, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { isAxiosError } from "axios"; | ||
|
|
||
| import { ERROR_MESSAGE } from "@/shared/api/types/error-response"; | ||
|
|
||
| import type { | ||
| ApiErrorResponse, | ||
| ApiErrorCode, | ||
| RequiredWith, | ||
| } from "@/shared/api/types/error-response"; | ||
| import type { AxiosError } from "axios"; | ||
|
|
||
| /** | ||
| * 서버 정의 에러(Business Error) 타입 가드 | ||
| * - HTTP 상태 코드는 정상이지만, 비즈니스 로직상 실패한 경우를 식별합니다. | ||
| * - 응답 본문(data)에 'prefix' 필드가 있는지 확인합니다. (추후 TODO) | ||
| */ | ||
|
Comment on lines
12
to
16
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 주석에 “HTTP 상태 코드는 정상이지만 비즈니스 로직상 실패한 경우”라고 적혀 있는데, 현재 구현은 status와 무관하게 prefix가 있으면 커스텀 에러로 분류되는 구조라서 설명과 실제 동작이 살짝 다른 것 같아서요! 지금 당장 기능에는 영향 없지만, 나중에 다른 사람이 호옥시 코드를 읽을 때 “200 OK인데 body로 내려오는 에러만 처리하나?”라고 오해할 수 있어서 주석을 “서버가 정의한 error payload 식별” 쪽으로 수정하거나 실제로 status 조건을 추가하면 더 좋을 것 같습니다 😽
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 호오 그렇네요!!!!!! 세심막내의 코리다..d=(^o^)=b |
||
| export const isValidCustomError = ( | ||
| error: unknown | ||
| ): error is RequiredWith<AxiosError<ApiErrorResponse>, "response"> => { | ||
| return ( | ||
| isAxiosError(error) && | ||
| !!error.response && | ||
| !!error.response.data && | ||
| typeof error.response.data.prefix === "string" | ||
| ); | ||
| }; | ||
|
|
||
| // 요청 종류 구분 (UI 피드백 방식을 결정하기 위함) | ||
| type ErrorType = "query" | "mutation"; | ||
|
|
||
| /** | ||
| * API 공통 에러 핸들러 | ||
| * @param error 발생한 에러 객체 | ||
| * @param type 요청 종류 ('query': 조회 -> 에러페이지 / 'mutation': 수정 -> 토스트) | ||
| */ | ||
| export const handleApiError = (error: unknown, type: ErrorType) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. type 파라미터를 |
||
| if (isValidCustomError(error)) { | ||
| const { prefix } = error.response.data; | ||
| const message = | ||
| ERROR_MESSAGE[prefix as ApiErrorCode] ?? | ||
| "알 수 없는 에러가 발생했습니다."; | ||
|
|
||
| console.error(message); | ||
| if (type == "mutation") { | ||
| // TODO: toast 메시지 handleApiError 내부처리 or message return 후 컴포넌트에서 처리 | ||
| return message; | ||
| } | ||
| } else if (isAxiosError(error)) { | ||
| console.error("서버/네트워크 오류입니다"); | ||
| // TODO: 에러 페이지 필요 | ||
| } else { | ||
| console.error("알 수 없는 에러 발생"); | ||
| // TODO: 에러 페이지 필요 | ||
| } | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기도 화살표 함수로 변경 부탁드립니다 !! 🙇♀️