diff --git a/frontend/src/apis/fetcher.ts b/frontend/src/apis/fetcher.ts index fea16a575..5c0830d48 100644 --- a/frontend/src/apis/fetcher.ts +++ b/frontend/src/apis/fetcher.ts @@ -1,4 +1,4 @@ -import { CustomError, NetworkError } from '@/utils/error'; +import { CustomError, NetworkError, UnhandledError } from '@/utils/error'; interface RequestProps { url: string; @@ -26,11 +26,15 @@ const fetcher = { return response; } catch (error) { + if (!navigator.onLine) { + throw new NetworkError(); + } + if (error instanceof CustomError) { throw error; } - throw new NetworkError(); + throw new UnhandledError(); } }, diff --git a/frontend/src/components/InviteModal/InviteModal.tsx b/frontend/src/components/InviteModal/InviteModal.tsx index 9e201b01b..9c277dbfc 100644 --- a/frontend/src/components/InviteModal/InviteModal.tsx +++ b/frontend/src/components/InviteModal/InviteModal.tsx @@ -31,11 +31,11 @@ const InviteModal = ({ isOpen, onClose, returnFocusRef }: InviteModalProps) => { const inviteUrl = INVITE_URL(roomUuid); const { copyToClipboard } = useClipBoard(); - const { show } = useToast(); + const { showToast } = useToast(); const handleCopy = () => { copyToClipboard(inviteUrl); - show('링크가 복사되었습니다!'); + showToast('링크가 복사되었습니다!'); }; return ( diff --git a/frontend/src/components/common/ErrorBoundary/AsyncErrorBoundary.tsx b/frontend/src/components/common/ErrorBoundary/AsyncErrorBoundary.tsx index 094fcebf8..c8d8231dc 100644 --- a/frontend/src/components/common/ErrorBoundary/AsyncErrorBoundary.tsx +++ b/frontend/src/components/common/ErrorBoundary/AsyncErrorBoundary.tsx @@ -6,7 +6,7 @@ import DeferredComponent from '../DeferredComponent/DeferredComponent'; import AsyncErrorFallback from '../ErrorFallback/AsyncErrorFallback/AsyncErrorFallback'; import SpinnerErrorFallback from '../ErrorFallback/SpinnerErrorFallback/SpinnerErrorFallback'; -import { CustomError } from '@/utils/error'; +import { CustomError, UnhandledError } from '@/utils/error'; interface AsyncErrorBoundaryProps { pendingFallback?: React.ReactNode; @@ -25,7 +25,7 @@ const AsyncErrorBoundary = ({ )} onError={(error) => { - if (error instanceof CustomError) { + if (error instanceof CustomError || error instanceof UnhandledError) { withScope((scope) => { scope.setLevel('warning'); scope.setTag('api', 'internalServerError'); diff --git a/frontend/src/components/common/ErrorFallback/AsyncErrorFallback/AsyncErrorFallback.tsx b/frontend/src/components/common/ErrorFallback/AsyncErrorFallback/AsyncErrorFallback.tsx index e8f95882b..b444b981e 100644 --- a/frontend/src/components/common/ErrorFallback/AsyncErrorFallback/AsyncErrorFallback.tsx +++ b/frontend/src/components/common/ErrorFallback/AsyncErrorFallback/AsyncErrorFallback.tsx @@ -7,7 +7,7 @@ import { } from '../ErrorFallback.styled'; import ErrorDdangkong from '@/assets/images/errorDdangkong.webp'; -import { CustomError } from '@/utils/error'; +import { CustomError, UnhandledError } from '@/utils/error'; interface AsyncErrorFallback { error: unknown; @@ -22,7 +22,9 @@ const AsyncErrorFallback = ({ error, resetError }: AsyncErrorFallback) => { return (
에러나서 슬픈 땅콩 -

{error instanceof CustomError && error.message}

+

+ {(error instanceof CustomError || error instanceof UnhandledError) && error.message} +

diff --git a/frontend/src/providers/ModalProvider/ModalProvider.tsx b/frontend/src/providers/ModalProvider/ModalProvider.tsx index b7d4b1825..48d147fc6 100644 --- a/frontend/src/providers/ModalProvider/ModalProvider.tsx +++ b/frontend/src/providers/ModalProvider/ModalProvider.tsx @@ -18,7 +18,7 @@ interface Modal extends ModalProps { } interface ModalDispatchContextProps { - show: (Component: React.FC | null, props?: ModalProps) => void; + showModal: (Component: React.FC | null, props?: ModalProps) => void; close: () => void; } @@ -33,7 +33,7 @@ const ModalProvider = ({ children }: PropsWithChildren) => { onConfirm: () => {}, }); - const show = (Component: React.FC | null, props?: ModalProps) => { + const showModal = (Component: React.FC | null, props?: ModalProps) => { setModal({ Component, title: props?.title, @@ -52,7 +52,7 @@ const ModalProvider = ({ children }: PropsWithChildren) => { })); }; - const dispatch = useMemo(() => ({ show, close }), []); + const dispatch = useMemo(() => ({ showModal, close }), []); return ( diff --git a/frontend/src/providers/QueryClientDefaultOptionProvider/QueryClientDefaultOptionProvider.tsx b/frontend/src/providers/QueryClientDefaultOptionProvider/QueryClientDefaultOptionProvider.tsx index 8e60785f8..d95c2511b 100644 --- a/frontend/src/providers/QueryClientDefaultOptionProvider/QueryClientDefaultOptionProvider.tsx +++ b/frontend/src/providers/QueryClientDefaultOptionProvider/QueryClientDefaultOptionProvider.tsx @@ -1,10 +1,12 @@ import { useQueryClient } from '@tanstack/react-query'; import { PropsWithChildren } from 'react'; +import { NETWORK_ERROR_STATUS, SERVER_ERROR_STATUS } from '@/constants/errorStatus'; import useDefaultMutationErrorHandler from '@/hooks/useDefaultMutationErrorHandler'; import { CustomError } from '@/utils/error'; -const isServerError = (status: number) => status >= 500 && status !== 555; +const isServerError = (status: number) => + status >= SERVER_ERROR_STATUS && status !== NETWORK_ERROR_STATUS; // QueryClient는 모든 Provider에 공유되면서 공통 에러 핸들링 로직에 Toast와 Modal을 넣기 위해 setDefaultOptions 사용 // 테스트 환경에서 retry 값이 있을 경우 에러 폴백 테스트가 돌지 않아 분기 처리 diff --git a/frontend/src/providers/ToastProvider/ToastProvider.tsx b/frontend/src/providers/ToastProvider/ToastProvider.tsx index 04e65de59..c9a08965d 100644 --- a/frontend/src/providers/ToastProvider/ToastProvider.tsx +++ b/frontend/src/providers/ToastProvider/ToastProvider.tsx @@ -4,7 +4,7 @@ import { createPortal } from 'react-dom'; import { toastLayout } from './ToastProvider.styled'; interface ToastContext { - show: (message: string) => void; + showToast: (message: string) => void; } export const ToastContext = createContext(null); @@ -13,7 +13,7 @@ const ToastProvider = ({ children }: PropsWithChildren) => { const [toastMessage, setToastMessage] = useState(''); const timerRef = useRef(null); - const show = useCallback((message: string) => { + const showToast = useCallback((message: string) => { if (timerRef.current) { clearTimeout(timerRef.current); } @@ -34,7 +34,7 @@ const ToastProvider = ({ children }: PropsWithChildren) => { }, []); return ( - + {children} {toastMessage && createPortal( diff --git a/frontend/src/utils/error.ts b/frontend/src/utils/error.ts index 87e649721..379882274 100644 --- a/frontend/src/utils/error.ts +++ b/frontend/src/utils/error.ts @@ -1,3 +1,4 @@ +import { NETWORK_ERROR_STATUS, UNHANDLED_ERROR_STATUS } from '@/constants/errorStatus'; import { ERROR_MESSAGE } from '@/constants/message'; import { ErrorCode } from '@/types/error'; @@ -20,6 +21,11 @@ export class CustomError extends Error { } export class NetworkError extends Error { - status = 555; + status = NETWORK_ERROR_STATUS; message = '네트워크가 불안정해요. 다시 시도해주세요!'; } + +export class UnhandledError extends Error { + status = UNHANDLED_ERROR_STATUS; + message = '예기치 못한 에러가 발생했어요. 관리자에게 문의 바랍니다.'; +}