-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#267 feat: React-Query, react-error-boundary 도입 및 초기 설정
- Loading branch information
1 parent
e7b5101
commit a145285
Showing
11 changed files
with
367 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import axios from 'axios'; | ||
import { | ||
useQuery, | ||
useMutation, | ||
UseQueryResult, | ||
UseMutationResult, | ||
} from '@tanstack/react-query'; | ||
import type { | ||
NewsRequest, | ||
NewsResponse, | ||
ApiResponse, | ||
PaginationParams, | ||
PaginatedResponse, | ||
} from '../types'; | ||
|
||
const API_URL = process.env.REACT_APP_API_URL; | ||
|
||
export const newsKeys = { | ||
all: ['news'] as const, | ||
lists: () => [...newsKeys.all, 'list'] as const, | ||
list: (params: PaginationParams) => [...newsKeys.lists(), params] as const, | ||
details: () => [...newsKeys.all, 'detail'] as const, | ||
detail: (id: number) => [...newsKeys.details(), id] as const, | ||
}; | ||
|
||
// API 함수들 | ||
export const getNews = async (id: number): Promise<NewsResponse> => { | ||
const { data } = await axios.get<ApiResponse<NewsResponse>>( | ||
`${API_URL}/api/news/${id}`, | ||
); | ||
return data.data; | ||
}; | ||
|
||
export const getNewsList = async ({ | ||
page, | ||
size, | ||
}: PaginationParams): Promise<PaginatedResponse<NewsResponse>> => { | ||
const { data } = await axios.get< | ||
ApiResponse<PaginatedResponse<NewsResponse>> | ||
>(`${API_URL}/api/news`, { | ||
params: { page, size }, | ||
}); | ||
return data.data; | ||
}; | ||
|
||
export const createNews = async (newsData: NewsRequest): Promise<number> => { | ||
const formData = new FormData(); | ||
formData.append( | ||
'newsReqDto', | ||
new Blob([JSON.stringify(newsData)], { type: 'application/json' }), | ||
); | ||
|
||
if (newsData.image) { | ||
formData.append('news_image', newsData.image); | ||
} | ||
|
||
const { data } = await axios.post<ApiResponse<number>>( | ||
`${API_URL}/api/news`, | ||
formData, | ||
{ | ||
headers: { 'Content-Type': 'multipart/form-data' }, | ||
}, | ||
); | ||
|
||
return data.data; | ||
}; | ||
|
||
export const updateNews = async ({ | ||
id, | ||
...newsData | ||
}: NewsRequest & { id: number }): Promise<void> => { | ||
const formData = new FormData(); | ||
formData.append( | ||
'newsReqDto', | ||
new Blob([JSON.stringify(newsData)], { type: 'application/json' }), | ||
); | ||
|
||
if (newsData.image) { | ||
formData.append('news_image', newsData.image); | ||
} | ||
|
||
await axios.post(`${API_URL}/api/news/${id}`, formData, { | ||
headers: { 'Content-Type': 'multipart/form-data' }, | ||
}); | ||
}; | ||
|
||
export const deleteNews = async (id: number): Promise<void> => { | ||
await axios.delete(`${API_URL}/api/news/${id}`); | ||
}; | ||
|
||
// React Query Hooks | ||
export const useGetNews = (id: number): UseQueryResult<NewsResponse> => { | ||
return useQuery({ | ||
queryKey: newsKeys.detail(id), | ||
queryFn: () => getNews(id), | ||
}); | ||
}; | ||
|
||
export const useGetNewsList = ( | ||
params: PaginationParams, | ||
): UseQueryResult<PaginatedResponse<NewsResponse>> => { | ||
return useQuery({ | ||
queryKey: newsKeys.list(params), | ||
queryFn: () => getNewsList(params), | ||
}); | ||
}; | ||
|
||
export const useCreateNews = (): UseMutationResult< | ||
number, | ||
Error, | ||
NewsRequest | ||
> => { | ||
return useMutation({ | ||
mutationFn: createNews, | ||
}); | ||
}; | ||
|
||
export const useUpdateNews = (): UseMutationResult< | ||
void, | ||
Error, | ||
NewsRequest & { id: number } | ||
> => { | ||
return useMutation({ | ||
mutationFn: updateNews, | ||
}); | ||
}; | ||
|
||
export const useDeleteNews = (): UseMutationResult<void, Error, number> => { | ||
return useMutation({ | ||
mutationFn: deleteNews, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// src/lib/axios/index.ts | ||
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; | ||
|
||
// API 에러 타입 정의 | ||
interface ApiErrorResponse { | ||
message: string; | ||
status: number; | ||
data?: any; | ||
} | ||
|
||
// 인스턴스 생성 함수 | ||
const createAxiosInstance = (): AxiosInstance => { | ||
const instance = axios.create({ | ||
baseURL: process.env.REACT_APP_API_URL, | ||
timeout: 10000, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
|
||
// 요청 인터셉터 | ||
instance.interceptors.request.use( | ||
(config) => { | ||
// 토큰이 필요한 경우 | ||
const token = localStorage.getItem('token'); | ||
if (token) { | ||
config.headers.Authorization = `Bearer ${token}`; | ||
} | ||
return config; | ||
}, | ||
(error: AxiosError) => { | ||
return Promise.reject(error); | ||
}, | ||
); | ||
|
||
// 응답 인터셉터 | ||
instance.interceptors.response.use( | ||
(response: AxiosResponse) => { | ||
return response; | ||
}, | ||
(error: AxiosError<ApiErrorResponse>) => { | ||
if (error.response) { | ||
// 서버 응답이 있는 경우 | ||
const { status, data } = error.response; | ||
|
||
switch (status) { | ||
case 401: | ||
// 인증 에러 처리 | ||
localStorage.removeItem('token'); | ||
// 로그인 페이지로 리다이렉트 등의 처리 | ||
break; | ||
case 403: | ||
// 권한 에러 처리 | ||
break; | ||
case 404: | ||
// Not Found 처리 | ||
break; | ||
case 500: | ||
// 서버 에러 처리 | ||
break; | ||
} | ||
|
||
return Promise.reject({ | ||
message: data?.message || '에러가 발생했습니다.', | ||
status: status, | ||
data: data, | ||
}); | ||
} | ||
|
||
if (error.request) { | ||
// 요청은 보냈지만 응답을 받지 못한 경우 | ||
return Promise.reject({ | ||
message: '서버에 연결할 수 없습니다.', | ||
status: 503, | ||
}); | ||
} | ||
|
||
// 요청 자체를 보내지 못한 경우 | ||
return Promise.reject({ | ||
message: '요청을 보낼 수 없습니다.', | ||
status: 0, | ||
}); | ||
}, | ||
); | ||
|
||
return instance; | ||
}; | ||
|
||
export const axiosInstance = createAxiosInstance(); | ||
|
||
// API 에러 핸들러 | ||
export const handleApiError = (error: unknown): ApiErrorResponse => { | ||
if (axios.isAxiosError(error)) { | ||
return { | ||
message: | ||
error.response?.data?.message || '알 수 없는 에러가 발생했습니다.', | ||
status: error.response?.status || 500, | ||
data: error.response?.data, | ||
}; | ||
} | ||
return { | ||
message: '알 수 없는 에러가 발생했습니다.', | ||
status: 500, | ||
}; | ||
}; | ||
|
||
// axios 타입 가드 | ||
export const isAxiosError = axios.isAxiosError; | ||
|
||
export default axiosInstance; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { QueryClientProvider } from '@tanstack/react-query'; | ||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; | ||
import { queryClient } from './queryClient'; | ||
|
||
interface ProvidersProps { | ||
children: React.ReactNode; | ||
} | ||
|
||
export const Providers = ({ children }: ProvidersProps) => { | ||
return ( | ||
<QueryClientProvider client={queryClient}> | ||
{children} | ||
<ReactQueryDevtools initialIsOpen={false} /> | ||
</QueryClientProvider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { QueryClient } from '@tanstack/react-query'; | ||
|
||
export const queryClient = new QueryClient({ | ||
defaultOptions: { | ||
queries: { | ||
staleTime: 5 * 60 * 1000, // 5분 | ||
gcTime: 30 * 60 * 1000, // 이전의 cacheTime | ||
retry: 1, | ||
refetchOnWindowFocus: false, | ||
}, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export interface ApiResponse<T> { | ||
data: T; | ||
message?: string; | ||
status: number; | ||
} | ||
|
||
export interface PaginationParams { | ||
page: number; | ||
size: number; | ||
} | ||
|
||
export interface PaginatedResponse<T> { | ||
content: T[]; | ||
totalPages: number; | ||
totalElements: number; | ||
size: number; | ||
number: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './common'; | ||
export * from './news'; |
Oops, something went wrong.