From d351175a2cdf4ae8ea5bedc0627bb0ebef8855ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Mon, 19 Aug 2024 22:35:18 +0900 Subject: [PATCH 01/36] =?UTF-8?q?chore:=20=EB=94=94=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=B2=84=EC=A0=84=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package-lock.json | 8 ++++---- client/package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index d01bee964..1629ab625 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,7 +12,7 @@ "@emotion/react": "^11.11.4", "@sentry/react": "^8.25.0", "@tanstack/react-query": "^5.51.23", - "haengdong-design": "^0.1.74", + "haengdong-design": "^0.1.75", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", @@ -10137,9 +10137,9 @@ } }, "node_modules/haengdong-design": { - "version": "0.1.74", - "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.74.tgz", - "integrity": "sha512-K75LDIR4wqR+Z8YDTMsYXm9sWQ60Qw4DLPSOSnsb5mzncX1u3/z+zLOb1gs/zS8YZznUwzu6HzavWh6Sl8guNQ==", + "version": "0.1.75", + "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.75.tgz", + "integrity": "sha512-N8d34JG2lnzq1pubcH7mSO0WL1cXHgH9YmVy5EZ+kETYxLIIE18H0oSz2HJdGTz4RfE2bdsnZBCf04LueEMimw==", "dependencies": { "@emotion/react": "^11.11.4", "@storybook/addon-webpack5-compiler-swc": "^1.0.5", diff --git a/client/package.json b/client/package.json index 5de424ec7..e6ba3d7ff 100644 --- a/client/package.json +++ b/client/package.json @@ -68,7 +68,7 @@ "@emotion/react": "^11.11.4", "@sentry/react": "^8.25.0", "@tanstack/react-query": "^5.51.23", - "haengdong-design": "^0.1.74", + "haengdong-design": "^0.1.75", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", @@ -80,4 +80,4 @@ "npm": ">=10.7.0", "node": ">=20.15.1" } -} \ No newline at end of file +} From ca353861d27a43dee0e254cad2eb0a1f1b328fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 20 Aug 2024 03:32:19 +0900 Subject: [PATCH 02/36] =?UTF-8?q?rename:=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/hooks/{useSetEventName.ts => useSetEventNamePage.ts} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename client/src/hooks/{useSetEventName.ts => useSetEventNamePage.ts} (91%) diff --git a/client/src/hooks/useSetEventName.ts b/client/src/hooks/useSetEventNamePage.ts similarity index 91% rename from client/src/hooks/useSetEventName.ts rename to client/src/hooks/useSetEventNamePage.ts index b36820735..f5b5bce12 100644 --- a/client/src/hooks/useSetEventName.ts +++ b/client/src/hooks/useSetEventNamePage.ts @@ -2,7 +2,7 @@ import {useState} from 'react'; import validateEventName from '@utils/validate/validateEventName'; -const useSetEventName = () => { +const useSetEventNamePage = () => { const [eventName, setEventName] = useState(''); const [errorMessage, setErrorMessage] = useState(null); const [canSubmit, setCanSubmit] = useState(false); @@ -29,4 +29,4 @@ const useSetEventName = () => { }; }; -export default useSetEventName; +export default useSetEventNamePage; From eab610b069f1cdc0e5aa947e8ae5ae7671fd0d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 20 Aug 2024 03:32:39 +0900 Subject: [PATCH 03/36] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20Error=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/utils/captureError.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/client/src/utils/captureError.ts b/client/src/utils/captureError.ts index ce70da972..4d37f4ef0 100644 --- a/client/src/utils/captureError.ts +++ b/client/src/utils/captureError.ts @@ -1,13 +1,9 @@ -import {NavigateFunction} from 'react-router-dom'; - import FetchError from '@errors/FetchError'; import {ErrorInfo} from '@hooks/useError/ErrorProvider'; -import {ROUTER_URLS} from '@constants/routerUrls'; - import sendLogToSentry from './sendLogToSentry'; -export const captureError = async (error: Error, navigate: NavigateFunction, eventId: string) => { +export const captureError = async (error: Error) => { // prod 환경에서만 Sentry capture 실행 if (process.env.NODE_ENV !== 'production') return; @@ -21,28 +17,28 @@ export const captureError = async (error: Error, navigate: NavigateFunction, eve case 'FORBIDDEN': sendLogToSentry({error, errorInfo}); - navigate(`${ROUTER_URLS.event}/${eventId}/login`); + break; case 'TOKEN_INVALID': sendLogToSentry({error, errorInfo}); - navigate(`${ROUTER_URLS.event}/${eventId}/login`); + break; case 'TOKEN_EXPIRED': sendLogToSentry({error, errorInfo}); - navigate(`${ROUTER_URLS.event}/${eventId}/login`); + break; case 'TOKEN_NOT_FOUND': sendLogToSentry({error, errorInfo}); - navigate(`${ROUTER_URLS.event}/${eventId}/login`); + break; // 비밀 번호를 까먹는 사람이 얼마나 많은 지 추측하기 위함 case 'PASSWORD_INVALID': sendLogToSentry({error, errorInfo, level: 'debug'}); - navigate(`${ROUTER_URLS.event}/${eventId}/login`); + break; // 1천만원 이상 입력하는 사람이 얼마나 많은 지 추측하기 위함 From a439e5f1a2411486fde3039445115d668732c45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 20 Aug 2024 03:32:49 +0900 Subject: [PATCH 04/36] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=83=81=ED=83=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/store/appErrorStore.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 client/src/store/appErrorStore.ts diff --git a/client/src/store/appErrorStore.ts b/client/src/store/appErrorStore.ts new file mode 100644 index 000000000..ca350d4cb --- /dev/null +++ b/client/src/store/appErrorStore.ts @@ -0,0 +1,14 @@ +import {create} from 'zustand'; + +type State = { + appError: Error | null; +}; + +type Action = { + updateAppError: (appError: State['appError']) => void; +}; + +export const useAppErrorStore = create(set => ({ + appError: null, + updateAppError: appError => set(() => ({appError})), +})); From 224941ebd09be3e325413d36a25e013922adb569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 20 Aug 2024 03:33:15 +0900 Subject: [PATCH 05/36] =?UTF-8?q?fix:=20=EB=B0=94=EB=80=90=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B4=EB=A6=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/CreateEventPage/SetEventNamePage.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/src/pages/CreateEventPage/SetEventNamePage.tsx b/client/src/pages/CreateEventPage/SetEventNamePage.tsx index fc6830467..9eb29b971 100644 --- a/client/src/pages/CreateEventPage/SetEventNamePage.tsx +++ b/client/src/pages/CreateEventPage/SetEventNamePage.tsx @@ -2,13 +2,12 @@ import {useNavigate} from 'react-router-dom'; import {FixedButton, MainLayout, LabelInput, Title, TopNav, Back} from 'haengdong-design'; import {css} from '@emotion/react'; -import useSetEventName from '@hooks/useSetEventName'; - import {ROUTER_URLS} from '@constants/routerUrls'; +import useSetEventNamePage from '@hooks/useSetEventNamePage'; const SetEventNamePage = () => { const navigate = useNavigate(); - const {eventName, errorMessage, canSubmit, handleEventNameChange} = useSetEventName(); + const {eventName, errorMessage, canSubmit, handleEventNameChange} = useSetEventNamePage(); const submitEventName = (event: React.FormEvent) => { event.preventDefault(); @@ -25,7 +24,7 @@ const SetEventNamePage = () => {
Date: Tue, 20 Aug 2024 03:33:28 +0900 Subject: [PATCH 06/36] =?UTF-8?q?fix:=20ToastProvider=20=EB=82=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/hooks/useToast/ToastProvider.tsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/client/src/hooks/useToast/ToastProvider.tsx b/client/src/hooks/useToast/ToastProvider.tsx index eed006967..1980a9bde 100644 --- a/client/src/hooks/useToast/ToastProvider.tsx +++ b/client/src/hooks/useToast/ToastProvider.tsx @@ -23,7 +23,6 @@ type ShowToast = ToastProps & { export const ToastProvider = ({children}: React.PropsWithChildren) => { const [currentToast, setCurrentToast] = useState(null); - const {errorInfo, clearError, clientErrorMessage} = useError(); const showToast = ({showingTime = DEFAULT_TIME, isAlwaysOn = false, ...toastProps}: ShowToast) => { setCurrentToast({showingTime, isAlwaysOn, ...toastProps}); @@ -33,22 +32,6 @@ export const ToastProvider = ({children}: React.PropsWithChildren) => { setCurrentToast(null); }; - useEffect(() => { - if (errorInfo !== null) { - showToast({ - message: clientErrorMessage || SERVER_ERROR_MESSAGES.UNHANDLED, - showingTime: DEFAULT_TIME, // TODO: (@weadie) 나중에 토스트 프로바이더를 제거한 토스트를 만들 것이기 때문에 많이 리펙터링 안함 - isAlwaysOn: false, - position: 'bottom', - bottom: '6.25rem', - // TODO: (@soha&weadie) zIndex의 값 추후에 꼭!!! 수정 - style: {zIndex: '1000'}, - }); - - clearError(DEFAULT_TIME); - } - }, [errorInfo, clientErrorMessage]); - useEffect(() => { if (!currentToast) return; From 59597a792da9b8fd3cf72528bd6026fbb4685b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 20 Aug 2024 03:37:44 +0900 Subject: [PATCH 07/36] =?UTF-8?q?refactor:=20AppErrorBoundary=20=EB=B0=8F?= =?UTF-8?q?=20QueryClientBoundary=EB=A5=BC=20=ED=86=B5=ED=95=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 26 +++++------ client/src/AppErrorBoundary.tsx | 71 ++++++++++++++++++++++++++++++ client/src/QueryClientBoundary.tsx | 21 +++++++++ client/src/apis/fetcher.ts | 3 +- client/src/utils/captureError.ts | 2 +- 5 files changed, 104 insertions(+), 19 deletions(-) create mode 100644 client/src/AppErrorBoundary.tsx create mode 100644 client/src/QueryClientBoundary.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 3a8b7f0f2..8e03f4c05 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,30 +1,24 @@ import {Outlet} from 'react-router-dom'; import {HDesignProvider} from 'haengdong-design'; import {Global} from '@emotion/react'; -import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {ToastProvider} from '@hooks/useToast/ToastProvider'; -import {ErrorProvider} from '@hooks/useError/ErrorProvider'; import {GlobalStyle} from './GlobalStyle'; -import UnhandledErrorBoundary from './UnhandledErrorBoundary'; - -const queryClient = new QueryClient(); +import AppErrorBoundary from './AppErrorBoundary'; +import QueryClientBoundary from './QueryClientBoundary'; const App: React.FC = () => { return ( - - - - - {/* */} - - - - - - + + + + + + + + ); }; diff --git a/client/src/AppErrorBoundary.tsx b/client/src/AppErrorBoundary.tsx new file mode 100644 index 000000000..a85a4bc4d --- /dev/null +++ b/client/src/AppErrorBoundary.tsx @@ -0,0 +1,71 @@ +import {ErrorBoundary} from 'react-error-boundary'; + +import ErrorPage from '@pages/ErrorPage/ErrorPage'; +import {captureError} from '@utils/captureError'; +import FetchError from '@errors/FetchError'; +import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage'; +import {useToast} from '@hooks/useToast/useToast'; +import {useAppErrorStore} from '@store/appErrorStore'; +import {useEffect, useState} from 'react'; + +interface ErrorFallbackProps { + error: Error; +} + +export type ErrorInfo = { + errorCode: string; + message: string; +}; + +const convertAppErrorToErrorInfo = (appError: Error) => { + if (appError instanceof Error) { + const errorInfo = + appError instanceof FetchError ? appError.errorInfo : {errorCode: appError.name, message: appError.message}; + + return errorInfo; + } else { + const errorInfo = {errorCode: UNKNOWN_ERROR, message: JSON.stringify(appError)}; + + return errorInfo; + } +}; + +const isUnhandledError = (errorInfo: ErrorInfo) => { + if (errorInfo.errorCode === 'INTERNAL_SERVER_ERROR') return true; + + return SERVER_ERROR_MESSAGES[errorInfo.errorCode] === undefined; +}; + +const AppErrorBoundary = ({children}: React.PropsWithChildren) => { + const {appError} = useAppErrorStore(); + const {showToast} = useToast(); + + useEffect(() => { + if (appError) { + captureError(appError instanceof FetchError ? appError : new Error(UNKNOWN_ERROR)); + const errorInfo = convertAppErrorToErrorInfo(appError); + console.log(errorInfo); + if (!isUnhandledError(errorInfo)) { + showToast({ + showingTime: 3000, + message: SERVER_ERROR_MESSAGES[errorInfo.errorCode], + type: 'error', + position: 'bottom', + bottom: '8rem', + }); + } else { + showToast({ + showingTime: 3000, + message: SERVER_ERROR_MESSAGES.UNHANDLED, + type: 'error', + position: 'bottom', + bottom: '8rem', + }); + } + } + }, [appError]); + + return }>{children}; +}; + +export default AppErrorBoundary; diff --git a/client/src/QueryClientBoundary.tsx b/client/src/QueryClientBoundary.tsx new file mode 100644 index 000000000..41de70a2e --- /dev/null +++ b/client/src/QueryClientBoundary.tsx @@ -0,0 +1,21 @@ +import {useAppErrorStore} from '@store/appErrorStore'; +import {MutationCache, QueryCache, QueryClient, QueryClientProvider} from '@tanstack/react-query'; + +const QueryClientBoundary = ({children}: React.PropsWithChildren) => { + const {updateAppError} = useAppErrorStore(); + const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error: Error) => { + updateAppError(error); + }, + }), + mutationCache: new MutationCache({ + onError: (error: Error) => { + updateAppError(error); + }, + }), + }); + return {children}; +}; + +export default QueryClientBoundary; diff --git a/client/src/apis/fetcher.ts b/client/src/apis/fetcher.ts index e87e3d395..2bfb7ada4 100644 --- a/client/src/apis/fetcher.ts +++ b/client/src/apis/fetcher.ts @@ -1,10 +1,9 @@ -import {ErrorInfo} from '@hooks/useError/ErrorProvider'; - import objectToQueryString from '@utils/objectToQueryString'; import {UNKNOWN_ERROR} from '@constants/errorMessage'; import FetchError from '../errors/FetchError'; +import {ErrorInfo} from 'AppErrorBoundary'; export type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; diff --git a/client/src/utils/captureError.ts b/client/src/utils/captureError.ts index 4d37f4ef0..051c9ce0e 100644 --- a/client/src/utils/captureError.ts +++ b/client/src/utils/captureError.ts @@ -1,7 +1,7 @@ import FetchError from '@errors/FetchError'; -import {ErrorInfo} from '@hooks/useError/ErrorProvider'; import sendLogToSentry from './sendLogToSentry'; +import {ErrorInfo} from 'AppErrorBoundary'; export const captureError = async (error: Error) => { // prod 환경에서만 Sentry capture 실행 From 296205c648aa194b701bb83efd34c995f62b053d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 20 Aug 2024 03:38:01 +0900 Subject: [PATCH 08/36] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20TODO:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=97=AD=ED=95=A0=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/UnhandledErrorBoundary.tsx | 14 +- client/src/hooks/useAuth/useAuth.test.tsx | 288 +++++++++--------- client/src/hooks/useAuth/useAuth.tsx | 30 +- client/src/hooks/useError/ErrorProvider.tsx | 120 ++++---- client/src/hooks/useError/useError.test.tsx | 242 +++++++-------- client/src/hooks/useEvent/useEvent.test.tsx | 158 +++++----- client/src/hooks/useEvent/useEvent.tsx | 22 +- client/src/hooks/useFetch/useFetch.test.tsx | 310 ++++++++++---------- client/src/hooks/useFetch/useFetch.ts | 96 +++--- client/src/hooks/useToast/useToast.test.tsx | 198 ++++++------- 10 files changed, 741 insertions(+), 737 deletions(-) diff --git a/client/src/UnhandledErrorBoundary.tsx b/client/src/UnhandledErrorBoundary.tsx index 78d017344..472ae0e51 100644 --- a/client/src/UnhandledErrorBoundary.tsx +++ b/client/src/UnhandledErrorBoundary.tsx @@ -1,10 +1,10 @@ -import {StrictPropsWithChildren} from 'haengdong-design/dist/type/strictPropsWithChildren'; -import {ErrorBoundary} from 'react-error-boundary'; +// import {StrictPropsWithChildren} from 'haengdong-design/dist/type/strictPropsWithChildren'; +// import {ErrorBoundary} from 'react-error-boundary'; -import ErrorPage from '@pages/ErrorPage/ErrorPage'; +// import ErrorPage from '@pages/ErrorPage/ErrorPage'; -const UnhandledErrorBoundary = ({children}: StrictPropsWithChildren) => { - return }>{children}; -}; +// const UnhandledErrorBoundary = ({children}: StrictPropsWithChildren) => { +// return }>{children}; +// }; -export default UnhandledErrorBoundary; +// export default UnhandledErrorBoundary; diff --git a/client/src/hooks/useAuth/useAuth.test.tsx b/client/src/hooks/useAuth/useAuth.test.tsx index ab520543f..1c8f1f07e 100644 --- a/client/src/hooks/useAuth/useAuth.test.tsx +++ b/client/src/hooks/useAuth/useAuth.test.tsx @@ -1,147 +1,147 @@ -import {renderHook, waitFor} from '@testing-library/react'; -import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; -import {act} from 'react'; -import {MemoryRouter} from 'react-router-dom'; - -import {useError} from '@hooks/useError/useError'; - -import {PASSWORD_LENGTH} from '@constants/password'; - -import {VALID_PASSWORD_FOR_TEST, VALID_TOKEN_FOR_TEST} from '@mocks/validValueForTest'; - -import {ErrorProvider} from '../useError/ErrorProvider'; - -import useAuth from './useAuth'; - -describe('useAuth', () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 0, - }, - }, - }); - const initializeProvider = () => - renderHook( - () => { - return {errorResult: useError(), authResult: useAuth()}; - }, - { - wrapper: ({children}) => ( - - - {children} - - - ), - }, - ); - - describe('auth', () => { - it('쿠키에 토큰이 담겨있지 않다면 인증이 실패한다', async () => { - const {result} = initializeProvider(); - - await act(async () => { - expect(await result.current.authResult.checkAuthentication()); - }); - - await waitFor(() => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_NOT_FOUND'); - }); - }); - - it('쿠키에 담겨있는 토큰이 올바르다면 인증에 성공한다', async () => { - document.cookie = `eventToken=${VALID_TOKEN_FOR_TEST}`; - - const {result} = initializeProvider(); - - await act(async () => { - expect(await result.current.authResult.checkAuthentication()); - }); - - await waitFor(() => { - expect(result.current.errorResult.errorInfo).toBe(null); - }); - }); - - it('쿠키에 담겨있는 토큰이 유효하지 않다면 인증에 실패한다.', async () => { - document.cookie = 'eventToken=fake-token'; - - const {result} = initializeProvider(); - - await act(async () => { - expect(await result.current.authResult.checkAuthentication()); - }); - - await waitFor(() => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_INVALID'); - }); - }); - - it('쿠키에 담겨있는 토큰이 만료되었다면 인증에 실패한다.', async () => { - document.cookie = 'eventToken=expired-token'; - - const {result} = initializeProvider(); - - await act(async () => { - expect(await result.current.authResult.checkAuthentication()); - }); - - await waitFor(() => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_EXPIRED'); - }); - }); - - it('쿠키에 담겨있는 토큰이 forbidden이라면 인증에 실패한다.', async () => { - document.cookie = 'eventToken=forbidden-token'; - - const {result} = initializeProvider(); - - await act(async () => { - expect(await result.current.authResult.checkAuthentication()); - }); - - await waitFor(() => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('FORBIDDEN'); - }); - }); - }); - - describe('login', () => { - it('비밀 번호가 올바르다면 로그인이 성공한다.', async () => { - const {result} = initializeProvider(); - - await act(async () => { - expect(await result.current.authResult.loginUser({password: String(VALID_PASSWORD_FOR_TEST)})); - }); - - await waitFor(() => { - expect(result.current.errorResult.errorInfo).toBe(null); - }); - }); - - it(`비밀 번호가 ${PASSWORD_LENGTH}자리가 아니라면 로그인에 실패한다.`, async () => { - const {result} = initializeProvider(); - - await act(async () => { - expect(await result.current.authResult.loginUser({password: '111'})); - }); - - await waitFor(() => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); - }); - }); +// import {renderHook, waitFor} from '@testing-library/react'; +// import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; +// import {act} from 'react'; +// import {MemoryRouter} from 'react-router-dom'; + +// import {useError} from '@hooks/useError/useError'; + +// import {PASSWORD_LENGTH} from '@constants/password'; + +// import {VALID_PASSWORD_FOR_TEST, VALID_TOKEN_FOR_TEST} from '@mocks/validValueForTest'; + +// import {ErrorProvider} from '../useError/ErrorProvider'; + +// import useAuth from './useAuth'; + +// describe('useAuth', () => { +// const queryClient = new QueryClient({ +// defaultOptions: { +// queries: { +// retry: 0, +// }, +// }, +// }); +// const initializeProvider = () => +// renderHook( +// () => { +// return {errorResult: useError(), authResult: useAuth()}; +// }, +// { +// wrapper: ({children}) => ( +// +// +// {children} +// +// +// ), +// }, +// ); + +// describe('auth', () => { +// it('쿠키에 토큰이 담겨있지 않다면 인증이 실패한다', async () => { +// const {result} = initializeProvider(); + +// await act(async () => { +// expect(await result.current.authResult.checkAuthentication()); +// }); + +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_NOT_FOUND'); +// }); +// }); + +// it('쿠키에 담겨있는 토큰이 올바르다면 인증에 성공한다', async () => { +// document.cookie = `eventToken=${VALID_TOKEN_FOR_TEST}`; + +// const {result} = initializeProvider(); + +// await act(async () => { +// expect(await result.current.authResult.checkAuthentication()); +// }); + +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo).toBe(null); +// }); +// }); + +// it('쿠키에 담겨있는 토큰이 유효하지 않다면 인증에 실패한다.', async () => { +// document.cookie = 'eventToken=fake-token'; + +// const {result} = initializeProvider(); + +// await act(async () => { +// expect(await result.current.authResult.checkAuthentication()); +// }); + +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_INVALID'); +// }); +// }); + +// it('쿠키에 담겨있는 토큰이 만료되었다면 인증에 실패한다.', async () => { +// document.cookie = 'eventToken=expired-token'; + +// const {result} = initializeProvider(); + +// await act(async () => { +// expect(await result.current.authResult.checkAuthentication()); +// }); + +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_EXPIRED'); +// }); +// }); + +// it('쿠키에 담겨있는 토큰이 forbidden이라면 인증에 실패한다.', async () => { +// document.cookie = 'eventToken=forbidden-token'; + +// const {result} = initializeProvider(); + +// await act(async () => { +// expect(await result.current.authResult.checkAuthentication()); +// }); + +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('FORBIDDEN'); +// }); +// }); +// }); + +// describe('login', () => { +// it('비밀 번호가 올바르다면 로그인이 성공한다.', async () => { +// const {result} = initializeProvider(); + +// await act(async () => { +// expect(await result.current.authResult.loginUser({password: String(VALID_PASSWORD_FOR_TEST)})); +// }); + +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo).toBe(null); +// }); +// }); + +// it(`비밀 번호가 ${PASSWORD_LENGTH}자리가 아니라면 로그인에 실패한다.`, async () => { +// const {result} = initializeProvider(); + +// await act(async () => { +// expect(await result.current.authResult.loginUser({password: '111'})); +// }); + +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); +// }); +// }); - it('비밀 번호가 틀렸다면 로그인에 실패한다.', async () => { - const {result} = initializeProvider(); +// it('비밀 번호가 틀렸다면 로그인에 실패한다.', async () => { +// const {result} = initializeProvider(); - await act(async () => { - expect(await result.current.authResult.loginUser({password: '9999'})); - }); +// await act(async () => { +// expect(await result.current.authResult.loginUser({password: '9999'})); +// }); - await waitFor(() => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('PASSWORD_INVALID'); - }); - }); - }); -}); +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('PASSWORD_INVALID'); +// }); +// }); +// }); +// }); diff --git a/client/src/hooks/useAuth/useAuth.tsx b/client/src/hooks/useAuth/useAuth.tsx index a294f6ad8..45bef3196 100644 --- a/client/src/hooks/useAuth/useAuth.tsx +++ b/client/src/hooks/useAuth/useAuth.tsx @@ -1,21 +1,21 @@ -import {RequestToken, requestPostAuthentication, requestPostToken} from '@apis/request/auth'; -import {useFetch} from '@hooks/useFetch/useFetch'; +// import {RequestToken, requestPostAuthentication, requestPostToken} from '@apis/request/auth'; +// import {useFetch} from '@hooks/useFetch/useFetch'; -import getEventIdByUrl from '@utils/getEventIdByUrl'; +// import getEventIdByUrl from '@utils/getEventIdByUrl'; -const useAuth = () => { - const {fetch} = useFetch(); - const eventId = getEventIdByUrl(); +// const useAuth = () => { +// const {fetch} = useFetch(); +// const eventId = getEventIdByUrl(); - const checkAuthentication = async () => { - return await fetch({queryFunction: () => requestPostAuthentication({eventId})}); - }; +// const checkAuthentication = async () => { +// return await fetch({queryFunction: () => requestPostAuthentication({eventId})}); +// }; - const loginUser = async ({password}: RequestToken) => { - return await fetch({queryFunction: () => requestPostToken({eventId, password})}); - }; +// const loginUser = async ({password}: RequestToken) => { +// return await fetch({queryFunction: () => requestPostToken({eventId, password})}); +// }; - return {checkAuthentication, loginUser}; -}; +// return {checkAuthentication, loginUser}; +// }; -export default useAuth; +// export default useAuth; diff --git a/client/src/hooks/useError/ErrorProvider.tsx b/client/src/hooks/useError/ErrorProvider.tsx index e1466cf4c..945d3b9af 100644 --- a/client/src/hooks/useError/ErrorProvider.tsx +++ b/client/src/hooks/useError/ErrorProvider.tsx @@ -1,69 +1,73 @@ -import {createContext, useState, useEffect, ReactNode} from 'react'; +// import {useState, useEffect, createContext, ReactNode} from 'react'; -import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; +// import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage'; +// import {useAppErrorStore} from '@store/appErrorStore'; +// import FetchError from '@errors/FetchError'; +// import {captureError} from '@utils/captureError'; +// import ErrorPage from '@pages/ErrorPage/ErrorPage'; +// import {useToast} from '@hooks/useToast/useToast'; +// import {ErrorBoundary} from 'react-error-boundary'; -// 에러 컨텍스트 생성 -export interface ErrorContextType { - clientErrorMessage: string; - setErrorInfo: (error: ErrorInfo) => void; - clearError: (ms?: number) => void; - errorInfo: ErrorInfo | null; -} +// export type ErrorInfo = { +// errorCode: string; +// message: string; +// }; -export const ErrorContext = createContext(undefined); +// export interface ErrorContextType { +// // clientErrorMessage: string; +// // setErrorInfo: (error: ErrorInfo) => void; +// // clearError: (ms?: number) => void; +// // errorInfo: ErrorInfo | null; +// setAppError: React.Dispatch>; +// } -// 에러 컨텍스트를 제공하는 프로바이더 컴포넌트 -interface ErrorProviderProps { - children: ReactNode; - callback?: (message: string) => void; -} +// export const ErrorContext = createContext(undefined); -export type ErrorInfo = { - errorCode: string; - message: string; -}; +// export const ErrorProvider = ({children}: React.PropsWithChildren) => { +// const [appError, setAppError] = useState(null); +// const [errorInfo, setErrorInfo] = useState(null); +// const {showToast} = useToast(); -export const ErrorProvider = ({children, callback}: ErrorProviderProps) => { - const [clientErrorMessage, setClientErrorMessage] = useState(''); - const [errorInfo, setErrorState] = useState(null); +// useEffect(() => { +// const catchAppError = () => { +// if (appError instanceof Error) { +// const errorInfo = +// appError instanceof FetchError ? appError.errorInfo : {errorCode: appError.name, message: appError.message}; +// setErrorInfo(errorInfo); +// captureError(appError); +// } else { +// setErrorInfo({errorCode: UNKNOWN_ERROR, message: JSON.stringify(appError)}); +// captureError(new Error(UNKNOWN_ERROR)); +// } +// }; - useEffect(() => { - if (errorInfo) { - if (isUnhandledError(errorInfo.errorCode)) { - // 에러바운더리로 보내기 +// if (appError) { +// catchAppError(); +// } +// }, [appError]); - throw errorInfo; - } +// useEffect(() => { +// if (errorInfo) { +// if (isUnhandledError(errorInfo.errorCode)) { +// // 에러바운더리로 보내기 +// // throw new Error(errorInfo.message); +// } else { +// showToast({ +// showingTime: 3000, +// message: errorInfo.message, +// type: 'error', +// position: 'bottom', +// bottom: '8rem', +// }); +// } +// } +// }, [errorInfo]); - const message = SERVER_ERROR_MESSAGES[errorInfo.errorCode]; - setClientErrorMessage(message); - // callback(message); - } - }, [errorInfo, callback]); +// const isUnhandledError = (errorCode: string) => { +// if (errorCode === 'INTERNAL_SERVER_ERROR') return true; - const setErrorInfo = (error: ErrorInfo) => { - setClientErrorMessage(''); - setErrorState(error); - }; +// return SERVER_ERROR_MESSAGES[errorCode] === undefined; +// }; - const clearError = (ms: number = 0) => { - if (errorInfo === null) return; - - setTimeout(() => { - setClientErrorMessage(''); - setErrorState(null); - }, ms); - }; - - return ( - - {children} - - ); -}; - -const isUnhandledError = (errorCode: string) => { - if (errorCode === 'INTERNAL_SERVER_ERROR') return true; - - return SERVER_ERROR_MESSAGES[errorCode] === undefined; -}; +// return {children}; +// }; diff --git a/client/src/hooks/useError/useError.test.tsx b/client/src/hooks/useError/useError.test.tsx index 7c39d0e95..b10860d37 100644 --- a/client/src/hooks/useError/useError.test.tsx +++ b/client/src/hooks/useError/useError.test.tsx @@ -1,121 +1,121 @@ -import {renderHook, waitFor} from '@testing-library/react'; -import {MemoryRouter} from 'react-router-dom'; -import {act} from 'react'; -import {HDesignProvider} from 'haengdong-design'; - -import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; - -import UnhandledErrorBoundary from '../../UnhandledErrorBoundary'; - -import {ErrorInfo, ErrorProvider} from './ErrorProvider'; -import {useError} from './useError'; - -describe('useError', () => { - const initializeProvider = () => - renderHook(() => useError(), { - wrapper: ({children}) => ( - - - - {children} - - - - ), - }); - - /** - * useError, ErrorProvider, UnhandledErrorBoundary에서 사용되는 `핸들링 가능한 에러`의 정의 - * - * : 서버에서 미리 정의한 에러 코드와 에러 메세지를 던져주는 경우 이를 `핸들링 가능한 에러`로 합니다. - * 다만 예외적으로 INTERNAL_SERVER_ERROR는 핸들링 `불가능`한 에러로 합니다. - */ - const errorCode = 'EVENT_NOT_FOUND'; - const errorInfo: ErrorInfo = {errorCode, message: '메세지입니다.'}; - const expectedClientErrorMessage = SERVER_ERROR_MESSAGES[errorCode]; - - describe('저장된 에러를 초기화한다.', () => { - it('에러 초기화 함수에 인자를 넘겨주지 않은 경우 바로 에러를 초기화한다.', async () => { - const {result} = initializeProvider(); - - await act(async () => result.current.setErrorInfo(errorInfo)); - - // 에러 메세지가 세팅되기 까지 대기 (없어도 통과하나 제대로 값이 들어간 후 초기화됨을 확인하기 위함) - await waitFor(() => { - expect(result.current.clientErrorMessage).toEqual(expectedClientErrorMessage); - }); - - await act(async () => result.current.clearError()); - - await waitFor(() => expect(result.current.errorInfo).toBe(null)); - }); - - it('저장된 에러가 없는데 초기화 함수를 호출할 경우 그냥 종료한다.', async () => { - const {result} = initializeProvider(); - - await act(async () => result.current.clearError()); - - await waitFor(() => expect(result.current.errorInfo).toBe(null)); - }); - }); - - describe('핸들링 가능한 에러', () => { - it('핸들링 가능한 에러인 경우 에러 메세지를 미리 정의된 에러 메세지로 세팅한다.', async () => { - const {result} = initializeProvider(); - - await act(async () => result.current.setErrorInfo(errorInfo)); - - await waitFor(() => expect(result.current.clientErrorMessage).toEqual(expectedClientErrorMessage)); - }); - }); - - describe('핸들링 불가능한 에러', () => { - it('에러 코드가 INTERNAL_SERVER_ERROR인 경우 핸들링 불가능한 에러로 판단하고 에러를 외부로 던진다.', async () => { - const {result} = initializeProvider(); - const errorCode = 'INTERNAL_SERVER_ERROR'; - const errorInfo: ErrorInfo = {errorCode, message: '서버 에러입니다.'}; - - await act(async () => { - try { - result.current.setErrorInfo(errorInfo); - } catch (error) { - expect(error).toBe(errorInfo); - } - }); - }); - - it('에러 코드가 UNHANDLED인 경우 핸들링 불가능한 에러로 판단하고 에러를 외부로 던진다.', async () => { - const {result} = initializeProvider(); - const errorCode = 'UNHANDLED'; - const errorInfo: ErrorInfo = {errorCode, message: '알 수 없는 에러입니다.'}; - - await act(async () => { - try { - result.current.setErrorInfo(errorInfo); - } catch (error) { - expect(error).toBe(errorInfo); - } - }); - }); - - it('에러 코드에 대응하는 에러메세지가 없는 에러인 경우 핸들링 불가능한 에러로 판단하고 에러를 외부로 던진다.', async () => { - const {result} = initializeProvider(); - const errorCode = 'something strange error...'; - const errorInfo: ErrorInfo = {errorCode, message: '정말 모르겠다.'}; - - await act(async () => { - try { - result.current.setErrorInfo(errorInfo); - } catch (error) { - expect(error).toBe(errorInfo); - } - }); - }); - }); - - it('Provider없이 useError를 사용할 경우 에러를 던진다.', () => { - expect(() => { - const _ = renderHook(() => useError()); - }).toThrow('useError must be used within an ErrorProvider'); - }); -}); +// import {renderHook, waitFor} from '@testing-library/react'; +// import {MemoryRouter} from 'react-router-dom'; +// import {act} from 'react'; +// import {HDesignProvider} from 'haengdong-design'; + +// import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; + +// import UnhandledErrorBoundary from '../../UnhandledErrorBoundary'; + +// import {ErrorInfo, ErrorProvider} from './ErrorProvider'; +// import {useError} from './useError'; + +// describe('useError', () => { +// const initializeProvider = () => +// renderHook(() => useError(), { +// wrapper: ({children}) => ( +// +// +// +// {children} +// +// +// +// ), +// }); + +// /** +// * useError, ErrorProvider, UnhandledErrorBoundary에서 사용되는 `핸들링 가능한 에러`의 정의 +// * +// * : 서버에서 미리 정의한 에러 코드와 에러 메세지를 던져주는 경우 이를 `핸들링 가능한 에러`로 합니다. +// * 다만 예외적으로 INTERNAL_SERVER_ERROR는 핸들링 `불가능`한 에러로 합니다. +// */ +// const errorCode = 'EVENT_NOT_FOUND'; +// const errorInfo: ErrorInfo = {errorCode, message: '메세지입니다.'}; +// const expectedClientErrorMessage = SERVER_ERROR_MESSAGES[errorCode]; + +// describe('저장된 에러를 초기화한다.', () => { +// it('에러 초기화 함수에 인자를 넘겨주지 않은 경우 바로 에러를 초기화한다.', async () => { +// const {result} = initializeProvider(); + +// await act(async () => result.current.setErrorInfo(errorInfo)); + +// // 에러 메세지가 세팅되기 까지 대기 (없어도 통과하나 제대로 값이 들어간 후 초기화됨을 확인하기 위함) +// await waitFor(() => { +// expect(result.current.clientErrorMessage).toEqual(expectedClientErrorMessage); +// }); + +// await act(async () => result.current.clearError()); + +// await waitFor(() => expect(result.current.errorInfo).toBe(null)); +// }); + +// it('저장된 에러가 없는데 초기화 함수를 호출할 경우 그냥 종료한다.', async () => { +// const {result} = initializeProvider(); + +// await act(async () => result.current.clearError()); + +// await waitFor(() => expect(result.current.errorInfo).toBe(null)); +// }); +// }); + +// describe('핸들링 가능한 에러', () => { +// it('핸들링 가능한 에러인 경우 에러 메세지를 미리 정의된 에러 메세지로 세팅한다.', async () => { +// const {result} = initializeProvider(); + +// await act(async () => result.current.setErrorInfo(errorInfo)); + +// await waitFor(() => expect(result.current.clientErrorMessage).toEqual(expectedClientErrorMessage)); +// }); +// }); + +// describe('핸들링 불가능한 에러', () => { +// it('에러 코드가 INTERNAL_SERVER_ERROR인 경우 핸들링 불가능한 에러로 판단하고 에러를 외부로 던진다.', async () => { +// const {result} = initializeProvider(); +// const errorCode = 'INTERNAL_SERVER_ERROR'; +// const errorInfo: ErrorInfo = {errorCode, message: '서버 에러입니다.'}; + +// await act(async () => { +// try { +// result.current.setErrorInfo(errorInfo); +// } catch (error) { +// expect(error).toBe(errorInfo); +// } +// }); +// }); + +// it('에러 코드가 UNHANDLED인 경우 핸들링 불가능한 에러로 판단하고 에러를 외부로 던진다.', async () => { +// const {result} = initializeProvider(); +// const errorCode = 'UNHANDLED'; +// const errorInfo: ErrorInfo = {errorCode, message: '알 수 없는 에러입니다.'}; + +// await act(async () => { +// try { +// result.current.setErrorInfo(errorInfo); +// } catch (error) { +// expect(error).toBe(errorInfo); +// } +// }); +// }); + +// it('에러 코드에 대응하는 에러메세지가 없는 에러인 경우 핸들링 불가능한 에러로 판단하고 에러를 외부로 던진다.', async () => { +// const {result} = initializeProvider(); +// const errorCode = 'something strange error...'; +// const errorInfo: ErrorInfo = {errorCode, message: '정말 모르겠다.'}; + +// await act(async () => { +// try { +// result.current.setErrorInfo(errorInfo); +// } catch (error) { +// expect(error).toBe(errorInfo); +// } +// }); +// }); +// }); + +// it('Provider없이 useError를 사용할 경우 에러를 던진다.', () => { +// expect(() => { +// const _ = renderHook(() => useError()); +// }).toThrow('useError must be used within an ErrorProvider'); +// }); +// }); diff --git a/client/src/hooks/useEvent/useEvent.test.tsx b/client/src/hooks/useEvent/useEvent.test.tsx index c636d49f8..eee498faa 100644 --- a/client/src/hooks/useEvent/useEvent.test.tsx +++ b/client/src/hooks/useEvent/useEvent.test.tsx @@ -1,79 +1,79 @@ -import {renderHook} from '@testing-library/react'; -import {MemoryRouter} from 'react-router-dom'; -import {act} from 'react'; -import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; - -import {useError} from '@hooks/useError/useError'; - -import {PASSWORD_LENGTH} from '@constants/password'; - -import {VALID_PASSWORD_FOR_TEST} from '@mocks/validValueForTest'; -import {VALID_EVENT_NAME_LENGTH_IN_SERVER} from '@mocks/serverConstants'; - -import {ErrorProvider} from '../useError/ErrorProvider'; - -import useEvent from './useEvent'; - -describe('useEvent', () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 0, - }, - }, - }); - - const initializeProvider = () => - renderHook( - () => { - return {errorResult: useError(), eventResult: useEvent()}; - }, - { - wrapper: ({children}) => ( - - - {children} - - - ), - }, - ); - - it('이름과 비밀번호를 받아 새로운 이벤트를 생성한다.', async () => { - const {result} = initializeProvider(); - - await act(async () => { - expect( - await result.current.eventResult.createNewEvent({eventName: '테스트이름', password: VALID_PASSWORD_FOR_TEST}), - ); - }); - - await act(async () => { - expect(result.current.errorResult.errorInfo).toBe(null); - }); - }); - - it(`이름 길이가 ${VALID_EVENT_NAME_LENGTH_IN_SERVER.min} ~ ${VALID_EVENT_NAME_LENGTH_IN_SERVER.max}사이가 아닌 경우 이벤트를 생성할 수 없다.`, async () => { - const {result} = initializeProvider(); - - await act(async () => { - expect(await result.current.eventResult.createNewEvent({eventName: '', password: VALID_PASSWORD_FOR_TEST})); - }); - - await act(async () => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_NAME_LENGTH_INVALID'); - }); - }); - - it(`비밀번호가 ${PASSWORD_LENGTH}자리수가 아닌 경우 이벤트를 생성할 수 없다`, async () => { - const {result} = initializeProvider(); - - await act(async () => { - expect(await result.current.eventResult.createNewEvent({eventName: '테스트이름', password: 1})); - }); - - await act(async () => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); - }); - }); -}); +// import {renderHook} from '@testing-library/react'; +// import {MemoryRouter} from 'react-router-dom'; +// import {act} from 'react'; +// import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; + +// import {useError} from '@hooks/useError/useError'; + +// import {PASSWORD_LENGTH} from '@constants/password'; + +// import {VALID_PASSWORD_FOR_TEST} from '@mocks/validValueForTest'; +// import {VALID_EVENT_NAME_LENGTH_IN_SERVER} from '@mocks/serverConstants'; + +// import {ErrorProvider} from '../useError/ErrorProvider'; + +// import useEvent from './useEvent'; + +// describe('useEvent', () => { +// const queryClient = new QueryClient({ +// defaultOptions: { +// queries: { +// retry: 0, +// }, +// }, +// }); + +// const initializeProvider = () => +// renderHook( +// () => { +// return {errorResult: useError(), eventResult: useEvent()}; +// }, +// { +// wrapper: ({children}) => ( +// +// +// {children} +// +// +// ), +// }, +// ); + +// it('이름과 비밀번호를 받아 새로운 이벤트를 생성한다.', async () => { +// const {result} = initializeProvider(); + +// await act(async () => { +// expect( +// await result.current.eventResult.createNewEvent({eventName: '테스트이름', password: VALID_PASSWORD_FOR_TEST}), +// ); +// }); + +// await act(async () => { +// expect(result.current.errorResult.errorInfo).toBe(null); +// }); +// }); + +// it(`이름 길이가 ${VALID_EVENT_NAME_LENGTH_IN_SERVER.min} ~ ${VALID_EVENT_NAME_LENGTH_IN_SERVER.max}사이가 아닌 경우 이벤트를 생성할 수 없다.`, async () => { +// const {result} = initializeProvider(); + +// await act(async () => { +// expect(await result.current.eventResult.createNewEvent({eventName: '', password: VALID_PASSWORD_FOR_TEST})); +// }); + +// await act(async () => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_NAME_LENGTH_INVALID'); +// }); +// }); + +// it(`비밀번호가 ${PASSWORD_LENGTH}자리수가 아닌 경우 이벤트를 생성할 수 없다`, async () => { +// const {result} = initializeProvider(); + +// await act(async () => { +// expect(await result.current.eventResult.createNewEvent({eventName: '테스트이름', password: 1})); +// }); + +// await act(async () => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); +// }); +// }); +// }); diff --git a/client/src/hooks/useEvent/useEvent.tsx b/client/src/hooks/useEvent/useEvent.tsx index e58733fc0..b9afa2a89 100644 --- a/client/src/hooks/useEvent/useEvent.tsx +++ b/client/src/hooks/useEvent/useEvent.tsx @@ -1,16 +1,16 @@ -// TODO: (@todari) useEvent는 이제 쓰지 않긴 해요...! +// // TODO: (@todari) useEvent는 이제 쓰지 않긴 해요...! -import {RequestPostNewEvent, ResponsePostNewEvent, requestPostNewEvent} from '@apis/request/event'; -import {useFetch} from '@hooks/useFetch/useFetch'; +// import {RequestPostNewEvent, ResponsePostNewEvent, requestPostNewEvent} from '@apis/request/event'; +// import {useFetch} from '@hooks/useFetch/useFetch'; -const useEvent = () => { - const {fetch} = useFetch(); +// const useEvent = () => { +// const {fetch} = useFetch(); - const createNewEvent = async ({eventName, password}: RequestPostNewEvent) => { - return await fetch({queryFunction: () => requestPostNewEvent({eventName, password})}); - }; +// const createNewEvent = async ({eventName, password}: RequestPostNewEvent) => { +// return await fetch({queryFunction: () => requestPostNewEvent({eventName, password})}); +// }; - return {createNewEvent}; -}; +// return {createNewEvent}; +// }; -export default useEvent; +// export default useEvent; diff --git a/client/src/hooks/useFetch/useFetch.test.tsx b/client/src/hooks/useFetch/useFetch.test.tsx index b4748125a..8545cd1d7 100644 --- a/client/src/hooks/useFetch/useFetch.test.tsx +++ b/client/src/hooks/useFetch/useFetch.test.tsx @@ -1,155 +1,155 @@ -import {renderHook, waitFor} from '@testing-library/react'; -import {MemoryRouter} from 'react-router-dom'; -import {act} from 'react'; - -import FetchError from '@errors/FetchError'; -import {useError} from '@hooks/useError/useError'; - -import {requestPostWithoutResponse} from '@apis/fetcher'; - -import {captureError} from '@utils/captureError'; - -import {UNKNOWN_ERROR} from '@constants/errorMessage'; - -import {ErrorProvider} from '../useError/ErrorProvider'; - -import {useFetch} from './useFetch'; - -describe('useFetch', () => { - const initializeProvider = () => - renderHook( - () => { - return {errorResult: useError(), fetchResult: useFetch()}; - }, - { - wrapper: ({children}) => ( - - {children} - - ), - }, - ); - - describe('요청이 성공하는 경우', () => { - it('요청이 성공했다면 그대로 api response body를 반환한다.', async () => { - const {result} = initializeProvider(); - const mockQueryFunction = jest.fn().mockResolvedValue('mocked data'); - - let data; - - await act(async () => { - data = await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); - }); - - expect(data).toBe('mocked data'); - }); - - it('onSuccess 콜백을 넘겨준 경우 콜백을 실행한다.', async () => { - const {result} = initializeProvider(); - const mockQueryFunction = jest.fn().mockResolvedValue('mocked data'); - const onSuccess = jest.fn(); - - await act(async () => { - await result.current.fetchResult.fetch({queryFunction: mockQueryFunction, onSuccess}); - }); - - expect(onSuccess).toHaveBeenCalled(); - }); - }); - - describe('요청이 실패하는 경우', () => { - describe('발생한 에러가 Error 인스턴스인 경우', () => { - const errorThrowFunction = () => requestPostWithoutResponse({endpoint: '/throw-handle-error'}); - - it('FetchError가 발생하면 해당 에러의 errorBody를 사용해 상태를 저장한다.', async () => { - const {result} = initializeProvider(); - const fetchError = new FetchError({ - errorInfo: {errorCode: 'UNHANDLED', message: 'Fetch error occurred'}, - name: 'UNHANDLED', - message: 'Fetch error occurred', - requestBody: '', - status: 400, - endpoint: '', - method: 'POST', - }); - const mockQueryFunction = jest.fn().mockRejectedValue(fetchError); - - await act(async () => { - await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); - }); - - await waitFor(() => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('UNHANDLED'); - expect(result.current.errorResult.errorInfo?.message).toBe('Fetch error occurred'); - }); - }); - - it('일반 Error가 발생하면 해당 에러의 name과 message를 사용해 상태를 저장한다.', async () => { - const {result} = initializeProvider(); - const mockError = new Error('일반 에러 발생'); - const mockQueryFunction = jest.fn().mockRejectedValue(mockError); - - try { - await act(async () => { - await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); - }); - } catch (error) { - // 에러 바운더리로 보내지는 에러라서 throw하는데 이를 받아줄 에러 바운더리를 호출하지 않았으므로 catch문에서 별다른 로직을 작성하지 않음 - } - - await waitFor(() => { - expect(result.current.errorResult.errorInfo?.errorCode).toBe('Error'); - expect(result.current.errorResult.errorInfo?.message).toBe('일반 에러 발생'); - }); - }); - - it('onError 콜백을 넘겨준 경우 onError를 실행한다.', async () => { - const {result} = initializeProvider(); - const onError = jest.fn(); - - await act(async () => { - await result.current.fetchResult.fetch({queryFunction: errorThrowFunction, onError}); - }); - - expect(onError).toHaveBeenCalled(); - }); - - it('에러가 발생하면 로그를 보낸다.', async () => { - const {result} = initializeProvider(); - - await act(async () => { - await result.current.fetchResult.fetch({queryFunction: errorThrowFunction}); - }); - - expect(captureError).toHaveBeenCalled(); - }); - }); - - describe('발생한 에러가 Error 인스턴스가 아닌 경우', () => { - const mockQueryFunction = jest.fn().mockRejectedValue('unexpected error'); - - it(`에러가 발생하면 그 에러를 던진다.`, async () => { - const {result} = initializeProvider(); - - // 에러가 발생하고 에러를 던지는지 확인 - await expect( - act(async () => { - await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); - }), - ).rejects.toThrow(new Error(UNKNOWN_ERROR)); - }); - - it('에러가 발생하면 로그를 보낸다.', async () => { - const {result} = initializeProvider(); - - await expect( - act(async () => { - await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); - }), - ).rejects.toThrow(new Error(UNKNOWN_ERROR)); - - expect(captureError).toHaveBeenCalled(); - }); - }); - }); -}); +// import {renderHook, waitFor} from '@testing-library/react'; +// import {MemoryRouter} from 'react-router-dom'; +// import {act} from 'react'; + +// import FetchError from '@errors/FetchError'; +// import {useError} from '@hooks/useError/useError'; + +// import {requestPostWithoutResponse} from '@apis/fetcher'; + +// import {captureError} from '@utils/captureError'; + +// import {UNKNOWN_ERROR} from '@constants/errorMessage'; + +// import {ErrorProvider} from '../useError/ErrorProvider'; + +// import {useFetch} from './useFetch'; + +// describe('useFetch', () => { +// const initializeProvider = () => +// renderHook( +// () => { +// return {errorResult: useError(), fetchResult: useFetch()}; +// }, +// { +// wrapper: ({children}) => ( +// +// {children} +// +// ), +// }, +// ); + +// describe('요청이 성공하는 경우', () => { +// it('요청이 성공했다면 그대로 api response body를 반환한다.', async () => { +// const {result} = initializeProvider(); +// const mockQueryFunction = jest.fn().mockResolvedValue('mocked data'); + +// let data; + +// await act(async () => { +// data = await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); +// }); + +// expect(data).toBe('mocked data'); +// }); + +// it('onSuccess 콜백을 넘겨준 경우 콜백을 실행한다.', async () => { +// const {result} = initializeProvider(); +// const mockQueryFunction = jest.fn().mockResolvedValue('mocked data'); +// const onSuccess = jest.fn(); + +// await act(async () => { +// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction, onSuccess}); +// }); + +// expect(onSuccess).toHaveBeenCalled(); +// }); +// }); + +// describe('요청이 실패하는 경우', () => { +// describe('발생한 에러가 Error 인스턴스인 경우', () => { +// const errorThrowFunction = () => requestPostWithoutResponse({endpoint: '/throw-handle-error'}); + +// it('FetchError가 발생하면 해당 에러의 errorBody를 사용해 상태를 저장한다.', async () => { +// const {result} = initializeProvider(); +// const fetchError = new FetchError({ +// errorInfo: {errorCode: 'UNHANDLED', message: 'Fetch error occurred'}, +// name: 'UNHANDLED', +// message: 'Fetch error occurred', +// requestBody: '', +// status: 400, +// endpoint: '', +// method: 'POST', +// }); +// const mockQueryFunction = jest.fn().mockRejectedValue(fetchError); + +// await act(async () => { +// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); +// }); + +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('UNHANDLED'); +// expect(result.current.errorResult.errorInfo?.message).toBe('Fetch error occurred'); +// }); +// }); + +// it('일반 Error가 발생하면 해당 에러의 name과 message를 사용해 상태를 저장한다.', async () => { +// const {result} = initializeProvider(); +// const mockError = new Error('일반 에러 발생'); +// const mockQueryFunction = jest.fn().mockRejectedValue(mockError); + +// try { +// await act(async () => { +// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); +// }); +// } catch (error) { +// // 에러 바운더리로 보내지는 에러라서 throw하는데 이를 받아줄 에러 바운더리를 호출하지 않았으므로 catch문에서 별다른 로직을 작성하지 않음 +// } + +// await waitFor(() => { +// expect(result.current.errorResult.errorInfo?.errorCode).toBe('Error'); +// expect(result.current.errorResult.errorInfo?.message).toBe('일반 에러 발생'); +// }); +// }); + +// it('onError 콜백을 넘겨준 경우 onError를 실행한다.', async () => { +// const {result} = initializeProvider(); +// const onError = jest.fn(); + +// await act(async () => { +// await result.current.fetchResult.fetch({queryFunction: errorThrowFunction, onError}); +// }); + +// expect(onError).toHaveBeenCalled(); +// }); + +// it('에러가 발생하면 로그를 보낸다.', async () => { +// const {result} = initializeProvider(); + +// await act(async () => { +// await result.current.fetchResult.fetch({queryFunction: errorThrowFunction}); +// }); + +// expect(captureError).toHaveBeenCalled(); +// }); +// }); + +// describe('발생한 에러가 Error 인스턴스가 아닌 경우', () => { +// const mockQueryFunction = jest.fn().mockRejectedValue('unexpected error'); + +// it(`에러가 발생하면 그 에러를 던진다.`, async () => { +// const {result} = initializeProvider(); + +// // 에러가 발생하고 에러를 던지는지 확인 +// await expect( +// act(async () => { +// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); +// }), +// ).rejects.toThrow(new Error(UNKNOWN_ERROR)); +// }); + +// it('에러가 발생하면 로그를 보낸다.', async () => { +// const {result} = initializeProvider(); + +// await expect( +// act(async () => { +// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); +// }), +// ).rejects.toThrow(new Error(UNKNOWN_ERROR)); + +// expect(captureError).toHaveBeenCalled(); +// }); +// }); +// }); +// }); diff --git a/client/src/hooks/useFetch/useFetch.ts b/client/src/hooks/useFetch/useFetch.ts index 35bf18a33..1df6cb082 100644 --- a/client/src/hooks/useFetch/useFetch.ts +++ b/client/src/hooks/useFetch/useFetch.ts @@ -1,63 +1,63 @@ -import {useState} from 'react'; -import {useNavigate} from 'react-router-dom'; +// import {useState} from 'react'; +// import {useNavigate} from 'react-router-dom'; -import FetchError from '@errors/FetchError'; -import {useError} from '@hooks/useError/useError'; +// import FetchError from '@errors/FetchError'; +// import {useError} from '@hooks/useError/useError'; -import {captureError} from '@utils/captureError'; -import getEventIdByUrl from '@utils/getEventIdByUrl'; +// import {captureError} from '@utils/captureError'; +// import getEventIdByUrl from '@utils/getEventIdByUrl'; -import {UNKNOWN_ERROR} from '@constants/errorMessage'; +// import {UNKNOWN_ERROR} from '@constants/errorMessage'; -type FetchProps = { - queryFunction: () => Promise; - onSuccess?: () => void; - onError?: () => void; -}; +// type FetchProps = { +// queryFunction: () => Promise; +// onSuccess?: () => void; +// onError?: () => void; +// }; -export const useFetch = () => { - const {setErrorInfo, clearError} = useError(); - const [loading, setLoading] = useState(false); - const navigate = useNavigate(); - const eventId = getEventIdByUrl(); +// export const useFetch = () => { +// const {setErrorInfo, clearError} = useError(); +// const [loading, setLoading] = useState(false); +// const navigate = useNavigate(); +// const eventId = getEventIdByUrl(); - const fetch = async ({queryFunction, onSuccess, onError}: FetchProps): Promise => { - setLoading(true); +// const fetch = async ({queryFunction, onSuccess, onError}: FetchProps): Promise => { +// setLoading(true); - clearError(); - try { - const result = await queryFunction(); +// clearError(); +// try { +// const result = await queryFunction(); - if (onSuccess) { - onSuccess(); - } +// if (onSuccess) { +// onSuccess(); +// } - return result; - } catch (error) { - if (error instanceof Error) { - const errorInfo = - error instanceof FetchError ? error.errorInfo : {errorCode: error.name, message: error.message}; +// return result; +// } catch (error) { +// if (error instanceof Error) { +// const errorInfo = +// error instanceof FetchError ? error.errorInfo : {errorCode: error.name, message: error.message}; - setErrorInfo(errorInfo); +// setErrorInfo(errorInfo); - if (onError) { - onError(); - } +// if (onError) { +// onError(); +// } - captureError(error, navigate, eventId); - } else { - setErrorInfo({errorCode: UNKNOWN_ERROR, message: JSON.stringify(error)}); - captureError(new Error(UNKNOWN_ERROR), navigate, eventId); +// captureError(error); +// } else { +// setErrorInfo({errorCode: UNKNOWN_ERROR, message: JSON.stringify(error)}); +// captureError(new Error(UNKNOWN_ERROR)); - // 에러를 throw 해 에러 바운더리로 보냅니다. 따라서 에러 이름은 중요하지 않음 - throw new Error(UNKNOWN_ERROR); - } - } finally { - setLoading(false); - } +// // 에러를 throw 해 에러 바운더리로 보냅니다. 따라서 에러 이름은 중요하지 않음 +// throw new Error(UNKNOWN_ERROR); +// } +// } finally { +// setLoading(false); +// } - return {} as T; - }; +// return {} as T; +// }; - return {loading, fetch}; -}; +// return {loading, fetch}; +// }; diff --git a/client/src/hooks/useToast/useToast.test.tsx b/client/src/hooks/useToast/useToast.test.tsx index 6f9522a24..86da9bf53 100644 --- a/client/src/hooks/useToast/useToast.test.tsx +++ b/client/src/hooks/useToast/useToast.test.tsx @@ -1,99 +1,99 @@ -import {render, screen, waitFor} from '@testing-library/react'; -import {act, ReactNode} from 'react'; -import {HDesignProvider} from 'haengdong-design'; - -import {useError} from '@hooks/useError/useError'; - -import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; - -import UnhandledErrorBoundary from '../../UnhandledErrorBoundary'; -import {ErrorInfo, ErrorProvider} from '../../hooks/useError/ErrorProvider'; // useError 경로에 맞게 설정 - -import {ToastProvider} from './ToastProvider'; // 위 코드에 해당하는 ToastProvider 경로 - -// 테스트용 헬퍼 컴포넌트 -const TestComponent = ({errorInfo}: {errorInfo: ErrorInfo}) => { - const {setErrorInfo} = useError(); - - // 테스트에서 직접 에러를 설정합니다. - const triggerError = () => { - setErrorInfo(errorInfo); - }; - - return ; -}; - -const setup = (ui: ReactNode) => - render( - - - - {ui} - - - , - ); - -beforeEach(() => { - jest.useFakeTimers(); -}); - -afterEach(() => { - jest.useRealTimers(); -}); - -describe('useToast', () => { - describe('error의 경우 자동으로 토스트를 띄워준다.', () => { - it('핸들링 가능한 에러인 경우 토스트가 뜬다.', async () => { - const errorCode = 'ACTION_NOT_FOUND'; - - setup( - , - ); - const errorMessage = SERVER_ERROR_MESSAGES[errorCode]; - - act(() => { - // 에러 트리거 버튼을 클릭 - screen.getByText('Trigger Error').click(); - }); - - // 토스트가 표시되는지 확인 - await waitFor(() => { - expect(screen.getByText(errorMessage)).toBeInTheDocument(); - }); - - // 타이머가 지나서 토스트가 사라지는지 확인 - jest.runAllTimers(); // Jest의 타이머를 실행 - await waitFor(() => { - expect(screen.queryByText(errorMessage)).not.toBeInTheDocument(); - }); - }); - - it('핸들링 불가능한 에러인 경우 토스트가 안뜬다.', async () => { - const errorCode = '핸들링이 안되는 에러 코드'; - - setup( - , - ); - - act(() => { - // 에러 트리거 버튼을 클릭 - screen.getByText('Trigger Error').click(); - }); - - await waitFor(() => { - expect(document.getElementById('toast')).not.toBeInTheDocument(); - }); - }); - }); -}); +// import {render, screen, waitFor} from '@testing-library/react'; +// import {act, ReactNode} from 'react'; +// import {HDesignProvider} from 'haengdong-design'; + +// import {useError} from '@hooks/useError/useError'; + +// import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; + +// import UnhandledErrorBoundary from '../../UnhandledErrorBoundary'; +// import {ErrorInfo, ErrorProvider} from '../../hooks/useError/ErrorProvider'; // useError 경로에 맞게 설정 + +// import {ToastProvider} from './ToastProvider'; // 위 코드에 해당하는 ToastProvider 경로 + +// // 테스트용 헬퍼 컴포넌트 +// const TestComponent = ({errorInfo}: {errorInfo: ErrorInfo}) => { +// const {setErrorInfo} = useError(); + +// // 테스트에서 직접 에러를 설정합니다. +// const triggerError = () => { +// setErrorInfo(errorInfo); +// }; + +// return ; +// }; + +// const setup = (ui: ReactNode) => +// render( +// +// +// +// {ui} +// +// +// , +// ); + +// beforeEach(() => { +// jest.useFakeTimers(); +// }); + +// afterEach(() => { +// jest.useRealTimers(); +// }); + +// describe('useToast', () => { +// describe('error의 경우 자동으로 토스트를 띄워준다.', () => { +// it('핸들링 가능한 에러인 경우 토스트가 뜬다.', async () => { +// const errorCode = 'ACTION_NOT_FOUND'; + +// setup( +// , +// ); +// const errorMessage = SERVER_ERROR_MESSAGES[errorCode]; + +// act(() => { +// // 에러 트리거 버튼을 클릭 +// screen.getByText('Trigger Error').click(); +// }); + +// // 토스트가 표시되는지 확인 +// await waitFor(() => { +// expect(screen.getByText(errorMessage)).toBeInTheDocument(); +// }); + +// // 타이머가 지나서 토스트가 사라지는지 확인 +// jest.runAllTimers(); // Jest의 타이머를 실행 +// await waitFor(() => { +// expect(screen.queryByText(errorMessage)).not.toBeInTheDocument(); +// }); +// }); + +// it('핸들링 불가능한 에러인 경우 토스트가 안뜬다.', async () => { +// const errorCode = '핸들링이 안되는 에러 코드'; + +// setup( +// , +// ); + +// act(() => { +// // 에러 트리거 버튼을 클릭 +// screen.getByText('Trigger Error').click(); +// }); + +// await waitFor(() => { +// expect(document.getElementById('toast')).not.toBeInTheDocument(); +// }); +// }); +// }); +// }); From 7e964e13447fcd3f77af1b36cc7defbd7be9718a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 21 Aug 2024 00:40:34 +0900 Subject: [PATCH 09/36] =?UTF-8?q?mover:=20AppErrorBoundary,=20QueryClientB?= =?UTF-8?q?oundary=20=EC=BD=94=EB=93=9C=20=EC=9C=84=EC=B9=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 4 ++-- client/src/apis/fetcher.ts | 2 +- .../AppErrorBoundary}/AppErrorBoundary.tsx | 18 +++++------------- .../QueryClientBoundary.tsx | 0 client/src/hooks/useToast/ToastProvider.tsx | 6 +----- client/src/utils/captureError.ts | 2 +- client/src/utils/sendLogToSentry.ts | 2 +- 7 files changed, 11 insertions(+), 23 deletions(-) rename client/src/{ => components/AppErrorBoundary}/AppErrorBoundary.tsx (84%) rename client/src/{ => components/QueryClientBoundary}/QueryClientBoundary.tsx (100%) diff --git a/client/src/App.tsx b/client/src/App.tsx index 8e03f4c05..ef750ee07 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,8 +5,8 @@ import {Global} from '@emotion/react'; import {ToastProvider} from '@hooks/useToast/ToastProvider'; import {GlobalStyle} from './GlobalStyle'; -import AppErrorBoundary from './AppErrorBoundary'; -import QueryClientBoundary from './QueryClientBoundary'; +import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; +import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; const App: React.FC = () => { return ( diff --git a/client/src/apis/fetcher.ts b/client/src/apis/fetcher.ts index 2bfb7ada4..7e9c1f231 100644 --- a/client/src/apis/fetcher.ts +++ b/client/src/apis/fetcher.ts @@ -3,7 +3,7 @@ import objectToQueryString from '@utils/objectToQueryString'; import {UNKNOWN_ERROR} from '@constants/errorMessage'; import FetchError from '../errors/FetchError'; -import {ErrorInfo} from 'AppErrorBoundary'; +import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; export type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; diff --git a/client/src/AppErrorBoundary.tsx b/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx similarity index 84% rename from client/src/AppErrorBoundary.tsx rename to client/src/components/AppErrorBoundary/AppErrorBoundary.tsx index a85a4bc4d..ec582140d 100644 --- a/client/src/AppErrorBoundary.tsx +++ b/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx @@ -6,11 +6,8 @@ import FetchError from '@errors/FetchError'; import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage'; import {useToast} from '@hooks/useToast/useToast'; import {useAppErrorStore} from '@store/appErrorStore'; -import {useEffect, useState} from 'react'; - -interface ErrorFallbackProps { - error: Error; -} +import {useEffect} from 'react'; +import {useNavigate} from 'react-router-dom'; export type ErrorInfo = { errorCode: string; @@ -39,12 +36,13 @@ const isUnhandledError = (errorInfo: ErrorInfo) => { const AppErrorBoundary = ({children}: React.PropsWithChildren) => { const {appError} = useAppErrorStore(); const {showToast} = useToast(); + const navigate = useNavigate(); useEffect(() => { if (appError) { captureError(appError instanceof FetchError ? appError : new Error(UNKNOWN_ERROR)); const errorInfo = convertAppErrorToErrorInfo(appError); - console.log(errorInfo); + if (!isUnhandledError(errorInfo)) { showToast({ showingTime: 3000, @@ -54,13 +52,7 @@ const AppErrorBoundary = ({children}: React.PropsWithChildren) => { bottom: '8rem', }); } else { - showToast({ - showingTime: 3000, - message: SERVER_ERROR_MESSAGES.UNHANDLED, - type: 'error', - position: 'bottom', - bottom: '8rem', - }); + navigate('/error'); } } }, [appError]); diff --git a/client/src/QueryClientBoundary.tsx b/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx similarity index 100% rename from client/src/QueryClientBoundary.tsx rename to client/src/components/QueryClientBoundary/QueryClientBoundary.tsx diff --git a/client/src/hooks/useToast/ToastProvider.tsx b/client/src/hooks/useToast/ToastProvider.tsx index 1980a9bde..037ba98b6 100644 --- a/client/src/hooks/useToast/ToastProvider.tsx +++ b/client/src/hooks/useToast/ToastProvider.tsx @@ -1,9 +1,5 @@ /** @jsxImportSource @emotion/react */ -import {createContext, useContext, useEffect, useState} from 'react'; - -import {useError} from '@hooks/useError/useError'; - -import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; +import {createContext, useEffect, useState} from 'react'; import {ToastProps} from '../../components/Toast/Toast.type'; import Toast from '../../components/Toast/Toast'; diff --git a/client/src/utils/captureError.ts b/client/src/utils/captureError.ts index 051c9ce0e..bd98b393b 100644 --- a/client/src/utils/captureError.ts +++ b/client/src/utils/captureError.ts @@ -1,7 +1,7 @@ import FetchError from '@errors/FetchError'; import sendLogToSentry from './sendLogToSentry'; -import {ErrorInfo} from 'AppErrorBoundary'; +import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; export const captureError = async (error: Error) => { // prod 환경에서만 Sentry capture 실행 diff --git a/client/src/utils/sendLogToSentry.ts b/client/src/utils/sendLogToSentry.ts index d93e526a4..bba09ed34 100644 --- a/client/src/utils/sendLogToSentry.ts +++ b/client/src/utils/sendLogToSentry.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/react'; -import {ErrorInfo} from '@hooks/useError/ErrorProvider'; +import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; import {UNKNOWN_ERROR} from '@constants/errorMessage'; From 78bb0881d5176a94ba6976a8f162eafb8983d30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 21 Aug 2024 00:40:57 +0900 Subject: [PATCH 10/36] =?UTF-8?q?fix:=20test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useDeleteMemberAction.test.tsx | 28 +++++++++---------- .../useSearchMemberReportList.test.tsx | 26 ++++++++--------- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx index 00436cafa..702f482b9 100644 --- a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx +++ b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx @@ -1,16 +1,18 @@ import {renderHook, waitFor} from '@testing-library/react'; import {MemoryRouter} from 'react-router-dom'; import {act} from 'react'; -import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {BillStep, MemberAction, MemberStep} from 'types/serviceType'; -import {ErrorProvider} from '@hooks/useError/ErrorProvider'; import useRequestGetStepList from '@hooks/queries/useRequestGetStepList'; import stepListJson from '@mocks/stepList.json'; import invalidMemberStepListJson from '@mocks/invalidMemberStepList.json'; import useDeleteMemberAction from './useDeleteMemberAction'; +import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; +import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; +import {ToastProvider} from '@hooks/useToast/ToastProvider'; +import {HDesignProvider} from 'haengdong-design'; const stepListMockData = stepListJson as (BillStep | MemberStep)[]; let memberActionList: MemberAction[] = []; @@ -22,14 +24,6 @@ for (let i = 0; i < stepListMockData.length; i++) { } describe('useDeleteMemberAction', () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 0, - }, - }, - }); - const initializeProvider = (list: MemberAction[] = memberActionList) => renderHook( () => { @@ -45,11 +39,15 @@ describe('useDeleteMemberAction', () => { }, { wrapper: ({children}) => ( - - - {children} - - + + + + + {children} + + + + ), }, ); diff --git a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx b/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx index a2b0de89d..51b1b5825 100644 --- a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx +++ b/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx @@ -1,29 +1,25 @@ import {renderHook, waitFor} from '@testing-library/react'; import {MemoryRouter} from 'react-router-dom'; -import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; +import {QueryClient} from '@tanstack/react-query'; import reportListJson from '../../mocks/reportList.json'; -import {ErrorProvider} from '../useError/ErrorProvider'; import useSearchMemberReportList from './useSearchMemberReportList'; +import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; +import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; +import {ToastProvider} from '@hooks/useToast/ToastProvider'; describe('useSearchMemberReportList', () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: 0, - }, - }, - }); - const initializeProvider = (name: string) => renderHook(() => useSearchMemberReportList({name}), { wrapper: ({children}) => ( - - - {children} - - + + + + {children} + + + ), }); From 3fa2cfac5aee834f5adf5f2309410ea784fc2ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 21 Aug 2024 00:41:11 +0900 Subject: [PATCH 11/36] =?UTF-8?q?chore:=20jest=20path=20alias=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/jest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/jest.config.ts b/client/jest.config.ts index 4bb1dbd3e..8181ab583 100644 --- a/client/jest.config.ts +++ b/client/jest.config.ts @@ -29,6 +29,7 @@ const config: Config = { '@/(.*)$': '/src/$1', // path alias를 적용하기 위함 '^@apis/(.*)$': '/src/apis/$1', '^@constants/(.*)$': '/src/constants/$1', + '^@components/(.*)$': '/src/components/$1', '^@hooks/(.*)$': '/src/hooks/$1', '^@utils/(.*)$': '/src/utils/$1', '^@pages/(.*)$': '/src/pages/$1', From f32b9b6c9cfb953ebf499453f3096bd407b6d6d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 21 Aug 2024 00:52:39 +0900 Subject: [PATCH 12/36] =?UTF-8?q?test:=20AppErrorBoundary=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppErrorBoundary.test.tsx | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx diff --git a/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx b/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx new file mode 100644 index 000000000..b4f4ef02b --- /dev/null +++ b/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx @@ -0,0 +1,78 @@ +import {render, screen, waitFor} from '@testing-library/react'; +import {act, ReactNode} from 'react'; + +import {useAppErrorStore} from '@store/appErrorStore'; +import FetchError from '@errors/FetchError'; +import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; +import {ToastProvider} from '@hooks/useToast/ToastProvider'; +import AppErrorBoundary from './AppErrorBoundary'; +import {MemoryRouter, useNavigate} from 'react-router-dom'; +import {HDesignProvider} from 'haengdong-design'; + +// 테스트용 헬퍼 컴포넌트 +const TestComponent = ({triggerError}: {triggerError: () => void}) => { + return ; +}; + +const setup = (ui: ReactNode) => + render( + + + + {ui} + + + , + ); + +describe('AppErrorBoundary', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), + })); + + it('핸들링 가능한 에러인 경우 토스트가 표시된다.', async () => { + const errorCode = 'EVENT_NOT_FOUND'; + const error = new FetchError({ + errorInfo: {errorCode, message: '서버의 에러메세지'}, + name: errorCode, + message: '에러메세지', + status: 200, + endpoint: '', + method: 'GET', + requestBody: '', + }); + + const {updateAppError} = useAppErrorStore.getState(); + + setup( updateAppError(error)} />); + + act(() => { + screen.getByText('Trigger Error').click(); + }); + + const errorMessage = SERVER_ERROR_MESSAGES[errorCode]; + + await waitFor(() => { + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + }); + + // it('핸들링 불가능한 에러인 경우 fallback이 표시된다.', async () => { + // const navigate = useNavigate(); + // (navigate as jest.Mock).mockImplementation(() => {}); + // const {updateAppError} = useAppErrorStore.getState(); + + // const error = new Error('알 수 없는 에러'); + // setup( updateAppError(error)} />); + + // act(() => { + // screen.getByText('Trigger Error').click(); + // }); + + // // TODO: (@todari) 해결안됨 + // await waitFor(() => { + // expect(navigate).toHaveBeenCalledWith('/error'); + // }); + // }); +}); From 031d8b555ae0e8cc23cd10894c1c42d0124de316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 21 Aug 2024 00:53:04 +0900 Subject: [PATCH 13/36] =?UTF-8?q?remove:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/UnhandledErrorBoundary.tsx | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 client/src/UnhandledErrorBoundary.tsx diff --git a/client/src/UnhandledErrorBoundary.tsx b/client/src/UnhandledErrorBoundary.tsx deleted file mode 100644 index 472ae0e51..000000000 --- a/client/src/UnhandledErrorBoundary.tsx +++ /dev/null @@ -1,10 +0,0 @@ -// import {StrictPropsWithChildren} from 'haengdong-design/dist/type/strictPropsWithChildren'; -// import {ErrorBoundary} from 'react-error-boundary'; - -// import ErrorPage from '@pages/ErrorPage/ErrorPage'; - -// const UnhandledErrorBoundary = ({children}: StrictPropsWithChildren) => { -// return }>{children}; -// }; - -// export default UnhandledErrorBoundary; From 6a9aabca91e4ad55e71686ba9633eae1a262ad58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 21 Aug 2024 00:53:45 +0900 Subject: [PATCH 14/36] =?UTF-8?q?fix:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/hooks/useError/useError.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/src/hooks/useError/useError.tsx b/client/src/hooks/useError/useError.tsx index ee7176233..db194c591 100644 --- a/client/src/hooks/useError/useError.tsx +++ b/client/src/hooks/useError/useError.tsx @@ -1,12 +1,12 @@ -import {useContext} from 'react'; +// import {useContext} from 'react'; -import {ErrorContext, ErrorContextType} from './ErrorProvider'; +// import {ErrorContext, ErrorContextType} from './ErrorProvider'; -// 에러 컨텍스트를 사용하는 커스텀 훅 -export const useError = (): ErrorContextType => { - const context = useContext(ErrorContext); - if (!context) { - throw new Error('useError must be used within an ErrorProvider'); - } - return context; -}; +// // 에러 컨텍스트를 사용하는 커스텀 훅 +// export const useError = (): ErrorContextType => { +// const context = useContext(ErrorContext); +// if (!context) { +// throw new Error('useError must be used within an ErrorProvider'); +// } +// return context; +// }; From 84aa761f4b84016f737fba19c9efa7a2e2aef455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 21 Aug 2024 02:29:11 +0900 Subject: [PATCH 15/36] =?UTF-8?q?fix:=20AppErrorBoundary,=20QueryClientBou?= =?UTF-8?q?ndary=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppErrorBoundary/AppErrorBoundary.tsx | 37 +++++++++++++++++-- .../QueryClientBoundary.tsx | 1 + 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx b/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx index ec582140d..6d4ba3d73 100644 --- a/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx +++ b/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx @@ -7,13 +7,16 @@ import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage'; import {useToast} from '@hooks/useToast/useToast'; import {useAppErrorStore} from '@store/appErrorStore'; import {useEffect} from 'react'; -import {useNavigate} from 'react-router-dom'; export type ErrorInfo = { errorCode: string; message: string; }; +type FallbackComponentProps = { + error: Error; +}; + const convertAppErrorToErrorInfo = (appError: Error) => { if (appError instanceof Error) { const errorInfo = @@ -36,7 +39,27 @@ const isUnhandledError = (errorInfo: ErrorInfo) => { const AppErrorBoundary = ({children}: React.PropsWithChildren) => { const {appError} = useAppErrorStore(); const {showToast} = useToast(); - const navigate = useNavigate(); + + const fallbackComponent = ({error}: FallbackComponentProps) => { + if (error) { + console.log(Error); + captureError(error instanceof FetchError ? error : new Error(UNKNOWN_ERROR)); + const errorInfo = convertAppErrorToErrorInfo(error); + if (!isUnhandledError(errorInfo)) { + showToast({ + showingTime: 3000, + message: SERVER_ERROR_MESSAGES[errorInfo.errorCode], + type: 'error', + position: 'bottom', + bottom: '8rem', + }); + } else { + return ; + } + } + + return null; + }; useEffect(() => { if (appError) { @@ -52,12 +75,18 @@ const AppErrorBoundary = ({children}: React.PropsWithChildren) => { bottom: '8rem', }); } else { - navigate('/error'); + showToast({ + showingTime: 3000, + message: SERVER_ERROR_MESSAGES.UNHANDLED, + type: 'error', + position: 'bottom', + bottom: '8rem', + }); } } }, [appError]); - return }>{children}; + return {children}; }; export default AppErrorBoundary; diff --git a/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx b/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx index 41de70a2e..42fbcbfea 100644 --- a/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx +++ b/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx @@ -7,6 +7,7 @@ const QueryClientBoundary = ({children}: React.PropsWithChildren) => { queryCache: new QueryCache({ onError: (error: Error) => { updateAppError(error); + throw error; }, }), mutationCache: new MutationCache({ From bc5d57dbb6e3471cb022f884195e7b02d56f905c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 21 Aug 2024 02:29:25 +0900 Subject: [PATCH 16/36] =?UTF-8?q?test:=20AppErrorBoundary=20test=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppErrorBoundary.test.tsx | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx b/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx index b4f4ef02b..f64410a8f 100644 --- a/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx +++ b/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx @@ -31,7 +31,7 @@ describe('AppErrorBoundary', () => { useNavigate: jest.fn(), })); - it('핸들링 가능한 에러인 경우 토스트가 표시된다.', async () => { + it('예상했던 에러인 경우 토스트가 표시된다.', async () => { const errorCode = 'EVENT_NOT_FOUND'; const error = new FetchError({ errorInfo: {errorCode, message: '서버의 에러메세지'}, @@ -58,21 +58,16 @@ describe('AppErrorBoundary', () => { }); }); - // it('핸들링 불가능한 에러인 경우 fallback이 표시된다.', async () => { - // const navigate = useNavigate(); - // (navigate as jest.Mock).mockImplementation(() => {}); - // const {updateAppError} = useAppErrorStore.getState(); + it('예상치 못한 에러인 경우 fallback이 표시된다.', async () => { + const error = new Error('알 수 없는 에러'); + const ErrorThrowingComponent = () => { + throw new Error('Test Error'); + }; + setup(); - // const error = new Error('알 수 없는 에러'); - // setup( updateAppError(error)} />); - - // act(() => { - // screen.getByText('Trigger Error').click(); - // }); - - // // TODO: (@todari) 해결안됨 - // await waitFor(() => { - // expect(navigate).toHaveBeenCalledWith('/error'); - // }); - // }); + // TODO: (@todari) 해결안됨 + await waitFor(() => { + expect(screen.getByText('알 수 없는 오류입니다.')).toBeInTheDocument(); + }); + }); }); From 90229d49da72e8e985d28165c94947d41c17f3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Wed, 21 Aug 2024 02:33:54 +0900 Subject: [PATCH 17/36] =?UTF-8?q?style:=20lint=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 4 ++-- client/src/apis/fetcher.ts | 3 ++- .../AppErrorBoundary/AppErrorBoundary.test.tsx | 11 +++++++---- .../components/AppErrorBoundary/AppErrorBoundary.tsx | 9 ++++++--- .../QueryClientBoundary/QueryClientBoundary.tsx | 3 ++- .../useDeleteMemberAction.test.tsx | 8 ++++---- .../useSearchMemberReportList.test.tsx | 7 ++++--- client/src/pages/CreateEventPage/SetEventNamePage.tsx | 3 ++- client/src/utils/captureError.ts | 2 +- 9 files changed, 30 insertions(+), 20 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index ef750ee07..b0d6f505a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,11 +3,11 @@ import {HDesignProvider} from 'haengdong-design'; import {Global} from '@emotion/react'; import {ToastProvider} from '@hooks/useToast/ToastProvider'; - -import {GlobalStyle} from './GlobalStyle'; import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; +import {GlobalStyle} from './GlobalStyle'; + const App: React.FC = () => { return ( diff --git a/client/src/apis/fetcher.ts b/client/src/apis/fetcher.ts index 7e9c1f231..ac163b93e 100644 --- a/client/src/apis/fetcher.ts +++ b/client/src/apis/fetcher.ts @@ -1,9 +1,10 @@ +import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; + import objectToQueryString from '@utils/objectToQueryString'; import {UNKNOWN_ERROR} from '@constants/errorMessage'; import FetchError from '../errors/FetchError'; -import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; export type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; diff --git a/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx b/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx index f64410a8f..0003b039a 100644 --- a/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx +++ b/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx @@ -1,13 +1,16 @@ import {render, screen, waitFor} from '@testing-library/react'; import {act, ReactNode} from 'react'; +import {MemoryRouter, useNavigate} from 'react-router-dom'; +import {HDesignProvider} from 'haengdong-design'; -import {useAppErrorStore} from '@store/appErrorStore'; import FetchError from '@errors/FetchError'; -import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; import {ToastProvider} from '@hooks/useToast/ToastProvider'; + +import {useAppErrorStore} from '@store/appErrorStore'; + +import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; + import AppErrorBoundary from './AppErrorBoundary'; -import {MemoryRouter, useNavigate} from 'react-router-dom'; -import {HDesignProvider} from 'haengdong-design'; // 테스트용 헬퍼 컴포넌트 const TestComponent = ({triggerError}: {triggerError: () => void}) => { diff --git a/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx b/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx index 6d4ba3d73..9859eb12c 100644 --- a/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx +++ b/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx @@ -1,12 +1,15 @@ import {ErrorBoundary} from 'react-error-boundary'; +import {useEffect} from 'react'; import ErrorPage from '@pages/ErrorPage/ErrorPage'; -import {captureError} from '@utils/captureError'; import FetchError from '@errors/FetchError'; -import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage'; import {useToast} from '@hooks/useToast/useToast'; + import {useAppErrorStore} from '@store/appErrorStore'; -import {useEffect} from 'react'; + +import {captureError} from '@utils/captureError'; + +import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage'; export type ErrorInfo = { errorCode: string; diff --git a/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx b/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx index 42fbcbfea..d6223633b 100644 --- a/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx +++ b/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx @@ -1,6 +1,7 @@ -import {useAppErrorStore} from '@store/appErrorStore'; import {MutationCache, QueryCache, QueryClient, QueryClientProvider} from '@tanstack/react-query'; +import {useAppErrorStore} from '@store/appErrorStore'; + const QueryClientBoundary = ({children}: React.PropsWithChildren) => { const {updateAppError} = useAppErrorStore(); const queryClient = new QueryClient({ diff --git a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx index 702f482b9..05447b53d 100644 --- a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx +++ b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx @@ -1,18 +1,18 @@ import {renderHook, waitFor} from '@testing-library/react'; import {MemoryRouter} from 'react-router-dom'; import {act} from 'react'; +import {HDesignProvider} from 'haengdong-design'; import {BillStep, MemberAction, MemberStep} from 'types/serviceType'; import useRequestGetStepList from '@hooks/queries/useRequestGetStepList'; +import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; +import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; +import {ToastProvider} from '@hooks/useToast/ToastProvider'; import stepListJson from '@mocks/stepList.json'; import invalidMemberStepListJson from '@mocks/invalidMemberStepList.json'; import useDeleteMemberAction from './useDeleteMemberAction'; -import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; -import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; -import {ToastProvider} from '@hooks/useToast/ToastProvider'; -import {HDesignProvider} from 'haengdong-design'; const stepListMockData = stepListJson as (BillStep | MemberStep)[]; let memberActionList: MemberAction[] = []; diff --git a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx b/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx index 51b1b5825..cd8ee8fae 100644 --- a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx +++ b/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx @@ -2,13 +2,14 @@ import {renderHook, waitFor} from '@testing-library/react'; import {MemoryRouter} from 'react-router-dom'; import {QueryClient} from '@tanstack/react-query'; -import reportListJson from '../../mocks/reportList.json'; - -import useSearchMemberReportList from './useSearchMemberReportList'; import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; import {ToastProvider} from '@hooks/useToast/ToastProvider'; +import reportListJson from '../../mocks/reportList.json'; + +import useSearchMemberReportList from './useSearchMemberReportList'; + describe('useSearchMemberReportList', () => { const initializeProvider = (name: string) => renderHook(() => useSearchMemberReportList({name}), { diff --git a/client/src/pages/CreateEventPage/SetEventNamePage.tsx b/client/src/pages/CreateEventPage/SetEventNamePage.tsx index 9eb29b971..f858e5d4b 100644 --- a/client/src/pages/CreateEventPage/SetEventNamePage.tsx +++ b/client/src/pages/CreateEventPage/SetEventNamePage.tsx @@ -2,9 +2,10 @@ import {useNavigate} from 'react-router-dom'; import {FixedButton, MainLayout, LabelInput, Title, TopNav, Back} from 'haengdong-design'; import {css} from '@emotion/react'; -import {ROUTER_URLS} from '@constants/routerUrls'; import useSetEventNamePage from '@hooks/useSetEventNamePage'; +import {ROUTER_URLS} from '@constants/routerUrls'; + const SetEventNamePage = () => { const navigate = useNavigate(); const {eventName, errorMessage, canSubmit, handleEventNameChange} = useSetEventNamePage(); diff --git a/client/src/utils/captureError.ts b/client/src/utils/captureError.ts index bd98b393b..41af89906 100644 --- a/client/src/utils/captureError.ts +++ b/client/src/utils/captureError.ts @@ -1,7 +1,7 @@ import FetchError from '@errors/FetchError'; +import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; import sendLogToSentry from './sendLogToSentry'; -import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; export const captureError = async (error: Error) => { // prod 환경에서만 Sentry capture 실행 From aec4174d6b25c4409d36f5191494a4be84973983 Mon Sep 17 00:00:00 2001 From: pakxe Date: Thu, 22 Aug 2024 18:57:21 +0900 Subject: [PATCH 18/36] =?UTF-8?q?feat:=20App=EC=97=90=20ErrorBoundary=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index b0d6f505a..1cae7b00d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,22 +3,25 @@ import {HDesignProvider} from 'haengdong-design'; import {Global} from '@emotion/react'; import {ToastProvider} from '@hooks/useToast/ToastProvider'; -import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; +import ErrorCatcher from '@components/AppErrorBoundary/ErrorCatcher'; +import UnhandledErrorBoundary from './UnhandledErrorBoudnary'; import {GlobalStyle} from './GlobalStyle'; const App: React.FC = () => { return ( - - - - - - - - + + + + + + + + + + ); }; From 785b82f091fda628b3bf2be1639c5658aa36aef2 Mon Sep 17 00:00:00 2001 From: pakxe Date: Thu, 22 Aug 2024 18:57:34 +0900 Subject: [PATCH 19/36] =?UTF-8?q?feat:=20UnhandledErrorBoundary=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/UnhandledErrorBoudnary.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 client/src/UnhandledErrorBoudnary.tsx diff --git a/client/src/UnhandledErrorBoudnary.tsx b/client/src/UnhandledErrorBoudnary.tsx new file mode 100644 index 000000000..78d017344 --- /dev/null +++ b/client/src/UnhandledErrorBoudnary.tsx @@ -0,0 +1,10 @@ +import {StrictPropsWithChildren} from 'haengdong-design/dist/type/strictPropsWithChildren'; +import {ErrorBoundary} from 'react-error-boundary'; + +import ErrorPage from '@pages/ErrorPage/ErrorPage'; + +const UnhandledErrorBoundary = ({children}: StrictPropsWithChildren) => { + return }>{children}; +}; + +export default UnhandledErrorBoundary; From be305a1795b9d5156943db9f5d55e7c9c148de37 Mon Sep 17 00:00:00 2001 From: pakxe Date: Thu, 22 Aug 2024 18:59:36 +0900 Subject: [PATCH 20/36] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=ED=95=98=EB=A9=B0=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=A7=81=EB=90=98=EB=8A=94=20=EC=97=90=EB=9F=AC=EB=A9=B4=20?= =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8,=20=ED=95=B8=EB=93=A4=EB=A7=81=20?= =?UTF-8?q?=EB=B6=88=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EB=A9=B4=20=EC=97=90=EB=9F=AC=20=EB=B0=94=EC=9A=B4=EB=8D=94?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EB=9D=84=EC=9A=B0=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=BA=90=EC=B2=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppErrorBoundary/ErrorCatcher.tsx | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 client/src/components/AppErrorBoundary/ErrorCatcher.tsx diff --git a/client/src/components/AppErrorBoundary/ErrorCatcher.tsx b/client/src/components/AppErrorBoundary/ErrorCatcher.tsx new file mode 100644 index 000000000..dde5f3d64 --- /dev/null +++ b/client/src/components/AppErrorBoundary/ErrorCatcher.tsx @@ -0,0 +1,62 @@ +import {useEffect} from 'react'; + +import FetchError from '@errors/FetchError'; +import {useToast} from '@hooks/useToast/useToast'; + +import {useAppErrorStore} from '@store/appErrorStore'; + +import {captureError} from '@utils/captureError'; + +import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage'; + +export type ErrorInfo = { + errorCode: string; + message: string; +}; + +const convertAppErrorToErrorInfo = (appError: Error) => { + if (appError instanceof Error) { + const errorInfo = + appError instanceof FetchError ? appError.errorInfo : {errorCode: appError.name, message: appError.message}; + + return errorInfo; + } else { + const errorInfo = {errorCode: UNKNOWN_ERROR, message: JSON.stringify(appError)}; + + return errorInfo; + } +}; + +const isUnhandledError = (errorInfo: ErrorInfo) => { + if (errorInfo.errorCode === 'INTERNAL_SERVER_ERROR') return true; + + return SERVER_ERROR_MESSAGES[errorInfo.errorCode] === undefined; +}; + +const ErrorCatcher = ({children}: React.PropsWithChildren) => { + const {appError} = useAppErrorStore(); + const {showToast} = useToast(); + + useEffect(() => { + if (appError) { + const errorInfo = convertAppErrorToErrorInfo(appError); + captureError(appError, errorInfo); + + if (!isUnhandledError(errorInfo)) { + showToast({ + showingTime: 3000, + message: SERVER_ERROR_MESSAGES[errorInfo.errorCode], + type: 'error', + position: 'bottom', + bottom: '8rem', + }); + } else { + throw appError; + } + } + }, [appError]); + + return children; +}; + +export default ErrorCatcher; From facd363db099d493e145b5beb8b5ee6465df0f2e Mon Sep 17 00:00:00 2001 From: pakxe Date: Thu, 22 Aug 2024 19:00:42 +0900 Subject: [PATCH 21/36] =?UTF-8?q?feat:=20=EC=97=90=EB=9F=AC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=20=EB=A9=94=EC=9D=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/ErrorPage/ErrorPage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/pages/ErrorPage/ErrorPage.tsx b/client/src/pages/ErrorPage/ErrorPage.tsx index f71f5da8e..31ef8c81f 100644 --- a/client/src/pages/ErrorPage/ErrorPage.tsx +++ b/client/src/pages/ErrorPage/ErrorPage.tsx @@ -1,10 +1,12 @@ import {MainLayout, Title} from 'haengdong-design'; -// TODO: (@weadie) 임시 에러 페이지입니다. const ErrorPage = () => { return ( - + <Title + title="알 수 없는 오류입니다." + description="오류가 난 상황에 대해 haengdongdj@gmail.com 로 연락주시면 소정의 상품을 드립니다." + /> </MainLayout> ); }; From eb81923eb8d4f497988f9c105a014190efd277cd Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 19:01:20 +0900 Subject: [PATCH 22/36] =?UTF-8?q?feat:=20errorInfo=EB=A5=BC=20=EC=95=88?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B5=AC=ED=98=84=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B2=83=EC=9D=B4=20=EC=95=84=EB=8B=8C=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=EB=90=9C=20=EA=B2=83=EC=9D=84=20=EC=9D=B8=EC=9E=90=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/utils/captureError.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/src/utils/captureError.ts b/client/src/utils/captureError.ts index 41af89906..67a30ac99 100644 --- a/client/src/utils/captureError.ts +++ b/client/src/utils/captureError.ts @@ -1,15 +1,11 @@ -import FetchError from '@errors/FetchError'; -import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; +import {ErrorInfo} from '@components/AppErrorBoundary/ErrorCatcher'; import sendLogToSentry from './sendLogToSentry'; -export const captureError = async (error: Error) => { +export const captureError = async (error: Error, errorInfo: ErrorInfo) => { // prod 환경에서만 Sentry capture 실행 if (process.env.NODE_ENV !== 'production') return; - const errorInfo: ErrorInfo = - error instanceof FetchError ? error.errorInfo : {message: error.message, errorCode: error.name}; - switch (errorInfo?.errorCode) { case 'INTERNAL_SERVER_ERROR': sendLogToSentry({error, errorInfo, level: 'fatal'}); From 39cc30b39212b464b04db402ceedb20676411918 Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 20:22:52 +0900 Subject: [PATCH 23/36] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?import=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/apis/fetcher.ts | 2 +- .../src/components/AppErrorBoundary/AppErrorBoundary.test.tsx | 2 +- .../src/components/QueryClientBoundary/QueryClientBoundary.tsx | 1 - .../hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx | 2 +- .../useSearchMemberReportList.test.tsx | 2 +- client/src/types/fetchErrorType.ts | 2 +- client/src/utils/sendLogToSentry.ts | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/client/src/apis/fetcher.ts b/client/src/apis/fetcher.ts index ac163b93e..2e1c2d067 100644 --- a/client/src/apis/fetcher.ts +++ b/client/src/apis/fetcher.ts @@ -1,4 +1,4 @@ -import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; +import {ErrorInfo} from '@components/AppErrorBoundary/ErrorCatcher'; import objectToQueryString from '@utils/objectToQueryString'; diff --git a/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx b/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx index 0003b039a..963036ca7 100644 --- a/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx +++ b/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx @@ -10,7 +10,7 @@ import {useAppErrorStore} from '@store/appErrorStore'; import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; -import AppErrorBoundary from './AppErrorBoundary'; +import AppErrorBoundary from './ErrorCatcher'; // 테스트용 헬퍼 컴포넌트 const TestComponent = ({triggerError}: {triggerError: () => void}) => { diff --git a/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx b/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx index d6223633b..b60cdb692 100644 --- a/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx +++ b/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx @@ -8,7 +8,6 @@ const QueryClientBoundary = ({children}: React.PropsWithChildren) => { queryCache: new QueryCache({ onError: (error: Error) => { updateAppError(error); - throw error; }, }), mutationCache: new MutationCache({ diff --git a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx index 05447b53d..b2f2d04dc 100644 --- a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx +++ b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx @@ -5,7 +5,7 @@ import {HDesignProvider} from 'haengdong-design'; import {BillStep, MemberAction, MemberStep} from 'types/serviceType'; import useRequestGetStepList from '@hooks/queries/useRequestGetStepList'; -import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; +import AppErrorBoundary from '@components/AppErrorBoundary/ErrorCatcher'; import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; import {ToastProvider} from '@hooks/useToast/ToastProvider'; diff --git a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx b/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx index cd8ee8fae..f7a577b68 100644 --- a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx +++ b/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx @@ -2,7 +2,7 @@ import {renderHook, waitFor} from '@testing-library/react'; import {MemoryRouter} from 'react-router-dom'; import {QueryClient} from '@tanstack/react-query'; -import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; +import AppErrorBoundary from '@components/AppErrorBoundary/ErrorCatcher'; import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; import {ToastProvider} from '@hooks/useToast/ToastProvider'; diff --git a/client/src/types/fetchErrorType.ts b/client/src/types/fetchErrorType.ts index 664fa368b..19fe8ee6f 100644 --- a/client/src/types/fetchErrorType.ts +++ b/client/src/types/fetchErrorType.ts @@ -1,4 +1,4 @@ -import {ErrorInfo} from '@hooks/useError/ErrorProvider'; +import {ErrorInfo} from '@components/AppErrorBoundary/ErrorCatcher'; import {Method} from '@apis/fetcher'; diff --git a/client/src/utils/sendLogToSentry.ts b/client/src/utils/sendLogToSentry.ts index bba09ed34..22d736b6c 100644 --- a/client/src/utils/sendLogToSentry.ts +++ b/client/src/utils/sendLogToSentry.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/react'; -import {ErrorInfo} from '@components/AppErrorBoundary/AppErrorBoundary'; +import {ErrorInfo} from '@components/AppErrorBoundary/ErrorCatcher'; import {UNKNOWN_ERROR} from '@constants/errorMessage'; From 727bb81ab0fa8f64952c5e97a1e5f8ebbe6d0dde Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 20:23:07 +0900 Subject: [PATCH 24/36] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=B4=EC=A7=84=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppErrorBoundary/AppErrorBoundary.tsx | 95 ------------------- client/src/hooks/useError/ErrorProvider.tsx | 73 -------------- 2 files changed, 168 deletions(-) delete mode 100644 client/src/components/AppErrorBoundary/AppErrorBoundary.tsx delete mode 100644 client/src/hooks/useError/ErrorProvider.tsx diff --git a/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx b/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx deleted file mode 100644 index 9859eb12c..000000000 --- a/client/src/components/AppErrorBoundary/AppErrorBoundary.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import {ErrorBoundary} from 'react-error-boundary'; -import {useEffect} from 'react'; - -import ErrorPage from '@pages/ErrorPage/ErrorPage'; -import FetchError from '@errors/FetchError'; -import {useToast} from '@hooks/useToast/useToast'; - -import {useAppErrorStore} from '@store/appErrorStore'; - -import {captureError} from '@utils/captureError'; - -import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage'; - -export type ErrorInfo = { - errorCode: string; - message: string; -}; - -type FallbackComponentProps = { - error: Error; -}; - -const convertAppErrorToErrorInfo = (appError: Error) => { - if (appError instanceof Error) { - const errorInfo = - appError instanceof FetchError ? appError.errorInfo : {errorCode: appError.name, message: appError.message}; - - return errorInfo; - } else { - const errorInfo = {errorCode: UNKNOWN_ERROR, message: JSON.stringify(appError)}; - - return errorInfo; - } -}; - -const isUnhandledError = (errorInfo: ErrorInfo) => { - if (errorInfo.errorCode === 'INTERNAL_SERVER_ERROR') return true; - - return SERVER_ERROR_MESSAGES[errorInfo.errorCode] === undefined; -}; - -const AppErrorBoundary = ({children}: React.PropsWithChildren) => { - const {appError} = useAppErrorStore(); - const {showToast} = useToast(); - - const fallbackComponent = ({error}: FallbackComponentProps) => { - if (error) { - console.log(Error); - captureError(error instanceof FetchError ? error : new Error(UNKNOWN_ERROR)); - const errorInfo = convertAppErrorToErrorInfo(error); - if (!isUnhandledError(errorInfo)) { - showToast({ - showingTime: 3000, - message: SERVER_ERROR_MESSAGES[errorInfo.errorCode], - type: 'error', - position: 'bottom', - bottom: '8rem', - }); - } else { - return <ErrorPage />; - } - } - - return null; - }; - - useEffect(() => { - if (appError) { - captureError(appError instanceof FetchError ? appError : new Error(UNKNOWN_ERROR)); - const errorInfo = convertAppErrorToErrorInfo(appError); - - if (!isUnhandledError(errorInfo)) { - showToast({ - showingTime: 3000, - message: SERVER_ERROR_MESSAGES[errorInfo.errorCode], - type: 'error', - position: 'bottom', - bottom: '8rem', - }); - } else { - showToast({ - showingTime: 3000, - message: SERVER_ERROR_MESSAGES.UNHANDLED, - type: 'error', - position: 'bottom', - bottom: '8rem', - }); - } - } - }, [appError]); - - return <ErrorBoundary FallbackComponent={fallbackComponent}>{children}</ErrorBoundary>; -}; - -export default AppErrorBoundary; diff --git a/client/src/hooks/useError/ErrorProvider.tsx b/client/src/hooks/useError/ErrorProvider.tsx deleted file mode 100644 index 945d3b9af..000000000 --- a/client/src/hooks/useError/ErrorProvider.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// import {useState, useEffect, createContext, ReactNode} from 'react'; - -// import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage'; -// import {useAppErrorStore} from '@store/appErrorStore'; -// import FetchError from '@errors/FetchError'; -// import {captureError} from '@utils/captureError'; -// import ErrorPage from '@pages/ErrorPage/ErrorPage'; -// import {useToast} from '@hooks/useToast/useToast'; -// import {ErrorBoundary} from 'react-error-boundary'; - -// export type ErrorInfo = { -// errorCode: string; -// message: string; -// }; - -// export interface ErrorContextType { -// // clientErrorMessage: string; -// // setErrorInfo: (error: ErrorInfo) => void; -// // clearError: (ms?: number) => void; -// // errorInfo: ErrorInfo | null; -// setAppError: React.Dispatch<React.SetStateAction<Error | null>>; -// } - -// export const ErrorContext = createContext<ErrorContextType | undefined>(undefined); - -// export const ErrorProvider = ({children}: React.PropsWithChildren) => { -// const [appError, setAppError] = useState<Error | null>(null); -// const [errorInfo, setErrorInfo] = useState<ErrorInfo | null>(null); -// const {showToast} = useToast(); - -// useEffect(() => { -// const catchAppError = () => { -// if (appError instanceof Error) { -// const errorInfo = -// appError instanceof FetchError ? appError.errorInfo : {errorCode: appError.name, message: appError.message}; -// setErrorInfo(errorInfo); -// captureError(appError); -// } else { -// setErrorInfo({errorCode: UNKNOWN_ERROR, message: JSON.stringify(appError)}); -// captureError(new Error(UNKNOWN_ERROR)); -// } -// }; - -// if (appError) { -// catchAppError(); -// } -// }, [appError]); - -// useEffect(() => { -// if (errorInfo) { -// if (isUnhandledError(errorInfo.errorCode)) { -// // 에러바운더리로 보내기 -// // throw new Error(errorInfo.message); -// } else { -// showToast({ -// showingTime: 3000, -// message: errorInfo.message, -// type: 'error', -// position: 'bottom', -// bottom: '8rem', -// }); -// } -// } -// }, [errorInfo]); - -// const isUnhandledError = (errorCode: string) => { -// if (errorCode === 'INTERNAL_SERVER_ERROR') return true; - -// return SERVER_ERROR_MESSAGES[errorCode] === undefined; -// }; - -// return <ErrorContext.Provider value={{setAppError}}>{children}</ErrorContext.Provider>; -// }; From 926232647119b6df330a315dcb7ba591362c2d5f Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 20:47:23 +0900 Subject: [PATCH 25/36] =?UTF-8?q?chore:=20=EA=B0=9C=ED=96=89=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/QueryClientBoundary/QueryClientBoundary.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx b/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx index b60cdb692..aeec24c3a 100644 --- a/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx +++ b/client/src/components/QueryClientBoundary/QueryClientBoundary.tsx @@ -4,6 +4,7 @@ import {useAppErrorStore} from '@store/appErrorStore'; const QueryClientBoundary = ({children}: React.PropsWithChildren) => { const {updateAppError} = useAppErrorStore(); + const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (error: Error) => { @@ -16,6 +17,7 @@ const QueryClientBoundary = ({children}: React.PropsWithChildren) => { }, }), }); + return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>; }; From fff00adf043fd38205f5ec3491e2291441a687d6 Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 21:43:55 +0900 Subject: [PATCH 26/36] =?UTF-8?q?feat:=20ErrorCatcher=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppErrorBoundary/ErrorCatcher.test.tsx | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx diff --git a/client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx b/client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx new file mode 100644 index 000000000..edc33567c --- /dev/null +++ b/client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx @@ -0,0 +1,92 @@ +import {render, screen, waitFor} from '@testing-library/react'; +import {act, ReactNode} from 'react'; +import {MemoryRouter} from 'react-router-dom'; +import {HDesignProvider} from 'haengdong-design'; + +import FetchError from '@errors/FetchError'; +import {ToastProvider} from '@hooks/useToast/ToastProvider'; + +import {useAppErrorStore} from '@store/appErrorStore'; + +import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; + +import UnhandledErrorBoundary from '../../UnhandledErrorBoudnary'; + +import ErrorCatcher from './ErrorCatcher'; + +// 테스트용 헬퍼 컴포넌트 +const TestComponent = ({triggerError}: {triggerError: () => void}) => { + return <button onClick={triggerError}>Trigger Error</button>; +}; + +const setup = (ui: ReactNode) => + render( + <HDesignProvider> + <UnhandledErrorBoundary> + <ToastProvider> + <ErrorCatcher> + <MemoryRouter>{ui}</MemoryRouter> + </ErrorCatcher> + </ToastProvider> + </UnhandledErrorBoundary> + </HDesignProvider>, + ); + +describe('ErrorCatcher', () => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), + })); + + it('핸들링 가능한 에러인 경우 토스트가 표시된다.', async () => { + const errorCode = 'EVENT_NOT_FOUND'; + const error = new FetchError({ + errorInfo: {errorCode, message: '서버의 에러메세지'}, + name: errorCode, + message: '에러메세지', + status: 200, + endpoint: '', + method: 'GET', + requestBody: '', + }); + + const {updateAppError} = useAppErrorStore.getState(); + + setup(<TestComponent triggerError={() => updateAppError(error)} />); + + act(() => { + screen.getByText('Trigger Error').click(); + }); + + const errorMessage = SERVER_ERROR_MESSAGES[errorCode]; + + await waitFor(() => { + expect(screen.getByText(errorMessage)).toBeInTheDocument(); + }); + }); + + it('핸들링 불가능한 에러인 경우 에러 바운더리가 표시된다.', async () => { + const errorCode = '모르겠는 에러'; + const error = new FetchError({ + errorInfo: {errorCode, message: '모르겠는 에러메세지'}, + name: errorCode, + message: '에러메세지', + status: 400, + endpoint: '', + method: 'GET', + requestBody: '', + }); + + const {updateAppError} = useAppErrorStore.getState(); + + setup(<TestComponent triggerError={() => updateAppError(error)} />); + + act(() => { + screen.getByText('Trigger Error').click(); + }); + + await waitFor(() => { + expect(screen.getByText('알 수 없는 오류입니다.')).toBeInTheDocument(); + }); + }); +}); From ed5e847ea85e47d06d7f65e2be2c89d5d9c97570 Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 21:44:30 +0900 Subject: [PATCH 27/36] =?UTF-8?q?feat:=20Toast=EC=9D=98=20showingTime=20?= =?UTF-8?q?=EC=9D=84=20=EC=98=B5=EC=85=94=EB=84=90=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Toast/Toast.tsx | 2 +- client/src/components/Toast/Toast.type.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/Toast/Toast.tsx b/client/src/components/Toast/Toast.tsx index edb17400d..5bf44d6fb 100644 --- a/client/src/components/Toast/Toast.tsx +++ b/client/src/components/Toast/Toast.tsx @@ -19,7 +19,7 @@ const Toast = ({ bottom = '0px', isClickToClose = true, position = 'bottom', - showingTime, + showingTime = 3000, message, onUndo, onClose, diff --git a/client/src/components/Toast/Toast.type.ts b/client/src/components/Toast/Toast.type.ts index 31ec64e3d..20b92c0db 100644 --- a/client/src/components/Toast/Toast.type.ts +++ b/client/src/components/Toast/Toast.type.ts @@ -12,7 +12,7 @@ export interface ToastOptionProps { onUndo?: () => void; isClickToClose?: boolean; onClose?: () => void; - showingTime: number; + showingTime?: number; } export interface ToastRequiredProps { From fd723ff1acc12735d3cee8e471d7a3d80b472a4d Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 21:44:50 +0900 Subject: [PATCH 28/36] =?UTF-8?q?chore:=20=EC=97=86=EC=96=B4=EC=A7=84=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A5=BC=20renderHook?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useMemberReportListInAction.test.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx b/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx index 4d94804f4..2754a140d 100644 --- a/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx +++ b/client/src/hooks/useMemberReportListInAction/useMemberReportListInAction.test.tsx @@ -5,7 +5,6 @@ import {MemoryRouter} from 'react-router-dom'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import memberReportListInActionJson from '../../mocks/memberReportListInAction.json'; -import {ErrorProvider} from '../useError/ErrorProvider'; import useMemberReportListInAction from './useMemberReportListInAction'; @@ -22,9 +21,7 @@ describe('useMemberReportListInActionTest', () => { renderHook(() => useMemberReportListInAction(actionId, totalPrice, () => {}), { wrapper: ({children}) => ( <QueryClientProvider client={queryClient}> - <MemoryRouter> - <ErrorProvider>{children}</ErrorProvider> - </MemoryRouter> + <MemoryRouter>{children}</MemoryRouter> </QueryClientProvider> ), }); From 4c2fee253400d6be611f6b65c9893ef55f032ab5 Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 21:45:08 +0900 Subject: [PATCH 29/36] =?UTF-8?q?refactor:=20return=EB=AC=B8=EC=9D=B4=20?= =?UTF-8?q?=EB=B0=98=EB=B3=B5=EB=90=98=EB=8A=94=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=9D=84=20=EB=A6=AC=ED=8E=99=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/hooks/useToast/ToastProvider.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/src/hooks/useToast/ToastProvider.tsx b/client/src/hooks/useToast/ToastProvider.tsx index 037ba98b6..be0d2ea15 100644 --- a/client/src/hooks/useToast/ToastProvider.tsx +++ b/client/src/hooks/useToast/ToastProvider.tsx @@ -29,12 +29,8 @@ export const ToastProvider = ({children}: React.PropsWithChildren) => { }; useEffect(() => { - if (!currentToast) return; - - if (!currentToast.isAlwaysOn) { - const timer = setTimeout(() => { - setCurrentToast(null); - }, currentToast.showingTime); + if (currentToast && !currentToast.isAlwaysOn) { + const timer = setTimeout(() => setCurrentToast(null), currentToast.showingTime); return () => clearTimeout(timer); } From 973e4eb42e84faea3abfbd178bc7c6d19c5e8a6b Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 21:45:20 +0900 Subject: [PATCH 30/36] =?UTF-8?q?feat:=20useToast=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/hooks/useToast/useToast.test.tsx | 179 +++++++++----------- 1 file changed, 80 insertions(+), 99 deletions(-) diff --git a/client/src/hooks/useToast/useToast.test.tsx b/client/src/hooks/useToast/useToast.test.tsx index 86da9bf53..d46f20846 100644 --- a/client/src/hooks/useToast/useToast.test.tsx +++ b/client/src/hooks/useToast/useToast.test.tsx @@ -1,99 +1,80 @@ -// import {render, screen, waitFor} from '@testing-library/react'; -// import {act, ReactNode} from 'react'; -// import {HDesignProvider} from 'haengdong-design'; - -// import {useError} from '@hooks/useError/useError'; - -// import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; - -// import UnhandledErrorBoundary from '../../UnhandledErrorBoundary'; -// import {ErrorInfo, ErrorProvider} from '../../hooks/useError/ErrorProvider'; // useError 경로에 맞게 설정 - -// import {ToastProvider} from './ToastProvider'; // 위 코드에 해당하는 ToastProvider 경로 - -// // 테스트용 헬퍼 컴포넌트 -// const TestComponent = ({errorInfo}: {errorInfo: ErrorInfo}) => { -// const {setErrorInfo} = useError(); - -// // 테스트에서 직접 에러를 설정합니다. -// const triggerError = () => { -// setErrorInfo(errorInfo); -// }; - -// return <button onClick={triggerError}>Trigger Error</button>; -// }; - -// const setup = (ui: ReactNode) => -// render( -// <HDesignProvider> -// <UnhandledErrorBoundary> -// <ErrorProvider> -// <ToastProvider>{ui}</ToastProvider> -// </ErrorProvider> -// </UnhandledErrorBoundary> -// </HDesignProvider>, -// ); - -// beforeEach(() => { -// jest.useFakeTimers(); -// }); - -// afterEach(() => { -// jest.useRealTimers(); -// }); - -// describe('useToast', () => { -// describe('error의 경우 자동으로 토스트를 띄워준다.', () => { -// it('핸들링 가능한 에러인 경우 토스트가 뜬다.', async () => { -// const errorCode = 'ACTION_NOT_FOUND'; - -// setup( -// <TestComponent -// errorInfo={{ -// errorCode, -// message: '서버의 에러메세지', -// }} -// />, -// ); -// const errorMessage = SERVER_ERROR_MESSAGES[errorCode]; - -// act(() => { -// // 에러 트리거 버튼을 클릭 -// screen.getByText('Trigger Error').click(); -// }); - -// // 토스트가 표시되는지 확인 -// await waitFor(() => { -// expect(screen.getByText(errorMessage)).toBeInTheDocument(); -// }); - -// // 타이머가 지나서 토스트가 사라지는지 확인 -// jest.runAllTimers(); // Jest의 타이머를 실행 -// await waitFor(() => { -// expect(screen.queryByText(errorMessage)).not.toBeInTheDocument(); -// }); -// }); - -// it('핸들링 불가능한 에러인 경우 토스트가 안뜬다.', async () => { -// const errorCode = '핸들링이 안되는 에러 코드'; - -// setup( -// <TestComponent -// errorInfo={{ -// errorCode, -// message: '핸들링이 안되는 에러 메세지', -// }} -// />, -// ); - -// act(() => { -// // 에러 트리거 버튼을 클릭 -// screen.getByText('Trigger Error').click(); -// }); - -// await waitFor(() => { -// expect(document.getElementById('toast')).not.toBeInTheDocument(); -// }); -// }); -// }); -// }); +import {render, renderHook, screen, waitFor} from '@testing-library/react'; +import {act} from 'react'; +import {HDesignProvider} from 'haengdong-design'; + +import {ToastProvider} from './ToastProvider'; +import {useToast} from './useToast'; + +const TOAST_CONFIG = { + message: 'Test Toast Message', +}; + +// 테스트용 컴포넌트: 토스트를 표시하기 위한 버튼 제공 +const TestComponent = () => { + const {showToast} = useToast(); + + const handleClick = () => { + showToast(TOAST_CONFIG); + }; + + return <button onClick={handleClick}>Show Toast</button>; +}; + +const setup = () => + render( + <HDesignProvider> + <ToastProvider> + <TestComponent /> + </ToastProvider> + </HDesignProvider>, + ); + +describe('ToastProvider', () => { + it('토스트를 띄우고 자동으로 사라지게 한다', async () => { + setup(); + + // 토스트를 띄우기 위해 버튼 클릭 + act(() => { + screen.getByText('Show Toast').click(); + }); + + // 토스트 메시지가 나타나는지 확인 + expect(screen.getByText(TOAST_CONFIG.message)).toBeInTheDocument(); + + // 1초 후에 토스트 메시지가 사라지는지 확인 + await waitFor( + () => { + expect(screen.queryByText(TOAST_CONFIG.message)).not.toBeInTheDocument(); + }, + {timeout: 3100}, + ); // 타임아웃을 3100ms로 설정하여 정확히 3초 후 확인 + }); + + it('토스트 닫기 버튼을 눌렀을 때 사라진다', async () => { + setup(); + + // 토스트를 띄우기 위해 버튼 클릭 + act(() => { + screen.getByText('Show Toast').click(); + }); + + // 토스트 메시지가 나타나는지 확인 + expect(screen.getByText(TOAST_CONFIG.message)).toBeInTheDocument(); + + // 토스트의 닫기 버튼을 클릭 + act(() => { + document.getElementById('toast')?.click(); + }); + + // 닫기 버튼을 클릭한 후 토스트가 사라지는지 확인 + await waitFor(() => { + expect(screen.queryByText(TOAST_CONFIG.message)).not.toBeInTheDocument(); + }); + }); + + it('Provider없이 useToast 사용할 경우 에러를 던진다.', () => { + expect(() => { + const _ = renderHook(() => useToast()); + }).toThrow('useToast는 ToastProvider 내에서 사용되어야 합니다.'); + }); +}); From 2fe3e976f4deef5c8a53b384461068820a1738f9 Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 21:45:36 +0900 Subject: [PATCH 31/36] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AppErrorBoundary.test.tsx | 76 --------- client/src/hooks/useAuth/useAuth.test.tsx | 147 ----------------- client/src/hooks/useAuth/useAuth.tsx | 21 --- client/src/hooks/useError/useError.test.tsx | 121 -------------- client/src/hooks/useError/useError.tsx | 12 -- client/src/hooks/useEvent/useEvent.test.tsx | 79 --------- client/src/hooks/useEvent/useEvent.tsx | 16 -- client/src/hooks/useFetch/useFetch.test.tsx | 155 ------------------ client/src/hooks/useFetch/useFetch.ts | 63 ------- 9 files changed, 690 deletions(-) delete mode 100644 client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx delete mode 100644 client/src/hooks/useAuth/useAuth.test.tsx delete mode 100644 client/src/hooks/useAuth/useAuth.tsx delete mode 100644 client/src/hooks/useError/useError.test.tsx delete mode 100644 client/src/hooks/useError/useError.tsx delete mode 100644 client/src/hooks/useEvent/useEvent.test.tsx delete mode 100644 client/src/hooks/useEvent/useEvent.tsx delete mode 100644 client/src/hooks/useFetch/useFetch.test.tsx delete mode 100644 client/src/hooks/useFetch/useFetch.ts diff --git a/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx b/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx deleted file mode 100644 index 963036ca7..000000000 --- a/client/src/components/AppErrorBoundary/AppErrorBoundary.test.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import {render, screen, waitFor} from '@testing-library/react'; -import {act, ReactNode} from 'react'; -import {MemoryRouter, useNavigate} from 'react-router-dom'; -import {HDesignProvider} from 'haengdong-design'; - -import FetchError from '@errors/FetchError'; -import {ToastProvider} from '@hooks/useToast/ToastProvider'; - -import {useAppErrorStore} from '@store/appErrorStore'; - -import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; - -import AppErrorBoundary from './ErrorCatcher'; - -// 테스트용 헬퍼 컴포넌트 -const TestComponent = ({triggerError}: {triggerError: () => void}) => { - return <button onClick={triggerError}>Trigger Error</button>; -}; - -const setup = (ui: ReactNode) => - render( - <HDesignProvider> - <ToastProvider> - <AppErrorBoundary> - <MemoryRouter>{ui}</MemoryRouter> - </AppErrorBoundary> - </ToastProvider> - </HDesignProvider>, - ); - -describe('AppErrorBoundary', () => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: jest.fn(), - })); - - it('예상했던 에러인 경우 토스트가 표시된다.', async () => { - const errorCode = 'EVENT_NOT_FOUND'; - const error = new FetchError({ - errorInfo: {errorCode, message: '서버의 에러메세지'}, - name: errorCode, - message: '에러메세지', - status: 200, - endpoint: '', - method: 'GET', - requestBody: '', - }); - - const {updateAppError} = useAppErrorStore.getState(); - - setup(<TestComponent triggerError={() => updateAppError(error)} />); - - act(() => { - screen.getByText('Trigger Error').click(); - }); - - const errorMessage = SERVER_ERROR_MESSAGES[errorCode]; - - await waitFor(() => { - expect(screen.getByText(errorMessage)).toBeInTheDocument(); - }); - }); - - it('예상치 못한 에러인 경우 fallback이 표시된다.', async () => { - const error = new Error('알 수 없는 에러'); - const ErrorThrowingComponent = () => { - throw new Error('Test Error'); - }; - setup(<ErrorThrowingComponent />); - - // TODO: (@todari) 해결안됨 - await waitFor(() => { - expect(screen.getByText('알 수 없는 오류입니다.')).toBeInTheDocument(); - }); - }); -}); diff --git a/client/src/hooks/useAuth/useAuth.test.tsx b/client/src/hooks/useAuth/useAuth.test.tsx deleted file mode 100644 index 1c8f1f07e..000000000 --- a/client/src/hooks/useAuth/useAuth.test.tsx +++ /dev/null @@ -1,147 +0,0 @@ -// import {renderHook, waitFor} from '@testing-library/react'; -// import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; -// import {act} from 'react'; -// import {MemoryRouter} from 'react-router-dom'; - -// import {useError} from '@hooks/useError/useError'; - -// import {PASSWORD_LENGTH} from '@constants/password'; - -// import {VALID_PASSWORD_FOR_TEST, VALID_TOKEN_FOR_TEST} from '@mocks/validValueForTest'; - -// import {ErrorProvider} from '../useError/ErrorProvider'; - -// import useAuth from './useAuth'; - -// describe('useAuth', () => { -// const queryClient = new QueryClient({ -// defaultOptions: { -// queries: { -// retry: 0, -// }, -// }, -// }); -// const initializeProvider = () => -// renderHook( -// () => { -// return {errorResult: useError(), authResult: useAuth()}; -// }, -// { -// wrapper: ({children}) => ( -// <QueryClientProvider client={queryClient}> -// <MemoryRouter> -// <ErrorProvider>{children}</ErrorProvider> -// </MemoryRouter> -// </QueryClientProvider> -// ), -// }, -// ); - -// describe('auth', () => { -// it('쿠키에 토큰이 담겨있지 않다면 인증이 실패한다', async () => { -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.authResult.checkAuthentication()); -// }); - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_NOT_FOUND'); -// }); -// }); - -// it('쿠키에 담겨있는 토큰이 올바르다면 인증에 성공한다', async () => { -// document.cookie = `eventToken=${VALID_TOKEN_FOR_TEST}`; - -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.authResult.checkAuthentication()); -// }); - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo).toBe(null); -// }); -// }); - -// it('쿠키에 담겨있는 토큰이 유효하지 않다면 인증에 실패한다.', async () => { -// document.cookie = 'eventToken=fake-token'; - -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.authResult.checkAuthentication()); -// }); - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_INVALID'); -// }); -// }); - -// it('쿠키에 담겨있는 토큰이 만료되었다면 인증에 실패한다.', async () => { -// document.cookie = 'eventToken=expired-token'; - -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.authResult.checkAuthentication()); -// }); - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_EXPIRED'); -// }); -// }); - -// it('쿠키에 담겨있는 토큰이 forbidden이라면 인증에 실패한다.', async () => { -// document.cookie = 'eventToken=forbidden-token'; - -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.authResult.checkAuthentication()); -// }); - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('FORBIDDEN'); -// }); -// }); -// }); - -// describe('login', () => { -// it('비밀 번호가 올바르다면 로그인이 성공한다.', async () => { -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.authResult.loginUser({password: String(VALID_PASSWORD_FOR_TEST)})); -// }); - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo).toBe(null); -// }); -// }); - -// it(`비밀 번호가 ${PASSWORD_LENGTH}자리가 아니라면 로그인에 실패한다.`, async () => { -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.authResult.loginUser({password: '111'})); -// }); - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); -// }); -// }); - -// it('비밀 번호가 틀렸다면 로그인에 실패한다.', async () => { -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.authResult.loginUser({password: '9999'})); -// }); - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('PASSWORD_INVALID'); -// }); -// }); -// }); -// }); diff --git a/client/src/hooks/useAuth/useAuth.tsx b/client/src/hooks/useAuth/useAuth.tsx deleted file mode 100644 index 45bef3196..000000000 --- a/client/src/hooks/useAuth/useAuth.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// import {RequestToken, requestPostAuthentication, requestPostToken} from '@apis/request/auth'; -// import {useFetch} from '@hooks/useFetch/useFetch'; - -// import getEventIdByUrl from '@utils/getEventIdByUrl'; - -// const useAuth = () => { -// const {fetch} = useFetch(); -// const eventId = getEventIdByUrl(); - -// const checkAuthentication = async () => { -// return await fetch({queryFunction: () => requestPostAuthentication({eventId})}); -// }; - -// const loginUser = async ({password}: RequestToken) => { -// return await fetch({queryFunction: () => requestPostToken({eventId, password})}); -// }; - -// return {checkAuthentication, loginUser}; -// }; - -// export default useAuth; diff --git a/client/src/hooks/useError/useError.test.tsx b/client/src/hooks/useError/useError.test.tsx deleted file mode 100644 index b10860d37..000000000 --- a/client/src/hooks/useError/useError.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -// import {renderHook, waitFor} from '@testing-library/react'; -// import {MemoryRouter} from 'react-router-dom'; -// import {act} from 'react'; -// import {HDesignProvider} from 'haengdong-design'; - -// import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; - -// import UnhandledErrorBoundary from '../../UnhandledErrorBoundary'; - -// import {ErrorInfo, ErrorProvider} from './ErrorProvider'; -// import {useError} from './useError'; - -// describe('useError', () => { -// const initializeProvider = () => -// renderHook(() => useError(), { -// wrapper: ({children}) => ( -// <HDesignProvider> -// <UnhandledErrorBoundary> -// <MemoryRouter> -// <ErrorProvider>{children}</ErrorProvider> -// </MemoryRouter> -// </UnhandledErrorBoundary> -// </HDesignProvider> -// ), -// }); - -// /** -// * useError, ErrorProvider, UnhandledErrorBoundary에서 사용되는 `핸들링 가능한 에러`의 정의 -// * -// * : 서버에서 미리 정의한 에러 코드와 에러 메세지를 던져주는 경우 이를 `핸들링 가능한 에러`로 합니다. -// * 다만 예외적으로 INTERNAL_SERVER_ERROR는 핸들링 `불가능`한 에러로 합니다. -// */ -// const errorCode = 'EVENT_NOT_FOUND'; -// const errorInfo: ErrorInfo = {errorCode, message: '메세지입니다.'}; -// const expectedClientErrorMessage = SERVER_ERROR_MESSAGES[errorCode]; - -// describe('저장된 에러를 초기화한다.', () => { -// it('에러 초기화 함수에 인자를 넘겨주지 않은 경우 바로 에러를 초기화한다.', async () => { -// const {result} = initializeProvider(); - -// await act(async () => result.current.setErrorInfo(errorInfo)); - -// // 에러 메세지가 세팅되기 까지 대기 (없어도 통과하나 제대로 값이 들어간 후 초기화됨을 확인하기 위함) -// await waitFor(() => { -// expect(result.current.clientErrorMessage).toEqual(expectedClientErrorMessage); -// }); - -// await act(async () => result.current.clearError()); - -// await waitFor(() => expect(result.current.errorInfo).toBe(null)); -// }); - -// it('저장된 에러가 없는데 초기화 함수를 호출할 경우 그냥 종료한다.', async () => { -// const {result} = initializeProvider(); - -// await act(async () => result.current.clearError()); - -// await waitFor(() => expect(result.current.errorInfo).toBe(null)); -// }); -// }); - -// describe('핸들링 가능한 에러', () => { -// it('핸들링 가능한 에러인 경우 에러 메세지를 미리 정의된 에러 메세지로 세팅한다.', async () => { -// const {result} = initializeProvider(); - -// await act(async () => result.current.setErrorInfo(errorInfo)); - -// await waitFor(() => expect(result.current.clientErrorMessage).toEqual(expectedClientErrorMessage)); -// }); -// }); - -// describe('핸들링 불가능한 에러', () => { -// it('에러 코드가 INTERNAL_SERVER_ERROR인 경우 핸들링 불가능한 에러로 판단하고 에러를 외부로 던진다.', async () => { -// const {result} = initializeProvider(); -// const errorCode = 'INTERNAL_SERVER_ERROR'; -// const errorInfo: ErrorInfo = {errorCode, message: '서버 에러입니다.'}; - -// await act(async () => { -// try { -// result.current.setErrorInfo(errorInfo); -// } catch (error) { -// expect(error).toBe(errorInfo); -// } -// }); -// }); - -// it('에러 코드가 UNHANDLED인 경우 핸들링 불가능한 에러로 판단하고 에러를 외부로 던진다.', async () => { -// const {result} = initializeProvider(); -// const errorCode = 'UNHANDLED'; -// const errorInfo: ErrorInfo = {errorCode, message: '알 수 없는 에러입니다.'}; - -// await act(async () => { -// try { -// result.current.setErrorInfo(errorInfo); -// } catch (error) { -// expect(error).toBe(errorInfo); -// } -// }); -// }); - -// it('에러 코드에 대응하는 에러메세지가 없는 에러인 경우 핸들링 불가능한 에러로 판단하고 에러를 외부로 던진다.', async () => { -// const {result} = initializeProvider(); -// const errorCode = 'something strange error...'; -// const errorInfo: ErrorInfo = {errorCode, message: '정말 모르겠다.'}; - -// await act(async () => { -// try { -// result.current.setErrorInfo(errorInfo); -// } catch (error) { -// expect(error).toBe(errorInfo); -// } -// }); -// }); -// }); - -// it('Provider없이 useError를 사용할 경우 에러를 던진다.', () => { -// expect(() => { -// const _ = renderHook(() => useError()); -// }).toThrow('useError must be used within an ErrorProvider'); -// }); -// }); diff --git a/client/src/hooks/useError/useError.tsx b/client/src/hooks/useError/useError.tsx deleted file mode 100644 index db194c591..000000000 --- a/client/src/hooks/useError/useError.tsx +++ /dev/null @@ -1,12 +0,0 @@ -// import {useContext} from 'react'; - -// import {ErrorContext, ErrorContextType} from './ErrorProvider'; - -// // 에러 컨텍스트를 사용하는 커스텀 훅 -// export const useError = (): ErrorContextType => { -// const context = useContext(ErrorContext); -// if (!context) { -// throw new Error('useError must be used within an ErrorProvider'); -// } -// return context; -// }; diff --git a/client/src/hooks/useEvent/useEvent.test.tsx b/client/src/hooks/useEvent/useEvent.test.tsx deleted file mode 100644 index eee498faa..000000000 --- a/client/src/hooks/useEvent/useEvent.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -// import {renderHook} from '@testing-library/react'; -// import {MemoryRouter} from 'react-router-dom'; -// import {act} from 'react'; -// import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; - -// import {useError} from '@hooks/useError/useError'; - -// import {PASSWORD_LENGTH} from '@constants/password'; - -// import {VALID_PASSWORD_FOR_TEST} from '@mocks/validValueForTest'; -// import {VALID_EVENT_NAME_LENGTH_IN_SERVER} from '@mocks/serverConstants'; - -// import {ErrorProvider} from '../useError/ErrorProvider'; - -// import useEvent from './useEvent'; - -// describe('useEvent', () => { -// const queryClient = new QueryClient({ -// defaultOptions: { -// queries: { -// retry: 0, -// }, -// }, -// }); - -// const initializeProvider = () => -// renderHook( -// () => { -// return {errorResult: useError(), eventResult: useEvent()}; -// }, -// { -// wrapper: ({children}) => ( -// <QueryClientProvider client={queryClient}> -// <MemoryRouter> -// <ErrorProvider>{children}</ErrorProvider> -// </MemoryRouter> -// </QueryClientProvider> -// ), -// }, -// ); - -// it('이름과 비밀번호를 받아 새로운 이벤트를 생성한다.', async () => { -// const {result} = initializeProvider(); - -// await act(async () => { -// expect( -// await result.current.eventResult.createNewEvent({eventName: '테스트이름', password: VALID_PASSWORD_FOR_TEST}), -// ); -// }); - -// await act(async () => { -// expect(result.current.errorResult.errorInfo).toBe(null); -// }); -// }); - -// it(`이름 길이가 ${VALID_EVENT_NAME_LENGTH_IN_SERVER.min} ~ ${VALID_EVENT_NAME_LENGTH_IN_SERVER.max}사이가 아닌 경우 이벤트를 생성할 수 없다.`, async () => { -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.eventResult.createNewEvent({eventName: '', password: VALID_PASSWORD_FOR_TEST})); -// }); - -// await act(async () => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_NAME_LENGTH_INVALID'); -// }); -// }); - -// it(`비밀번호가 ${PASSWORD_LENGTH}자리수가 아닌 경우 이벤트를 생성할 수 없다`, async () => { -// const {result} = initializeProvider(); - -// await act(async () => { -// expect(await result.current.eventResult.createNewEvent({eventName: '테스트이름', password: 1})); -// }); - -// await act(async () => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); -// }); -// }); -// }); diff --git a/client/src/hooks/useEvent/useEvent.tsx b/client/src/hooks/useEvent/useEvent.tsx deleted file mode 100644 index b9afa2a89..000000000 --- a/client/src/hooks/useEvent/useEvent.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// // TODO: (@todari) useEvent는 이제 쓰지 않긴 해요...! - -// import {RequestPostNewEvent, ResponsePostNewEvent, requestPostNewEvent} from '@apis/request/event'; -// import {useFetch} from '@hooks/useFetch/useFetch'; - -// const useEvent = () => { -// const {fetch} = useFetch(); - -// const createNewEvent = async ({eventName, password}: RequestPostNewEvent) => { -// return await fetch<ResponsePostNewEvent>({queryFunction: () => requestPostNewEvent({eventName, password})}); -// }; - -// return {createNewEvent}; -// }; - -// export default useEvent; diff --git a/client/src/hooks/useFetch/useFetch.test.tsx b/client/src/hooks/useFetch/useFetch.test.tsx deleted file mode 100644 index 8545cd1d7..000000000 --- a/client/src/hooks/useFetch/useFetch.test.tsx +++ /dev/null @@ -1,155 +0,0 @@ -// import {renderHook, waitFor} from '@testing-library/react'; -// import {MemoryRouter} from 'react-router-dom'; -// import {act} from 'react'; - -// import FetchError from '@errors/FetchError'; -// import {useError} from '@hooks/useError/useError'; - -// import {requestPostWithoutResponse} from '@apis/fetcher'; - -// import {captureError} from '@utils/captureError'; - -// import {UNKNOWN_ERROR} from '@constants/errorMessage'; - -// import {ErrorProvider} from '../useError/ErrorProvider'; - -// import {useFetch} from './useFetch'; - -// describe('useFetch', () => { -// const initializeProvider = () => -// renderHook( -// () => { -// return {errorResult: useError(), fetchResult: useFetch()}; -// }, -// { -// wrapper: ({children}) => ( -// <MemoryRouter> -// <ErrorProvider>{children}</ErrorProvider> -// </MemoryRouter> -// ), -// }, -// ); - -// describe('요청이 성공하는 경우', () => { -// it('요청이 성공했다면 그대로 api response body를 반환한다.', async () => { -// const {result} = initializeProvider(); -// const mockQueryFunction = jest.fn().mockResolvedValue('mocked data'); - -// let data; - -// await act(async () => { -// data = await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); -// }); - -// expect(data).toBe('mocked data'); -// }); - -// it('onSuccess 콜백을 넘겨준 경우 콜백을 실행한다.', async () => { -// const {result} = initializeProvider(); -// const mockQueryFunction = jest.fn().mockResolvedValue('mocked data'); -// const onSuccess = jest.fn(); - -// await act(async () => { -// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction, onSuccess}); -// }); - -// expect(onSuccess).toHaveBeenCalled(); -// }); -// }); - -// describe('요청이 실패하는 경우', () => { -// describe('발생한 에러가 Error 인스턴스인 경우', () => { -// const errorThrowFunction = () => requestPostWithoutResponse({endpoint: '/throw-handle-error'}); - -// it('FetchError가 발생하면 해당 에러의 errorBody를 사용해 상태를 저장한다.', async () => { -// const {result} = initializeProvider(); -// const fetchError = new FetchError({ -// errorInfo: {errorCode: 'UNHANDLED', message: 'Fetch error occurred'}, -// name: 'UNHANDLED', -// message: 'Fetch error occurred', -// requestBody: '', -// status: 400, -// endpoint: '', -// method: 'POST', -// }); -// const mockQueryFunction = jest.fn().mockRejectedValue(fetchError); - -// await act(async () => { -// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); -// }); - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('UNHANDLED'); -// expect(result.current.errorResult.errorInfo?.message).toBe('Fetch error occurred'); -// }); -// }); - -// it('일반 Error가 발생하면 해당 에러의 name과 message를 사용해 상태를 저장한다.', async () => { -// const {result} = initializeProvider(); -// const mockError = new Error('일반 에러 발생'); -// const mockQueryFunction = jest.fn().mockRejectedValue(mockError); - -// try { -// await act(async () => { -// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); -// }); -// } catch (error) { -// // 에러 바운더리로 보내지는 에러라서 throw하는데 이를 받아줄 에러 바운더리를 호출하지 않았으므로 catch문에서 별다른 로직을 작성하지 않음 -// } - -// await waitFor(() => { -// expect(result.current.errorResult.errorInfo?.errorCode).toBe('Error'); -// expect(result.current.errorResult.errorInfo?.message).toBe('일반 에러 발생'); -// }); -// }); - -// it('onError 콜백을 넘겨준 경우 onError를 실행한다.', async () => { -// const {result} = initializeProvider(); -// const onError = jest.fn(); - -// await act(async () => { -// await result.current.fetchResult.fetch({queryFunction: errorThrowFunction, onError}); -// }); - -// expect(onError).toHaveBeenCalled(); -// }); - -// it('에러가 발생하면 로그를 보낸다.', async () => { -// const {result} = initializeProvider(); - -// await act(async () => { -// await result.current.fetchResult.fetch({queryFunction: errorThrowFunction}); -// }); - -// expect(captureError).toHaveBeenCalled(); -// }); -// }); - -// describe('발생한 에러가 Error 인스턴스가 아닌 경우', () => { -// const mockQueryFunction = jest.fn().mockRejectedValue('unexpected error'); - -// it(`에러가 발생하면 그 에러를 던진다.`, async () => { -// const {result} = initializeProvider(); - -// // 에러가 발생하고 에러를 던지는지 확인 -// await expect( -// act(async () => { -// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); -// }), -// ).rejects.toThrow(new Error(UNKNOWN_ERROR)); -// }); - -// it('에러가 발생하면 로그를 보낸다.', async () => { -// const {result} = initializeProvider(); - -// await expect( -// act(async () => { -// await result.current.fetchResult.fetch({queryFunction: mockQueryFunction}); -// }), -// ).rejects.toThrow(new Error(UNKNOWN_ERROR)); - -// expect(captureError).toHaveBeenCalled(); -// }); -// }); -// }); -// }); diff --git a/client/src/hooks/useFetch/useFetch.ts b/client/src/hooks/useFetch/useFetch.ts deleted file mode 100644 index 1df6cb082..000000000 --- a/client/src/hooks/useFetch/useFetch.ts +++ /dev/null @@ -1,63 +0,0 @@ -// import {useState} from 'react'; -// import {useNavigate} from 'react-router-dom'; - -// import FetchError from '@errors/FetchError'; -// import {useError} from '@hooks/useError/useError'; - -// import {captureError} from '@utils/captureError'; -// import getEventIdByUrl from '@utils/getEventIdByUrl'; - -// import {UNKNOWN_ERROR} from '@constants/errorMessage'; - -// type FetchProps<T> = { -// queryFunction: () => Promise<T>; -// onSuccess?: () => void; -// onError?: () => void; -// }; - -// export const useFetch = () => { -// const {setErrorInfo, clearError} = useError(); -// const [loading, setLoading] = useState(false); -// const navigate = useNavigate(); -// const eventId = getEventIdByUrl(); - -// const fetch = async <T>({queryFunction, onSuccess, onError}: FetchProps<T>): Promise<T> => { -// setLoading(true); - -// clearError(); -// try { -// const result = await queryFunction(); - -// if (onSuccess) { -// onSuccess(); -// } - -// return result; -// } catch (error) { -// if (error instanceof Error) { -// const errorInfo = -// error instanceof FetchError ? error.errorInfo : {errorCode: error.name, message: error.message}; - -// setErrorInfo(errorInfo); - -// if (onError) { -// onError(); -// } - -// captureError(error); -// } else { -// setErrorInfo({errorCode: UNKNOWN_ERROR, message: JSON.stringify(error)}); -// captureError(new Error(UNKNOWN_ERROR)); - -// // 에러를 throw 해 에러 바운더리로 보냅니다. 따라서 에러 이름은 중요하지 않음 -// throw new Error(UNKNOWN_ERROR); -// } -// } finally { -// setLoading(false); -// } - -// return {} as T; -// }; - -// return {loading, fetch}; -// }; From f6f24d2ef56164db0be49e733a4c11a4a6d5932c Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Thu, 22 Aug 2024 22:02:03 +0900 Subject: [PATCH 32/36] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=EC=9D=B4=20=EC=BB=A4=EB=B2=84?= =?UTF-8?q?=EB=A6=AC=EC=A7=80=EC=97=90=20=EB=9C=A8=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20ignore=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/jest.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/jest.config.ts b/client/jest.config.ts index 8181ab583..a75677a5a 100644 --- a/client/jest.config.ts +++ b/client/jest.config.ts @@ -19,6 +19,8 @@ const config: Config = { '<rootDir>/src/errors/', '<rootDir>/src/components/', '<rootDir>/src/store/', + '<rootDir>/src/pages/', + '<rootDir>/src/hooks/queries', ], verbose: true, From a8b8297643610a18b48f92b1551d7e2cc0550c12 Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Fri, 23 Aug 2024 00:23:32 +0900 Subject: [PATCH 33/36] =?UTF-8?q?rename:=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{UnhandledErrorBoudnary.tsx => UnhandledErrorBoundary.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/src/{UnhandledErrorBoudnary.tsx => UnhandledErrorBoundary.tsx} (100%) diff --git a/client/src/UnhandledErrorBoudnary.tsx b/client/src/UnhandledErrorBoundary.tsx similarity index 100% rename from client/src/UnhandledErrorBoudnary.tsx rename to client/src/UnhandledErrorBoundary.tsx From ce53134fe5d7d630536a7fdb25e6e4226f95e2fb Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Fri, 23 Aug 2024 00:24:30 +0900 Subject: [PATCH 34/36] =?UTF-8?q?chore:=20import=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx b/client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx index edc33567c..8d7946b8a 100644 --- a/client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx +++ b/client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx @@ -10,7 +10,7 @@ import {useAppErrorStore} from '@store/appErrorStore'; import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; -import UnhandledErrorBoundary from '../../UnhandledErrorBoudnary'; +import UnhandledErrorBoundary from '../../UnhandledErrorBoundary'; import ErrorCatcher from './ErrorCatcher'; From 3b55284165c2200d42cea19b646bb16f53a157d5 Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Fri, 23 Aug 2024 00:24:55 +0900 Subject: [PATCH 35/36] =?UTF-8?q?chore:=20=EB=B3=91=ED=9E=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index aa41b88b5..4e9ce39e1 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,36 +1,24 @@ import {Outlet} from 'react-router-dom'; import {HDesignProvider} from 'haengdong-design'; import {Global} from '@emotion/react'; -import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {ReactQueryDevtools} from '@tanstack/react-query-devtools'; import {ToastProvider} from '@hooks/useToast/ToastProvider'; import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary'; import ErrorCatcher from '@components/AppErrorBoundary/ErrorCatcher'; -import UnhandledErrorBoundary from './UnhandledErrorBoudnary'; import {GlobalStyle} from './GlobalStyle'; import UnhandledErrorBoundary from './UnhandledErrorBoundary'; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60, // 1 minute - gcTime: 1000 * 60, // 1 minute - }, - }, -}); - const App: React.FC = () => { return ( <HDesignProvider> - <QueryClientProvider client={queryClient}> - <ReactQueryDevtools initialIsOpen={false} /> - <UnhandledErrorBoundary> - <Global styles={GlobalStyle} /> - <ErrorProvider> - {/* <ErrorProvider callback={toast}> */} - <ToastProvider> + <UnhandledErrorBoundary> + <Global styles={GlobalStyle} /> + <ToastProvider> + <ErrorCatcher> + <QueryClientBoundary> + <ReactQueryDevtools initialIsOpen={false} /> <Outlet /> </QueryClientBoundary> </ErrorCatcher> From 056aee1911955ccf58dcb067cc2341ca99a40fa2 Mon Sep 17 00:00:00 2001 From: pakxe <pigkill40@naver.com> Date: Fri, 23 Aug 2024 00:25:37 +0900 Subject: [PATCH 36/36] =?UTF-8?q?chore:=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=98=A4=EB=A5=98=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=86=B5=EA=B3=BC?= =?UTF-8?q?=EA=B0=80=20=EC=95=88=EB=90=98=EB=AF=80=EB=A1=9C=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EB=8B=A4=EC=9A=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package-lock.json | 26 ++++---------------------- client/package.json | 2 +- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 1d6cfaa28..d86774ea2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,7 +12,7 @@ "@emotion/react": "^11.11.4", "@sentry/react": "^8.25.0", "@tanstack/react-query": "^5.51.23", - "haengdong-design": "^0.1.80", + "haengdong-design": "^0.1.79", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1", @@ -10165,14 +10165,13 @@ } }, "node_modules/haengdong-design": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.80.tgz", - "integrity": "sha512-rqfuFKBcqXL6LuPt4iiiM+ZNHBV++hHVGLICiqMiN9mUcl+x3zgNxiAe/80sHPH0HsZsSbuKdkksUqwdX8adhg==", + "version": "0.1.79", + "resolved": "https://registry.npmjs.org/haengdong-design/-/haengdong-design-0.1.79.tgz", + "integrity": "sha512-J5uFXmX+1VMzdU+aMTl36hJYrjzreLNhrtLMSBrdHtjwEROsYddbbXDlHTTUWaj7TpDQ1n5jgsy39jlKYv2m7w==", "dependencies": { "@emotion/react": "^11.11.4", "@storybook/addon-webpack5-compiler-swc": "^1.0.5", "@svgr/webpack": "^8.1.0", - "lottie-react": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.1" @@ -14263,23 +14262,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lottie-react": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/lottie-react/-/lottie-react-2.4.0.tgz", - "integrity": "sha512-pDJGj+AQlnlyHvOHFK7vLdsDcvbuqvwPZdMlJ360wrzGFurXeKPr8SiRCjLf3LrNYKANQtSsh5dz9UYQHuqx4w==", - "dependencies": { - "lottie-web": "^5.10.2" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/lottie-web": { - "version": "5.12.2", - "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz", - "integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==" - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", diff --git a/client/package.json b/client/package.json index 4f01260c2..d4297e7ba 100644 --- a/client/package.json +++ b/client/package.json @@ -69,7 +69,7 @@ "@emotion/react": "^11.11.4", "@sentry/react": "^8.25.0", "@tanstack/react-query": "^5.51.23", - "haengdong-design": "^0.1.80", + "haengdong-design": "^0.1.79", "react": "^18.3.1", "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18.3.1",