From e31689120f91933f4a128caaba934a5be434bcea Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 00:47:06 +0900 Subject: [PATCH 01/16] =?UTF-8?q?style:=20errorBody=20->=20errorInfo?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=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 --- client/src/apis/fetcher.ts | 12 +++++----- client/src/components/Toast/ToastProvider.tsx | 10 ++++---- client/src/errors/FetchError.ts | 8 +++---- client/src/hooks/useAuth/useAuth.test.tsx | 20 +++++++++------- client/src/hooks/useEvent/useEvent.test.tsx | 10 ++++---- client/src/hooks/useFetch/useFetch.test.tsx | 13 +++++----- client/src/hooks/useFetch/useFetch.ts | 13 +++++----- client/src/types/fetchErrorType.ts | 4 ++-- client/src/utils/captureError.ts | 24 +++++++++---------- client/src/utils/sendLogToSentry.ts | 8 +++---- 10 files changed, 63 insertions(+), 59 deletions(-) diff --git a/client/src/apis/fetcher.ts b/client/src/apis/fetcher.ts index 01c4bec7f..e87e3d395 100644 --- a/client/src/apis/fetcher.ts +++ b/client/src/apis/fetcher.ts @@ -1,4 +1,4 @@ -import {ServerError} from 'ErrorProvider'; +import {ErrorInfo} from '@hooks/useError/ErrorProvider'; import objectToQueryString from '@utils/objectToQueryString'; @@ -96,15 +96,15 @@ const errorHandler = async ({url, options, body}: ErrorHandlerProps) => { const response: Response = await fetch(url, options); if (!response.ok) { - const serverErrorBody: ServerError = await response.json(); + const serverErrorInfo: ErrorInfo = await response.json(); throw new FetchError({ status: response.status, requestBody: body, endpoint: response.url, - errorBody: serverErrorBody, - name: serverErrorBody.errorCode, - message: serverErrorBody.message || '', + errorInfo: serverErrorInfo, + name: serverErrorInfo.errorCode, + message: serverErrorInfo.message || '', method: options.method, }); } @@ -112,7 +112,7 @@ const errorHandler = async ({url, options, body}: ErrorHandlerProps) => { return response; } catch (error) { if (error instanceof Error) { - throw error; + throw error; // 그대로 FetchError || Error 인스턴스를 던집니다. } throw new Error(UNKNOWN_ERROR); diff --git a/client/src/components/Toast/ToastProvider.tsx b/client/src/components/Toast/ToastProvider.tsx index d842a3a08..e989877cc 100644 --- a/client/src/components/Toast/ToastProvider.tsx +++ b/client/src/components/Toast/ToastProvider.tsx @@ -3,7 +3,7 @@ import {createContext, useContext, useEffect, useState} from 'react'; import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; -import {useError} from '../../ErrorProvider'; +import {useError} from '../../hooks/useError/ErrorProvider'; import {ToastProps} from './Toast.type'; import Toast from './Toast'; @@ -23,7 +23,7 @@ type ShowToast = ToastProps & { const ToastProvider = ({children}: React.PropsWithChildren) => { const [currentToast, setCurrentToast] = useState(null); - const {hasError, errorMessage, clearError} = useError(); + const {errorInfo, clearError, clientErrorMessage} = useError(); const showToast = ({showingTime = DEFAULT_TIME, isAlwaysOn = false, ...toastProps}: ShowToast) => { setCurrentToast({showingTime, isAlwaysOn, ...toastProps}); @@ -34,9 +34,9 @@ const ToastProvider = ({children}: React.PropsWithChildren) => { }; useEffect(() => { - if (hasError) { + if (errorInfo !== null) { showToast({ - message: errorMessage || SERVER_ERROR_MESSAGES.UNHANDLED, + message: errorInfo.message || SERVER_ERROR_MESSAGES.UNHANDLED, showingTime: DEFAULT_TIME, // TODO: (@weadie) 나중에 토스트 프로바이더를 제거한 토스트를 만들 것이기 때문에 많이 리펙터링 안함 isAlwaysOn: false, position: 'bottom', @@ -47,7 +47,7 @@ const ToastProvider = ({children}: React.PropsWithChildren) => { clearError(DEFAULT_TIME); } - }, [errorMessage, hasError]); + }, [errorInfo, clientErrorMessage]); useEffect(() => { if (!currentToast) return; diff --git a/client/src/errors/FetchError.ts b/client/src/errors/FetchError.ts index f7bf21fe9..9123a80b5 100644 --- a/client/src/errors/FetchError.ts +++ b/client/src/errors/FetchError.ts @@ -4,16 +4,16 @@ class FetchError extends Error { requestBody; status; endpoint; - errorBody; + errorInfo; method; - constructor({requestBody, status, endpoint, errorBody, method, name, message}: FetchErrorType) { - super(errorBody.errorCode); + constructor({requestBody, status, endpoint, errorInfo, method, name, message}: FetchErrorType) { + super(errorInfo.errorCode); this.requestBody = requestBody; this.status = status; this.endpoint = endpoint; - this.errorBody = errorBody; + this.errorInfo = errorInfo; this.method = method; this.name = name; this.message = message; diff --git a/client/src/hooks/useAuth/useAuth.test.tsx b/client/src/hooks/useAuth/useAuth.test.tsx index 48aaade10..130aa73f2 100644 --- a/client/src/hooks/useAuth/useAuth.test.tsx +++ b/client/src/hooks/useAuth/useAuth.test.tsx @@ -2,11 +2,13 @@ import {renderHook, waitFor} from '@testing-library/react'; 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, useError} from '../../ErrorProvider'; +import {ErrorProvider} from '../useError/ErrorProvider'; import useAuth from './useAuth'; @@ -34,7 +36,7 @@ describe('useAuth', () => { }); await waitFor(() => { - expect(result.current.errorResult.error?.errorCode).toBe('TOKEN_NOT_FOUND'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_NOT_FOUND'); }); }); @@ -48,7 +50,7 @@ describe('useAuth', () => { }); await waitFor(() => { - expect(result.current.errorResult.error).toBe(null); + expect(result.current.errorResult.errorInfo).toBe(null); }); }); @@ -62,7 +64,7 @@ describe('useAuth', () => { }); await waitFor(() => { - expect(result.current.errorResult.error?.errorCode).toBe('TOKEN_INVALID'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_INVALID'); }); }); @@ -76,7 +78,7 @@ describe('useAuth', () => { }); await waitFor(() => { - expect(result.current.errorResult.error?.errorCode).toBe('TOKEN_EXPIRED'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('TOKEN_EXPIRED'); }); }); @@ -90,7 +92,7 @@ describe('useAuth', () => { }); await waitFor(() => { - expect(result.current.errorResult.error?.errorCode).toBe('FORBIDDEN'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('FORBIDDEN'); }); }); }); @@ -104,7 +106,7 @@ describe('useAuth', () => { }); await waitFor(() => { - expect(result.current.errorResult.error).toBe(null); + expect(result.current.errorResult.errorInfo).toBe(null); }); }); @@ -116,7 +118,7 @@ describe('useAuth', () => { }); await waitFor(() => { - expect(result.current.errorResult.error?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); }); }); @@ -128,7 +130,7 @@ describe('useAuth', () => { }); await waitFor(() => { - expect(result.current.errorResult.error?.errorCode).toBe('PASSWORD_INVALID'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('PASSWORD_INVALID'); }); }); }); diff --git a/client/src/hooks/useEvent/useEvent.test.tsx b/client/src/hooks/useEvent/useEvent.test.tsx index b6a616b00..8e952a0db 100644 --- a/client/src/hooks/useEvent/useEvent.test.tsx +++ b/client/src/hooks/useEvent/useEvent.test.tsx @@ -2,12 +2,14 @@ import {renderHook} from '@testing-library/react'; import {MemoryRouter} from 'react-router-dom'; import {act} from 'react'; +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, useError} from '../../ErrorProvider'; +import {ErrorProvider} from '../useError/ErrorProvider'; import useEvent from './useEvent'; @@ -36,7 +38,7 @@ describe('useEvent', () => { }); await act(async () => { - expect(result.current.errorResult.error).toBe(null); + expect(result.current.errorResult.errorInfo).toBe(null); }); }); @@ -48,7 +50,7 @@ describe('useEvent', () => { }); await act(async () => { - expect(result.current.errorResult.error?.errorCode).toBe('EVENT_NAME_LENGTH_INVALID'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_NAME_LENGTH_INVALID'); }); }); @@ -60,7 +62,7 @@ describe('useEvent', () => { }); await act(async () => { - expect(result.current.errorResult.error?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('EVENT_PASSWORD_FORMAT_INVALID'); }); }); }); diff --git a/client/src/hooks/useFetch/useFetch.test.tsx b/client/src/hooks/useFetch/useFetch.test.tsx index 253d60b6e..b4748125a 100644 --- a/client/src/hooks/useFetch/useFetch.test.tsx +++ b/client/src/hooks/useFetch/useFetch.test.tsx @@ -3,6 +3,7 @@ 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'; @@ -10,7 +11,7 @@ import {captureError} from '@utils/captureError'; import {UNKNOWN_ERROR} from '@constants/errorMessage'; -import {ErrorProvider, useError} from '../../ErrorProvider'; +import {ErrorProvider} from '../useError/ErrorProvider'; import {useFetch} from './useFetch'; @@ -63,7 +64,7 @@ describe('useFetch', () => { it('FetchError가 발생하면 해당 에러의 errorBody를 사용해 상태를 저장한다.', async () => { const {result} = initializeProvider(); const fetchError = new FetchError({ - errorBody: {errorCode: 'UNHANDLED', message: 'Fetch error occurred'}, + errorInfo: {errorCode: 'UNHANDLED', message: 'Fetch error occurred'}, name: 'UNHANDLED', message: 'Fetch error occurred', requestBody: '', @@ -78,8 +79,8 @@ describe('useFetch', () => { }); await waitFor(() => { - expect(result.current.errorResult.error?.errorCode).toBe('UNHANDLED'); - expect(result.current.errorResult.error?.message).toBe('Fetch error occurred'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('UNHANDLED'); + expect(result.current.errorResult.errorInfo?.message).toBe('Fetch error occurred'); }); }); @@ -97,8 +98,8 @@ describe('useFetch', () => { } await waitFor(() => { - expect(result.current.errorResult.error?.errorCode).toBe('Error'); - expect(result.current.errorResult.error?.message).toBe('일반 에러 발생'); + expect(result.current.errorResult.errorInfo?.errorCode).toBe('Error'); + expect(result.current.errorResult.errorInfo?.message).toBe('일반 에러 발생'); }); }); diff --git a/client/src/hooks/useFetch/useFetch.ts b/client/src/hooks/useFetch/useFetch.ts index 2d034fa5e..35bf18a33 100644 --- a/client/src/hooks/useFetch/useFetch.ts +++ b/client/src/hooks/useFetch/useFetch.ts @@ -2,14 +2,13 @@ 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'; -import {useError} from '../../ErrorProvider'; - type FetchProps = { queryFunction: () => Promise; onSuccess?: () => void; @@ -17,7 +16,7 @@ type FetchProps = { }; export const useFetch = () => { - const {setError, clearError} = useError(); + const {setErrorInfo, clearError} = useError(); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const eventId = getEventIdByUrl(); @@ -36,10 +35,10 @@ export const useFetch = () => { return result; } catch (error) { if (error instanceof Error) { - const errorBody = - error instanceof FetchError ? error.errorBody : {errorCode: error.name, message: error.message}; + const errorInfo = + error instanceof FetchError ? error.errorInfo : {errorCode: error.name, message: error.message}; - setError(errorBody); + setErrorInfo(errorInfo); if (onError) { onError(); @@ -47,7 +46,7 @@ export const useFetch = () => { captureError(error, navigate, eventId); } else { - setError({errorCode: UNKNOWN_ERROR, message: JSON.stringify(error)}); + setErrorInfo({errorCode: UNKNOWN_ERROR, message: JSON.stringify(error)}); captureError(new Error(UNKNOWN_ERROR), navigate, eventId); // 에러를 throw 해 에러 바운더리로 보냅니다. 따라서 에러 이름은 중요하지 않음 diff --git a/client/src/types/fetchErrorType.ts b/client/src/types/fetchErrorType.ts index 2ab327e19..664fa368b 100644 --- a/client/src/types/fetchErrorType.ts +++ b/client/src/types/fetchErrorType.ts @@ -1,4 +1,4 @@ -import {ServerError} from 'ErrorProvider'; +import {ErrorInfo} from '@hooks/useError/ErrorProvider'; import {Method} from '@apis/fetcher'; @@ -6,6 +6,6 @@ export type FetchErrorType = Error & { requestBody: string; status: number; endpoint: string; - errorBody: ServerError; + errorInfo: ErrorInfo; method: Method; }; diff --git a/client/src/utils/captureError.ts b/client/src/utils/captureError.ts index d0af34729..ce70da972 100644 --- a/client/src/utils/captureError.ts +++ b/client/src/utils/captureError.ts @@ -1,7 +1,7 @@ import {NavigateFunction} from 'react-router-dom'; import FetchError from '@errors/FetchError'; -import {ServerError} from 'ErrorProvider'; +import {ErrorInfo} from '@hooks/useError/ErrorProvider'; import {ROUTER_URLS} from '@constants/routerUrls'; @@ -11,47 +11,47 @@ export const captureError = async (error: Error, navigate: NavigateFunction, eve // prod 환경에서만 Sentry capture 실행 if (process.env.NODE_ENV !== 'production') return; - const errorBody: ServerError = - error instanceof FetchError ? error.errorBody : {message: error.message, errorCode: error.name}; + const errorInfo: ErrorInfo = + error instanceof FetchError ? error.errorInfo : {message: error.message, errorCode: error.name}; - switch (errorBody?.errorCode) { + switch (errorInfo?.errorCode) { case 'INTERNAL_SERVER_ERROR': - sendLogToSentry({error, errorBody, level: 'fatal'}); + sendLogToSentry({error, errorInfo, level: 'fatal'}); break; case 'FORBIDDEN': - sendLogToSentry({error, errorBody}); + sendLogToSentry({error, errorInfo}); navigate(`${ROUTER_URLS.event}/${eventId}/login`); break; case 'TOKEN_INVALID': - sendLogToSentry({error, errorBody}); + sendLogToSentry({error, errorInfo}); navigate(`${ROUTER_URLS.event}/${eventId}/login`); break; case 'TOKEN_EXPIRED': - sendLogToSentry({error, errorBody}); + sendLogToSentry({error, errorInfo}); navigate(`${ROUTER_URLS.event}/${eventId}/login`); break; case 'TOKEN_NOT_FOUND': - sendLogToSentry({error, errorBody}); + sendLogToSentry({error, errorInfo}); navigate(`${ROUTER_URLS.event}/${eventId}/login`); break; // 비밀 번호를 까먹는 사람이 얼마나 많은 지 추측하기 위함 case 'PASSWORD_INVALID': - sendLogToSentry({error, errorBody, level: 'debug'}); + sendLogToSentry({error, errorInfo, level: 'debug'}); navigate(`${ROUTER_URLS.event}/${eventId}/login`); break; // 1천만원 이상 입력하는 사람이 얼마나 많은 지 추측하기 위함 case 'BILL_ACTION_PRICE_INVALID': - sendLogToSentry({error, errorBody, level: 'debug'}); + sendLogToSentry({error, errorInfo, level: 'debug'}); break; default: - sendLogToSentry({error, errorBody, level: 'fatal'}); + sendLogToSentry({error, errorInfo, level: 'fatal'}); break; } }; diff --git a/client/src/utils/sendLogToSentry.ts b/client/src/utils/sendLogToSentry.ts index 165cf323b..d93e526a4 100644 --- a/client/src/utils/sendLogToSentry.ts +++ b/client/src/utils/sendLogToSentry.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/react'; -import {ServerError} from 'ErrorProvider'; +import {ErrorInfo} from '@hooks/useError/ErrorProvider'; import {UNKNOWN_ERROR} from '@constants/errorMessage'; @@ -21,12 +21,12 @@ type SentryLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'log'; type SendLogToSentry = { level?: SentryLevel; error: Error; - errorBody: ServerError; + errorInfo: ErrorInfo; }; -const sendLogToSentry = ({level = 'error', error, errorBody}: SendLogToSentry) => { +const sendLogToSentry = ({level = 'error', error, errorInfo}: SendLogToSentry) => { Sentry.withScope(scope => { - const {errorCode, message} = errorBody; + const {errorCode, message} = errorInfo; scope.setLevel(level); scope.setTag('environment', process.env.NODE_ENV); From dda91e877ec19e725b78a9caf8a5de057cb7a20e Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 00:47:45 +0900 Subject: [PATCH 02/16] =?UTF-8?q?test:=20jest=EC=97=90=EC=84=9C=20svg?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EB=AA=BB=EC=9D=BD=EC=9C=BC?= =?UTF-8?q?=EB=AF=80=EB=A1=9C=20=EC=9D=B4=EB=A5=BC=20=EB=AA=A8=ED=82=B9?= =?UTF-8?q?=ED=95=B4=20=EC=98=A4=EB=A5=98=EA=B0=80=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EB=8C=80?= =?UTF-8?q?=EC=B2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/jest.config.ts | 1 + client/src/mocks/svg.ts | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 client/src/mocks/svg.ts diff --git a/client/jest.config.ts b/client/jest.config.ts index 7d54d6b1d..2a366604e 100644 --- a/client/jest.config.ts +++ b/client/jest.config.ts @@ -34,6 +34,7 @@ const config: Config = { '^@types/(.*)$': '/src/types/$1', '^@errors/(.*)$': '/src/errors/$1', '^@mocks/(.*)$': '/src/mocks/$1', + '\\.svg$': '/src/mocks/svg.ts', }, testEnvironmentOptions: { customExportConditions: [''], diff --git a/client/src/mocks/svg.ts b/client/src/mocks/svg.ts new file mode 100644 index 000000000..ffe2050a0 --- /dev/null +++ b/client/src/mocks/svg.ts @@ -0,0 +1,2 @@ +export default 'SvgrURL'; +export const ReactComponent = 'div'; From a3438d024a4ca9acb73226d661632c263991899b Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 00:48:16 +0900 Subject: [PATCH 03/16] =?UTF-8?q?chore:=20coverage=EC=97=90=EC=84=9C=20err?= =?UTF-8?q?orProvider=EA=B0=80=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20ig?= =?UTF-8?q?nore=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 --- client/jest.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/jest.config.ts b/client/jest.config.ts index 2a366604e..67c58ec59 100644 --- a/client/jest.config.ts +++ b/client/jest.config.ts @@ -17,7 +17,6 @@ const config: Config = { '/src/request/', '/src/constants/', '/src/errors/', - '/src/ErrorProvider.tsx', ], verbose: true, From dafaa91e175e4383ef20dd8a19ff85b42634b867 Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 00:49:54 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=82=AD=EB=B9=84=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=9D=B8=20hasError=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=ED=95=98=EA=B3=A0=20errorMessage=EB=8A=94=20=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B3=B4=EC=97=AC=EC=A7=80=EB=8A=94=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=EC=9D=B4=EB=AF=80=EB=A1=9C=20client?= =?UTF-8?q?ErrorMessage=EB=A1=9C=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/hooks/useError/ErrorProvider.tsx | 69 +++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 client/src/hooks/useError/ErrorProvider.tsx diff --git a/client/src/hooks/useError/ErrorProvider.tsx b/client/src/hooks/useError/ErrorProvider.tsx new file mode 100644 index 000000000..e1466cf4c --- /dev/null +++ b/client/src/hooks/useError/ErrorProvider.tsx @@ -0,0 +1,69 @@ +import {createContext, useState, useEffect, ReactNode} from 'react'; + +import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; + +// 에러 컨텍스트 생성 +export interface ErrorContextType { + clientErrorMessage: string; + setErrorInfo: (error: ErrorInfo) => void; + clearError: (ms?: number) => void; + errorInfo: ErrorInfo | null; +} + +export const ErrorContext = createContext(undefined); + +// 에러 컨텍스트를 제공하는 프로바이더 컴포넌트 +interface ErrorProviderProps { + children: ReactNode; + callback?: (message: string) => void; +} + +export type ErrorInfo = { + errorCode: string; + message: string; +}; + +export const ErrorProvider = ({children, callback}: ErrorProviderProps) => { + const [clientErrorMessage, setClientErrorMessage] = useState(''); + const [errorInfo, setErrorState] = useState(null); + + useEffect(() => { + if (errorInfo) { + if (isUnhandledError(errorInfo.errorCode)) { + // 에러바운더리로 보내기 + + throw errorInfo; + } + + const message = SERVER_ERROR_MESSAGES[errorInfo.errorCode]; + setClientErrorMessage(message); + // callback(message); + } + }, [errorInfo, callback]); + + const setErrorInfo = (error: ErrorInfo) => { + setClientErrorMessage(''); + setErrorState(error); + }; + + 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; +}; From 8fd839a7ead19f562190bf256b010789cdcba0d9 Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 00:50:13 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20ErrorProvider=EC=97=90?= =?UTF-8?q?=EC=84=9C=20useError=EB=A5=BC=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/ErrorProvider.tsx | 83 -------------------------- client/src/hooks/useError/useError.tsx | 12 ++++ 2 files changed, 12 insertions(+), 83 deletions(-) delete mode 100644 client/src/ErrorProvider.tsx create mode 100644 client/src/hooks/useError/useError.tsx diff --git a/client/src/ErrorProvider.tsx b/client/src/ErrorProvider.tsx deleted file mode 100644 index c657d9337..000000000 --- a/client/src/ErrorProvider.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import {createContext, useState, useContext, useEffect, ReactNode} from 'react'; - -import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; - -// 에러 컨텍스트 생성 -interface ErrorContextType { - hasError: boolean; - errorMessage: string; - setError: (error: ServerError) => void; - clearError: (ms?: number) => void; - error: ServerError | null; -} - -const ErrorContext = createContext(undefined); - -// 에러 컨텍스트를 제공하는 프로바이더 컴포넌트 -interface ErrorProviderProps { - children: ReactNode; - callback?: (message: string) => void; -} - -export type ServerError = { - errorCode: string; - message: string; -}; - -export const ErrorProvider = ({children, callback}: ErrorProviderProps) => { - const [hasError, setHasError] = useState(false); - const [errorMessage, setErrorMessage] = useState(''); - const [error, setErrorState] = useState(null); - - useEffect(() => { - if (error) { - if (isUnhandledError(error.errorCode)) { - // 에러바운더리로 보내기 - - throw error; - } - - setHasError(true); - const message = SERVER_ERROR_MESSAGES[error.errorCode]; - setErrorMessage(message); - // callback(message); - } - }, [error, callback]); - - const setError = (error: ServerError) => { - setHasError(true); - setErrorMessage(''); - setErrorState(error); - }; - - const clearError = (ms: number = 0) => { - if (error === null) return; - - setTimeout(() => { - setHasError(false); - setErrorMessage(''); - setErrorState(null); - }, ms); - }; - - return ( - - {children} - - ); -}; - -// 에러 컨텍스트를 사용하는 커스텀 훅 -export const useError = (): ErrorContextType => { - const context = useContext(ErrorContext); - if (!context) { - throw new Error('useError must be used within an ErrorProvider'); - } - return context; -}; - -const isUnhandledError = (errorCode: string) => { - if (errorCode === 'INTERNAL_SERVER_ERROR') return true; - - return SERVER_ERROR_MESSAGES[errorCode] === undefined; -}; diff --git a/client/src/hooks/useError/useError.tsx b/client/src/hooks/useError/useError.tsx new file mode 100644 index 000000000..ee7176233 --- /dev/null +++ b/client/src/hooks/useError/useError.tsx @@ -0,0 +1,12 @@ +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; +}; From 399f100c0e4262e6e3b5a010e9fedc5fb0872a3f Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 00:50:42 +0900 Subject: [PATCH 06/16] =?UTF-8?q?test:=20=EC=A0=84=EC=97=AD=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?useError=20=ED=9B=85=EC=9D=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=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 --- client/src/hooks/useError/useError.test.tsx | 121 ++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 client/src/hooks/useError/useError.test.tsx diff --git a/client/src/hooks/useError/useError.test.tsx b/client/src/hooks/useError/useError.test.tsx new file mode 100644 index 000000000..7c39d0e95 --- /dev/null +++ b/client/src/hooks/useError/useError.test.tsx @@ -0,0 +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'); + }); +}); From 792e458181b71975a51beeb2eb4611080828bb94 Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 00:51:04 +0900 Subject: [PATCH 07/16] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EB=B3=80=EA=B2=BD=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20import=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/App.tsx | 2 +- .../useDeleteMemberAction/useDeleteMemberAction.test.tsx | 2 +- .../useSearchMemberReportList.test.tsx | 2 +- client/src/hooks/useStepList/useStepList.test.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 64de1e68c..14a019e5d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,7 +6,7 @@ import {ToastProvider} from '@components/Toast/ToastProvider'; import {GlobalStyle} from './GlobalStyle'; // import toast from 'react-simple-toasts'; -import {ErrorProvider} from './ErrorProvider'; +import {ErrorProvider} from './hooks/useError/ErrorProvider'; import UnhandledErrorBoundary from './UnhandledErrorBoundary'; const App: React.FC = () => { diff --git a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx index 9ca4d7bff..0e3ae3fdb 100644 --- a/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx +++ b/client/src/hooks/useDeleteMemberAction/useDeleteMemberAction.test.tsx @@ -6,7 +6,7 @@ import {BillStep, MemberAction, MemberStep} from 'types/serviceType'; import stepListJson from '../../mocks/stepList.json'; import StepListProvider, {useStepList} from '../useStepList/useStepList'; -import {ErrorProvider} from '../../ErrorProvider'; +import {ErrorProvider} from '../useError/ErrorProvider'; import invalidMemberStepListJson from '../../mocks/invalidMemberStepList.json'; import useDeleteMemberAction from './useDeleteMemberAction'; diff --git a/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx b/client/src/hooks/useSearchMemberReportList/useSearchMemberReportList.test.tsx index 2688a9a04..667f0bfb6 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 reportListJson from '../../mocks/reportList.json'; -import {ErrorProvider} from '../../ErrorProvider'; +import {ErrorProvider} from '../useError/ErrorProvider'; import useSearchMemberReportList from './useSearchMemberReportList'; diff --git a/client/src/hooks/useStepList/useStepList.test.tsx b/client/src/hooks/useStepList/useStepList.test.tsx index 81963b73a..ae4cca978 100644 --- a/client/src/hooks/useStepList/useStepList.test.tsx +++ b/client/src/hooks/useStepList/useStepList.test.tsx @@ -6,7 +6,7 @@ import {Bill} from 'types/serviceType'; import StepListProvider, {useStepList} from '../useStepList/useStepList'; import stepListJson from '../../mocks/stepList.json'; -import {ErrorProvider} from '../../ErrorProvider'; +import {ErrorProvider} from '../useError/ErrorProvider'; const stepListMockData = stepListJson; @@ -105,7 +105,7 @@ describe('useStepList', () => { it('provider안에서 호출되지 않으면 에러를 던진다.', () => { expect(() => { - const {result} = renderHook(() => useStepList()); + const _ = renderHook(() => useStepList()); }).toThrow('useStepList는 StepListProvider 내에서 사용되어야 합니다.'); }); }); From 75886157f5e38112449842e74c367919ed90bc12 Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 01:24:37 +0900 Subject: [PATCH 08/16] =?UTF-8?q?chore:=20toBeInTheDocument=20=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/jest.setup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/jest.setup.ts b/client/jest.setup.ts index d3ff94078..9fe320b2c 100644 --- a/client/jest.setup.ts +++ b/client/jest.setup.ts @@ -1,5 +1,6 @@ import {server} from './src/mocks/server'; import * as router from 'react-router'; +import '@testing-library/jest-dom'; // toBeInTheDocument를 인식하기 위해 @testing-library/jest-dom/extend-expect추가 beforeAll(() => { server.listen(); From 23c38d430a7d64fb410c1ff5998cd3897c99d36a Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 01:24:47 +0900 Subject: [PATCH 09/16] =?UTF-8?q?chore:=20toBeInTheDocument=20=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package-lock.json | 11 +++++++++++ client/package.json | 1 + 2 files changed, 12 insertions(+) diff --git a/client/package-lock.json b/client/package-lock.json index ca9b099ba..34f7700fa 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -32,6 +32,7 @@ "@types/react": "^18.3.3", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^18.3.0", + "@types/testing-library__jest-dom": "^6.0.0", "@typescript-eslint/eslint-plugin": "^7.16.0", "@typescript-eslint/parser": "^7.16.0", "cypress": "^13.13.2", @@ -5007,6 +5008,16 @@ "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", "dev": true }, + "node_modules/@types/testing-library__jest-dom": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-6.0.0.tgz", + "integrity": "sha512-bnreXCgus6IIadyHNlN/oI5FfX4dWgvGhOPvpr7zzCYDGAPIfvyIoAozMBINmhmsVuqV0cncejF2y5KC7ScqOg==", + "deprecated": "This is a stub types definition. @testing-library/jest-dom provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "@testing-library/jest-dom": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", diff --git a/client/package.json b/client/package.json index fe7f565ed..775c14c0b 100644 --- a/client/package.json +++ b/client/package.json @@ -31,6 +31,7 @@ "@types/react": "^18.3.3", "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^18.3.0", + "@types/testing-library__jest-dom": "^6.0.0", "@typescript-eslint/eslint-plugin": "^7.16.0", "@typescript-eslint/parser": "^7.16.0", "cypress": "^13.13.2", From 3320688926d92380ab855090e73d71b020d0f3ac Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 01:26:04 +0900 Subject: [PATCH 10/16] =?UTF-8?q?chore:=20Property=20'toBeInTheDocument'?= =?UTF-8?q?=20does=20not=20exist=20on=20type=20'JestMatchers'?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=EB=A5=BC=20=EB=B0=9C=EC=83=9D=EC=8B=9C?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=95=8A=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/tsconfig.json b/client/tsconfig.json index 7c749b6f5..8b7b30d14 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -20,7 +20,7 @@ // "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - "types": ["jest", "node", "cypress"], + "types": ["jest", "@testing-library/jest-dom", "node", "cypress"], "esModuleInterop": true, "strictNullChecks": true, "strictFunctionTypes": true, From c6193f13620064fe3fa6e8a741bb029d37a753b5 Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 01:26:25 +0900 Subject: [PATCH 11/16] =?UTF-8?q?refactor:=20ToastProvider=EC=97=90?= =?UTF-8?q?=EC=84=9C=20useToast=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Toast/ToastProvider.tsx | 84 ------------------- client/src/hooks/useToast/useToast.tsx | 13 +++ 2 files changed, 13 insertions(+), 84 deletions(-) delete mode 100644 client/src/components/Toast/ToastProvider.tsx create mode 100644 client/src/hooks/useToast/useToast.tsx diff --git a/client/src/components/Toast/ToastProvider.tsx b/client/src/components/Toast/ToastProvider.tsx deleted file mode 100644 index e989877cc..000000000 --- a/client/src/components/Toast/ToastProvider.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import {createContext, useContext, useEffect, useState} from 'react'; - -import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; - -import {useError} from '../../hooks/useError/ErrorProvider'; - -import {ToastProps} from './Toast.type'; -import Toast from './Toast'; - -export const ToastContext = createContext(null); - -const DEFAULT_TIME = 3000; - -interface ToastContextProps { - showToast: (args: ShowToast) => void; -} - -type ShowToast = ToastProps & { - showingTime?: number; - isAlwaysOn?: boolean; -}; - -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}); - }; - - const closeToast = () => { - setCurrentToast(null); - }; - - useEffect(() => { - if (errorInfo !== null) { - showToast({ - message: errorInfo.message || 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; - - if (!currentToast.isAlwaysOn) { - const timer = setTimeout(() => { - setCurrentToast(null); - }, currentToast.showingTime); - - return () => clearTimeout(timer); - } - - return; - }, [currentToast]); - - return ( - - {currentToast && } - {children} - - ); -}; - -const useToast = () => { - const context = useContext(ToastContext); - - if (!context) { - throw new Error('useToast는 ToastProvider 내에서 사용되어야 합니다.'); - } - - return context; -}; - -export {ToastProvider, useToast}; diff --git a/client/src/hooks/useToast/useToast.tsx b/client/src/hooks/useToast/useToast.tsx new file mode 100644 index 000000000..189847405 --- /dev/null +++ b/client/src/hooks/useToast/useToast.tsx @@ -0,0 +1,13 @@ +import {useContext} from 'react'; + +import {ToastContext} from './ToastProvider'; + +export const useToast = () => { + const context = useContext(ToastContext); + + if (!context) { + throw new Error('useToast는 ToastProvider 내에서 사용되어야 합니다.'); + } + + return context; +}; From cf9072d4a5fac7616247e88e332fa902bb82eb82 Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 01:26:54 +0900 Subject: [PATCH 12/16] =?UTF-8?q?test:=20=ED=95=B8=EB=93=A4=EB=A7=81=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=97=90=EB=9F=AC=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=20=EC=8B=9C=20=ED=86=A0=EC=8A=A4=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EB=9D=84=EC=9A=B0=EA=B3=A0,=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=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=20=EC=8B=9C=20=ED=86=A0=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=9D=84=EC=9A=B0=EC=A7=80=20=EC=95=8A=EB=8A=94?= =?UTF-8?q?=EC=A7=80=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 --- client/src/hooks/useToast/useToast.test.tsx | 99 +++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 client/src/hooks/useToast/useToast.test.tsx diff --git a/client/src/hooks/useToast/useToast.test.tsx b/client/src/hooks/useToast/useToast.test.tsx new file mode 100644 index 000000000..6f9522a24 --- /dev/null +++ b/client/src/hooks/useToast/useToast.test.tsx @@ -0,0 +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(); + }); + }); + }); +}); From e57509abdb628082998d371725fb42f1f4a9e219 Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 01:27:25 +0900 Subject: [PATCH 13/16] =?UTF-8?q?feat:=20Toast=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=EC=97=90=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=97=98=EB=A6=AC=EB=A8=BC=ED=8A=B8=20=EC=8B=9D=EB=B3=84?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20id=3D'toast'=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/components/Toast/Toast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Toast/Toast.tsx b/client/src/components/Toast/Toast.tsx index d19b11ca3..edb17400d 100644 --- a/client/src/components/Toast/Toast.tsx +++ b/client/src/components/Toast/Toast.tsx @@ -51,7 +51,7 @@ const Toast = ({ }; return createPortal( -
+
From 7ecd473fc9ef339bb28e991ece1e29f40054bed3 Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 01:27:50 +0900 Subject: [PATCH 14/16] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20ho?= =?UTF-8?q?oks=EB=82=B4=EB=B6=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/hooks/useToast/ToastProvider.tsx | 72 +++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 client/src/hooks/useToast/ToastProvider.tsx diff --git a/client/src/hooks/useToast/ToastProvider.tsx b/client/src/hooks/useToast/ToastProvider.tsx new file mode 100644 index 000000000..eed006967 --- /dev/null +++ b/client/src/hooks/useToast/ToastProvider.tsx @@ -0,0 +1,72 @@ +/** @jsxImportSource @emotion/react */ +import {createContext, useContext, useEffect, useState} from 'react'; + +import {useError} from '@hooks/useError/useError'; + +import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage'; + +import {ToastProps} from '../../components/Toast/Toast.type'; +import Toast from '../../components/Toast/Toast'; + +export const ToastContext = createContext(null); + +const DEFAULT_TIME = 3000; + +interface ToastContextProps { + showToast: (args: ShowToast) => void; +} + +type ShowToast = ToastProps & { + showingTime?: number; + isAlwaysOn?: boolean; +}; + +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}); + }; + + const closeToast = () => { + 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; + + if (!currentToast.isAlwaysOn) { + const timer = setTimeout(() => { + setCurrentToast(null); + }, currentToast.showingTime); + + return () => clearTimeout(timer); + } + + return; + }, [currentToast]); + + return ( + + {currentToast && } + {children} + + ); +}; From 422925d2b4929dde2a430a992876620121902c4e Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 01:29:04 +0900 Subject: [PATCH 15/16] =?UTF-8?q?chore:=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20import=20?= =?UTF-8?q?=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/App.tsx | 2 +- .../DeleteMemberActionModal/DeleteMemberActionModal.tsx | 2 +- client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx | 2 +- client/src/pages/EventPage/EventPageLayout.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 14a019e5d..e3b8e69d6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,7 +2,7 @@ import {Outlet} from 'react-router-dom'; import {HDesignProvider} from 'haengdong-design'; import {Global} from '@emotion/react'; -import {ToastProvider} from '@components/Toast/ToastProvider'; +import {ToastProvider} from '@hooks/useToast/ToastProvider'; import {GlobalStyle} from './GlobalStyle'; // import toast from 'react-simple-toasts'; diff --git a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx b/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx index b4c24afbb..04242fad1 100644 --- a/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx +++ b/client/src/components/Modal/SetActionModal/DeleteMemberActionModal/DeleteMemberActionModal.tsx @@ -2,8 +2,8 @@ import type {MemberAction, MemberType} from 'types/serviceType'; import {BottomSheet, Flex, Input, Text, IconButton, FixedButton, Icon} from 'haengdong-design'; -import {useToast} from '@components/Toast/ToastProvider'; import useDeleteMemberAction from '@hooks/useDeleteMemberAction/useDeleteMemberAction'; +import {useToast} from '@hooks/useToast/useToast'; import {bottomSheetHeaderStyle, bottomSheetStyle, inputGroupStyle} from './DeleteMemberActionModal.style'; diff --git a/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx index d33f5b6e7..fae728ae4 100644 --- a/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx +++ b/client/src/pages/CreateEventPage/CompleteCreateEventPage.tsx @@ -3,7 +3,7 @@ import {Button, FixedButton, Flex, Input, MainLayout, Text, Title, TopNav} from import {CopyToClipboard} from 'react-copy-to-clipboard'; import {css} from '@emotion/react'; -import {useToast} from '@components/Toast/ToastProvider'; +import {useToast} from '@hooks/useToast/useToast'; import getEventPageUrlByEnvironment from '@utils/getEventPageUrlByEnvironment'; diff --git a/client/src/pages/EventPage/EventPageLayout.tsx b/client/src/pages/EventPage/EventPageLayout.tsx index 5aa61a570..0c67454ac 100644 --- a/client/src/pages/EventPage/EventPageLayout.tsx +++ b/client/src/pages/EventPage/EventPageLayout.tsx @@ -3,9 +3,9 @@ import {Outlet, useMatch} from 'react-router-dom'; import {useEffect, useState} from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; -import {useToast} from '@components/Toast/ToastProvider'; import {requestGetEventName} from '@apis/request/event'; import StepListProvider from '@hooks/useStepList/useStepList'; +import {useToast} from '@hooks/useToast/useToast'; import useNavSwitch from '@hooks/useNavSwitch'; From bd264287582b58eceaa87237385a93268ee9b1aa Mon Sep 17 00:00:00 2001 From: pakxe Date: Sat, 17 Aug 2024 01:33:40 +0900 Subject: [PATCH 16/16] =?UTF-8?q?chore:=20components=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=EB=8A=94=20=EC=BB=A4=EB=B2=84=EB=A6=AC?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=ED=8F=AC=ED=95=A8=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20ignore=EC=97=90=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/jest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/jest.config.ts b/client/jest.config.ts index 67c58ec59..a618bce0c 100644 --- a/client/jest.config.ts +++ b/client/jest.config.ts @@ -17,6 +17,7 @@ const config: Config = { '/src/request/', '/src/constants/', '/src/errors/', + '/src/components/', ], verbose: true,