diff --git a/packages/react-native/src/App.tsx b/packages/react-native/src/App.tsx index 401f04dd..1d50f86a 100644 --- a/packages/react-native/src/App.tsx +++ b/packages/react-native/src/App.tsx @@ -1,3 +1,4 @@ +import { CanceledError } from 'axios'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import { NavigationContainer } from '@react-navigation/native'; import StackNavigator from '@routes/StackNavigator'; @@ -21,7 +22,12 @@ const queryClient = new QueryClient({ }, mutations: { onError: (err) => { - Sentry.captureException(err); + if (err instanceof CanceledError) { + return; + } + if (!__DEV__) { + Sentry.captureException(err); + } Alert.alert('오류가 발생했어요', '잠시뒤에 시도해보세요.'); }, }, diff --git a/packages/react-native/src/apis/mutations/useAddSchedule.ts b/packages/react-native/src/apis/mutations/useAddSchedule.ts index b983f4fa..9dda48ba 100644 --- a/packages/react-native/src/apis/mutations/useAddSchedule.ts +++ b/packages/react-native/src/apis/mutations/useAddSchedule.ts @@ -17,14 +17,12 @@ export default function useAddSchedule(id: number) { useNavigation>(); const addSchedule = async ({ day, name, description }: AddScheduleProps) => { - const response = await authAxios.post('/api/schedule/location', { + await authAxios.post('/api/schedule/location', { scheduleId: id, day, name, description, }); - - return response.data; }; return useMutation({ diff --git a/packages/react-native/src/apis/mutations/useAddTripPlan.ts b/packages/react-native/src/apis/mutations/useAddTripPlan.ts index 9ac70ecb..cbaa3dcc 100644 --- a/packages/react-native/src/apis/mutations/useAddTripPlan.ts +++ b/packages/react-native/src/apis/mutations/useAddTripPlan.ts @@ -32,15 +32,9 @@ export default function useAddTripPlan() { customForm.appendImage('image', image); - const response = await authAxios.post( - '/api/schedule', - customForm.getForm(), - { - headers: { 'Content-Type': 'multipart/form-data' }, - }, - ); - - return response.data; + await authAxios.post('/api/schedule', customForm.getForm(), { + headers: { 'Content-Type': 'multipart/form-data' }, + }); }; return useMutation({ diff --git a/packages/react-native/src/apis/mutations/useDeleteTripPlan.ts b/packages/react-native/src/apis/mutations/useDeleteTripPlan.ts index e5ac2343..e29a43a7 100644 --- a/packages/react-native/src/apis/mutations/useDeleteTripPlan.ts +++ b/packages/react-native/src/apis/mutations/useDeleteTripPlan.ts @@ -11,9 +11,7 @@ export default function useDeleteTripPlan(options?: UseDeleteTripPlanOptions) { const authAxios = useAuthAxios(); const deleteTripPlan = async (id: number) => { - const response = await authAxios.delete(`/api/schedule/${id}`); - - return response.data; + await authAxios.delete(`/api/schedule/${id}`); }; return useMutation({ diff --git a/packages/react-native/src/apis/mutations/useQuizSubmitMutation.ts b/packages/react-native/src/apis/mutations/useQuizSubmitMutation.ts index ad441106..8fe90692 100644 --- a/packages/react-native/src/apis/mutations/useQuizSubmitMutation.ts +++ b/packages/react-native/src/apis/mutations/useQuizSubmitMutation.ts @@ -1,8 +1,9 @@ import { useRef } from 'react'; -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { City, Region } from '@/constants/CITY'; import useAuthAxios from '../useAuthAxios'; import { ServerResponse } from '@/types/response'; +import QUERY_KEYS from '@/constants/QUERY_KEYS'; interface QuizSubmitRequestParams { id: number; @@ -26,6 +27,7 @@ interface UseQuizSubmitMutationReturns { export default function useQuizSubmitMutation() { const ref = useRef({} as UseQuizSubmitMutationReturns); const authAxios = useAuthAxios(); + const queryClient = useQueryClient(); const submitAnswer = async ({ answer, id }: QuizSubmitRequestParams) => { const result = await authAxios.post>( @@ -36,6 +38,9 @@ export default function useQuizSubmitMutation() { const { mutateAsync, isPending } = useMutation({ mutationFn: submitAnswer, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.MY_BADGES] }); + }, }); ref.current.submitQuiz = mutateAsync; diff --git a/packages/react-native/src/apis/queries/detail/useDetailQuery.ts b/packages/react-native/src/apis/queries/detail/useDetailQuery.ts index 5d6da2b5..5a099011 100644 --- a/packages/react-native/src/apis/queries/detail/useDetailQuery.ts +++ b/packages/react-native/src/apis/queries/detail/useDetailQuery.ts @@ -3,6 +3,7 @@ import QUERY_KEYS from '@/constants/QUERY_KEYS'; import useAuthAxios from '@/apis/useAuthAxios'; import { City, Region } from '@/constants/CITY'; import { ServerResponse } from '@/types/response'; +import removeHTMLTag from '@/utils/removeHTMLTag'; interface DetailResponse { contentId: string; @@ -35,8 +36,8 @@ export default function useDetailQuery({ `/api/spot/${id}?workId=${workId}`, ); const originOverview = result.data.result.overview; - result.data.result.overview = originOverview.replace('
', ''); - result.data.result.overview = originOverview.replace('
', ''); + result.data.result.overview = removeHTMLTag(originOverview); + return result.data.result; }; diff --git a/packages/react-native/src/apis/queries/spot/useSpotDetailQuery.ts b/packages/react-native/src/apis/queries/spot/useSpotDetailQuery.ts index 52f65e46..d3f61da6 100644 --- a/packages/react-native/src/apis/queries/spot/useSpotDetailQuery.ts +++ b/packages/react-native/src/apis/queries/spot/useSpotDetailQuery.ts @@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import QUERY_KEYS from '@/constants/QUERY_KEYS'; import useAuthAxios from '@/apis/useAuthAxios'; import { ServerResponse } from '@/types/response'; +import removeHTMLTag from '@/utils/removeHTMLTag'; interface UseSpotDetailQueryParams { id?: number; @@ -31,6 +32,8 @@ export default function useSpotDetailQuery({ id }: UseSpotDetailQueryParams) { const result = await authAxios.get>( `/api/around/${spotId}`, ); + const originOverview = result.data.result.overview; + result.data.result.overview = removeHTMLTag(originOverview); return result.data.result; }; diff --git a/packages/react-native/src/apis/useAuthAxios.ts b/packages/react-native/src/apis/useAuthAxios.ts index 115e1d1c..352358ef 100644 --- a/packages/react-native/src/apis/useAuthAxios.ts +++ b/packages/react-native/src/apis/useAuthAxios.ts @@ -1,4 +1,4 @@ -import axios, { isAxiosError } from 'axios'; +import axios, { InternalAxiosRequestConfig, isAxiosError } from 'axios'; import { BASE_URL } from '@env'; import { useToken } from '@/hooks/useToken'; import { ServerResponse } from '@/types/response'; @@ -9,6 +9,10 @@ interface RefreshTokenResponse { refreshToken: string; } +interface CustomAxiosConfig extends InternalAxiosRequestConfig { + retry?: boolean; +} + const getNewToken = async (baseURL: string, refresh: string) => { const { data } = await axios.post>( `${baseURL}/api/refresh`, @@ -19,6 +23,9 @@ const getNewToken = async (baseURL: string, refresh: string) => { return data.result; }; +const requestDebounceKeyValue: Record = {}; +const controller = new AbortController(); + const useAuthAxios = () => { const { access, refresh, setAccess, setRefresh } = useToken(); @@ -28,25 +35,51 @@ const useAuthAxios = () => { Authorization: `Bearer ${access}`, }, timeout: 100000, + signal: controller.signal, + }); + + instance.interceptors.request.use((config) => { + const customConfig = config as CustomAxiosConfig; + const retry = customConfig?.retry; + if ( + config.url && + !retry && + (config.method === 'post' || + config.method === 'patch' || + config.method === 'delete') + ) { + const currentTime = new Date(); + currentTime.setMilliseconds(0); + const requestKey = config.url; + const isExistRequest = + requestDebounceKeyValue[requestKey] === currentTime.getTime(); + + if (isExistRequest) { + controller.abort(); + return config; + } + + requestDebounceKeyValue[requestKey] = currentTime.getTime(); + } + return config; }); instance.interceptors.response.use( (res) => res, async (error) => { - const { - config, - response: { status }, - } = error; + const { config, response } = error; + const status = response?.status; if (status === 401) { try { const { accessToken: newAccessToken, refreshToken: newRefreshToken } = await getNewToken(BASE_URL, refresh); config.headers.Authorization = `Bearer ${newAccessToken}`; - const response = await axios(config); + config.retry = true; + const retryResponse = await axios(config); setAccess(newAccessToken); setRefresh(newRefreshToken); - return await Promise.resolve(response); + return await Promise.resolve(retryResponse); } catch (err) { if (isAxiosError(err) && err.response?.status === 401) { await AppStorage.deleteData('token'); diff --git a/packages/react-native/src/components/tripPlan/TripPlanPostForm.tsx b/packages/react-native/src/components/tripPlan/TripPlanPostForm.tsx index 950d3dab..839a6add 100644 --- a/packages/react-native/src/components/tripPlan/TripPlanPostForm.tsx +++ b/packages/react-native/src/components/tripPlan/TripPlanPostForm.tsx @@ -9,6 +9,7 @@ import ImageSelect from '../common/ImageSelect'; import useGallery from '@/hooks/useGallery'; import useAddTripPlan from '@/apis/mutations/useAddTripPlan'; import { getDateString } from '@/utils/date'; +import MutationLoadingModal from '../common/MutationLoadingModal'; export default function TripPlanPostForm() { const { @@ -21,7 +22,7 @@ export default function TripPlanPostForm() { setDate, validate, } = useTripPlanFormState(); - const { mutate } = useAddTripPlan(); + const { mutate, isPending } = useAddTripPlan(); const { getPhoto } = useGallery(); @@ -47,6 +48,7 @@ export default function TripPlanPostForm() { return ( + diff --git a/packages/react-native/src/constants/CITY.ts b/packages/react-native/src/constants/CITY.ts index 8284ea75..698ad21a 100644 --- a/packages/react-native/src/constants/CITY.ts +++ b/packages/react-native/src/constants/CITY.ts @@ -76,6 +76,7 @@ export enum City { SANGJU, MUNGYEONG, GYEONGSAN, + GUNWI, UISEONG, CHEONGSONG, YEONGYANG, @@ -273,6 +274,7 @@ export const REGION = { 상주: City.SANGJU, 문경: City.MUNGYEONG, 경산: City.GYEONGSAN, + 군위: City.GUNWI, 의성: City.UISEONG, 청송: City.CHEONGSONG, 영양: City.YEONGYANG, diff --git a/packages/react-native/src/pages/TripPlanner/AddSchedule.tsx b/packages/react-native/src/pages/TripPlanner/AddSchedule.tsx index 22a7265a..68ae98b8 100644 --- a/packages/react-native/src/pages/TripPlanner/AddSchedule.tsx +++ b/packages/react-native/src/pages/TripPlanner/AddSchedule.tsx @@ -96,7 +96,7 @@ export default function AddSchedule() { {spotList.attraction.length > 0 && ( <> - 나의 SPOT! + 담은 관광지 diff --git a/packages/react-native/src/utils/removeHTMLTag.ts b/packages/react-native/src/utils/removeHTMLTag.ts new file mode 100644 index 00000000..b8212295 --- /dev/null +++ b/packages/react-native/src/utils/removeHTMLTag.ts @@ -0,0 +1,3 @@ +export default function removeHTMLTag(content: string) { + return content.replace('
', '').replace('
', '').replace('
', ''); +}