diff --git a/package.json b/package.json index ab27a65e..55b5456a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@lukemorales/query-key-factory": "^1.3.4", "@tanstack/react-query": "^5.81.5", "@vanilla-extract/recipes": "^0.5.7", + "axios": "^1.11.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", "react": "^19.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d794ce5e..d487c4ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@vanilla-extract/recipes': specifier: ^0.5.7 version: 0.5.7(@vanilla-extract/css@1.17.4) + axios: + specifier: ^1.11.0 + version: 1.11.0 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -1191,6 +1194,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1199,6 +1205,9 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1309,6 +1318,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1426,6 +1439,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1729,6 +1746,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -1737,6 +1763,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2112,6 +2142,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -2357,6 +2395,9 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4057,12 +4098,22 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.10.3: {} + axios@1.11.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} better-opn@3.0.2: @@ -4175,6 +4226,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -4264,6 +4319,8 @@ snapshots: defu@6.1.4: {} + delayed-stream@1.0.0: {} + dequal@2.0.3: {} destr@2.0.5: {} @@ -4654,6 +4711,8 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.11: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -4663,6 +4722,14 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fsevents@2.3.2: optional: true @@ -5018,6 +5085,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + min-indent@1.0.1: {} minimatch@3.1.2: @@ -5270,6 +5343,8 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} diff --git a/src/App.tsx b/src/App.tsx index e2978361..cde283ba 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,11 @@ import { RouterProvider } from 'react-router-dom'; import router from '@router/Router'; import '@styles/global.css.ts'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { queryClient } from '@api/queryClient'; const App = () => { - const queryClient = new QueryClient(); return ( diff --git a/src/api/.gitkeep b/src/api/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/api/axiosInstance.ts b/src/api/axiosInstance.ts new file mode 100644 index 00000000..87eb05fe --- /dev/null +++ b/src/api/axiosInstance.ts @@ -0,0 +1,39 @@ +import { ROUTES } from '@router/constant/Routes'; +import axios from 'axios'; + +const axiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL, + timeout: 10000, + headers: { + Accept: 'application/json', + }, +}); + +axiosInstance.interceptors.request.use( + config => { + const token = localStorage.getItem('accessToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + error => { + return Promise.reject(error); + } +); + +axiosInstance.interceptors.response.use( + response => { + return response; + }, + error => { + //후에 이 조건문에 리이슈 로직 추가 + if (error.response?.status === 401) { + localStorage.removeItem('accessToken'); + window.location.href = ROUTES.HOME; + } + return Promise.reject(error); + } +); + +export default axiosInstance; diff --git a/src/api/queryClient.ts b/src/api/queryClient.ts new file mode 100644 index 00000000..d3e919f4 --- /dev/null +++ b/src/api/queryClient.ts @@ -0,0 +1,16 @@ +import { QueryCache, QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: error => { + if (import.meta.env.DEV) console.error('[React Query Error]', error); + }, + }), + defaultOptions: { + queries: { + retry: 1, + staleTime: 1000 * 60 * 2, + gcTime: 1000 * 60 * 5, + }, + }, +}); diff --git a/src/api/request.ts b/src/api/request.ts new file mode 100644 index 00000000..df8d5cc7 --- /dev/null +++ b/src/api/request.ts @@ -0,0 +1,66 @@ +import { isAxiosError, type AxiosRequestConfig } from 'axios'; +import axiosInstance from '@/api/axiosInstance'; +import type { BaseResponse } from '@/api/types'; + +export const HTTPMethod = { + GET: 'GET', + POST: 'POST', + PUT: 'PUT', + DELETE: 'DELETE', + PATCH: 'PATCH', +} as const; + +export type HTTPMethodType = (typeof HTTPMethod)[keyof typeof HTTPMethod]; + +export interface RequestConfig { + method: HTTPMethodType; + url: string; + query?: Record; + body?: unknown | FormData; + headers?: Record; + withCredentials?: boolean; +} + +export const request = async (config: RequestConfig): Promise => { + const { method, url, query, body, headers, withCredentials } = config; + + const requestConfig: AxiosRequestConfig = { + method, + url, + params: query, + data: body, + withCredentials, + }; + + if (headers) { + requestConfig.headers = headers; + } else if (body && !(body instanceof FormData)) { + requestConfig.headers = { 'Content-Type': 'application/json' }; + } + + try { + const response = await axiosInstance.request>(requestConfig); + return response.data.data; + } catch (error: unknown) { + if (!isAxiosError(error)) { + console.error(`[실패] ${url} : 네트워크 오류`); + throw error; + } + + if (error.response) { + const { status, data } = error.response; + const message = data?.message; + + const displayMessage = status + ' ' + message; + + if (import.meta.env.DEV) { + console.error(`[실패] ${url} : ${displayMessage}`); + } + } else { + if (import.meta.env.DEV) { + console.error(`[실패] ${url} : 서버에 연결할 수 없습니다.`); + } + } + throw error; + } +}; diff --git a/src/api/types.ts b/src/api/types.ts new file mode 100644 index 00000000..c3ca496b --- /dev/null +++ b/src/api/types.ts @@ -0,0 +1,6 @@ +export interface BaseResponse { + success?: boolean; + code?: number; + message?: string; + data: T; +}