diff --git a/.pnp.cjs b/.pnp.cjs index f9e62327..d83667df 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -8738,7 +8738,6 @@ const RAW_RUNTIME_STATE = ["date-fns", "npm:3.3.1"],\ ["framer-motion", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:11.2.10"],\ ["jotai", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:2.8.3"],\ - ["js-cookie", "npm:3.0.5"],\ ["jwt-decode", "npm:4.0.0"],\ ["lodash.debounce", "npm:4.0.8"],\ ["qrcode.react", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.1.0"],\ @@ -8746,6 +8745,7 @@ const RAW_RUNTIME_STATE = ["react-daum-postcode", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:3.1.3"],\ ["react-dom", "virtual:de80dc576383b2386358abc0e9fe49c00e3397fe355a0337462b73ab3115c2e557eb85784ee0fe776394cc11dd020b4e84dbbd75acf72ee6d54415d82d21f5c5#npm:18.2.0"],\ ["react-dropzone", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:14.2.3"],\ + ["react-error-boundary", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:4.1.2"],\ ["react-hook-form", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:7.50.0"],\ ["react-intersection-observer", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:9.8.0"],\ ["react-pdf", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:9.0.0"],\ @@ -13248,15 +13248,6 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ - ["js-cookie", [\ - ["npm:3.0.5", {\ - "packageLocation": "./.yarn/cache/js-cookie-npm-3.0.5-8fc8fcc9b4-04a0e56040.zip/node_modules/js-cookie/",\ - "packageDependencies": [\ - ["js-cookie", "npm:3.0.5"]\ - ],\ - "linkType": "HARD"\ - }]\ - ]],\ ["js-tokens", [\ ["npm:4.0.0", {\ "packageLocation": "./.yarn/cache/js-tokens-npm-4.0.0-0ac852e9e2-e248708d37.zip/node_modules/js-tokens/",\ @@ -16632,6 +16623,29 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["react-error-boundary", [\ + ["npm:4.1.2", {\ + "packageLocation": "./.yarn/cache/react-error-boundary-npm-4.1.2-7591172537-0737e5259b.zip/node_modules/react-error-boundary/",\ + "packageDependencies": [\ + ["react-error-boundary", "npm:4.1.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:4.1.2", {\ + "packageLocation": "./.yarn/__virtual__/react-error-boundary-virtual-f9f7566544/0/cache/react-error-boundary-npm-4.1.2-7591172537-0737e5259b.zip/node_modules/react-error-boundary/",\ + "packageDependencies": [\ + ["react-error-boundary", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:4.1.2"],\ + ["@babel/runtime", "npm:7.23.8"],\ + ["@types/react", "npm:18.2.48"],\ + ["react", "npm:18.2.0"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["react-fast-compare", [\ ["npm:3.2.2", {\ "packageLocation": "./.yarn/cache/react-fast-compare-npm-3.2.2-45b585a872-0bbd2f3eb4.zip/node_modules/react-fast-compare/",\ diff --git a/apps/admin/package.json b/apps/admin/package.json index 6861f31d..81b30ed1 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -28,7 +28,6 @@ "date-fns": "^3.3.1", "framer-motion": "^11.2.10", "jotai": "^2.8.3", - "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "lodash.debounce": "^4.0.8", "qrcode.react": "^3.1.0", @@ -36,6 +35,7 @@ "react-daum-postcode": "^3.1.3", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-error-boundary": "^4.1.2", "react-hook-form": "^7.50.0", "react-intersection-observer": "^9.8.0", "react-pdf": "^9.0.0", diff --git a/apps/admin/src/atoms/useAuthAtom.ts b/apps/admin/src/atoms/useAuthAtom.ts index dce328cf..25d7fe61 100644 --- a/apps/admin/src/atoms/useAuthAtom.ts +++ b/apps/admin/src/atoms/useAuthAtom.ts @@ -1,7 +1,5 @@ -import Cookies from 'js-cookie'; -import { LOCAL_STORAGE, COOKIES } from '@boolti/api'; +import { LOCAL_STORAGE } from '@boolti/api'; import { atom, useAtom } from 'jotai'; -import { useEffect } from 'react'; const storageMethod = { getItem: (key: string, initialValue: string | null) => { @@ -15,41 +13,9 @@ const storageMethod = { }, }; -const accessTokenAtom = atom( - (() => { - const accessTokenFromCookie = Cookies.get(COOKIES.ACCESS_TOKEN); - const accessTokenFromStorage = storageMethod.getItem(LOCAL_STORAGE.ACCESS_TOKEN, null); +const accessTokenAtom = atom(null); - if (accessTokenFromCookie) { - localStorage.setItem(LOCAL_STORAGE.ACCESS_TOKEN, accessTokenFromCookie); - return accessTokenFromCookie; - } - - if (accessTokenFromStorage) { - return accessTokenFromStorage; - } - - return null; - })(), -); - -const refreshTokenAtom = atom( - (() => { - const refreshTokenFromCookie = Cookies.get(COOKIES.ACCESS_TOKEN); - const refreshTokenFromStorage = storageMethod.getItem(LOCAL_STORAGE.REFRESH_TOKEN, null); - - if (refreshTokenFromCookie) { - localStorage.setItem(LOCAL_STORAGE.REFRESH_TOKEN, refreshTokenFromCookie); - return refreshTokenFromCookie; - } - - if (refreshTokenFromStorage) { - return refreshTokenFromStorage; - } - - return null; - })(), -); +const refreshTokenAtom = atom(null); export const useAuthAtom = () => { const [accessToken, setAccessToken] = useAtom(accessTokenAtom); @@ -65,40 +31,12 @@ export const useAuthAtom = () => { const removeToken = () => { storageMethod.removeItem(LOCAL_STORAGE.ACCESS_TOKEN); storageMethod.removeItem(LOCAL_STORAGE.REFRESH_TOKEN); - Cookies.remove(COOKIES.ACCESS_TOKEN); - Cookies.remove(COOKIES.REFRESH_TOKEN); setAccessToken(null); setRefreshToken(null); }; const isLogin = () => !!accessToken && !!refreshToken; - useEffect(() => { - const handler = ({ key, newValue }: StorageEvent) => { - switch (key) { - case LOCAL_STORAGE.ACCESS_TOKEN: { - setAccessToken(newValue); - newValue - ? Cookies.set(COOKIES.ACCESS_TOKEN, newValue) - : Cookies.remove(COOKIES.ACCESS_TOKEN); - return; - } - case LOCAL_STORAGE.REFRESH_TOKEN: { - setRefreshToken(newValue); - newValue - ? Cookies.set(COOKIES.REFRESH_TOKEN, newValue) - : Cookies.remove(COOKIES.REFRESH_TOKEN); - return; - } - } - }; - window.addEventListener('storage', handler); - - return () => { - window.removeEventListener('storage', handler); - }; - }, [setAccessToken, setRefreshToken]); - return { setToken, removeToken, diff --git a/apps/admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx b/apps/admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx index 7dd711e4..0f98550e 100644 --- a/apps/admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx +++ b/apps/admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx @@ -1,48 +1,62 @@ -import { BooltiHTTPError, LOCAL_STORAGE } from '@boolti/api'; -import React from 'react'; -import { Navigate } from 'react-router-dom'; +import { + BooltiHttpError, + BooltiHttpErrorParams, + LOCAL_STORAGE, + checkIsAuthError, + checkIsHttpError, +} from '@boolti/api'; +import { useNavigate } from 'react-router-dom'; import { PATH } from '../../constants/routes'; -interface AuthErrorBoundaryProps { - children?: React.ReactNode; -} - -interface AuthErrorBoundaryState { - status: BooltiHTTPError['status'] | null; -} - -const initialState: AuthErrorBoundaryState = { - status: null, -}; - -class AuthErrorBoundary extends React.Component { - public state: AuthErrorBoundaryState = initialState; - - public static getDerivedStateFromError(error: Error): AuthErrorBoundaryState { - if (error instanceof BooltiHTTPError) { - return { - status: error.status, - }; - } - - return { - status: null, +import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; +import { checkIsWebView, isWebViewBridgeAvailable, requestToken } from '@boolti/bridge'; +import { useEffect } from 'react'; + +const AuthErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => { + const navigate = useNavigate(); + + useEffect(() => { + const reset = async () => { + if (checkIsAuthError(error)) { + if (checkIsWebView() && isWebViewBridgeAvailable()) { + const token = (await requestToken()).data.token; + localStorage.setItem(LOCAL_STORAGE.ACCESS_TOKEN, token); + resetErrorBoundary(); + } else { + navigate(PATH.LOGIN, { replace: true }); + } + } else { + if (checkIsHttpError(error)) { + let customOptions: BooltiHttpErrorParams['customOptions']; + try { + const body = await error.response.json(); + customOptions = { + errorTraceId: body.errorTraceId, + type: body.type, + detail: body.detail, + }; + } catch { + throw new BooltiHttpError({ + request: error.request, + response: error.response, + options: error.options, + customOptions, + }); + } + } + navigate(PATH.HOME, { replace: true }); + } }; - } - - public render() { - if (this.state.status !== null) { - this.setState(initialState); - window.localStorage.removeItem(LOCAL_STORAGE.ACCESS_TOKEN); - window.localStorage.removeItem(LOCAL_STORAGE.REFRESH_TOKEN); + reset(); + }, []); - return ; - } + return null; +}; - return this.props.children; - } -} +const AuthErrorBoundary = ({ children }: React.PropsWithChildren) => { + return {children}; +}; export default AuthErrorBoundary; diff --git a/apps/admin/src/components/ErrorBoundary/GlobalErrorBoundary.tsx b/apps/admin/src/components/ErrorBoundary/GlobalErrorBoundary.tsx index 9bce1ccc..450029e3 100644 --- a/apps/admin/src/components/ErrorBoundary/GlobalErrorBoundary.tsx +++ b/apps/admin/src/components/ErrorBoundary/GlobalErrorBoundary.tsx @@ -1,4 +1,4 @@ -import { isBooltiHTTPError } from '@boolti/api/src/BooltiHTTPError'; +import { checkIsBooltiHttpError } from '@boolti/api'; import { useEffect } from 'react'; import { Navigate, useRouteError } from 'react-router-dom'; import { PATH } from '~/constants/routes'; @@ -7,7 +7,7 @@ const GlobalErrorBoundary = () => { const error = useRouteError(); useEffect(() => { - if (error instanceof Error && isBooltiHTTPError(error)) { + if (error instanceof Error && checkIsBooltiHttpError(error)) { const errorMessage = '[BooltiHTTPError] errorTraceId:' + error.errorTraceId + '\n'; '[BooltiHTTPError] type' + error.type + '\n'; '[BooltiHTTPError] detail' + error.detail; diff --git a/packages/api/src/BooltiHTTPError.ts b/packages/api/src/BooltiHTTPError.ts index dea710dc..aa92924b 100644 --- a/packages/api/src/BooltiHTTPError.ts +++ b/packages/api/src/BooltiHTTPError.ts @@ -3,36 +3,32 @@ import { HTTPError } from 'ky'; import { ERROR_CODE } from './constants'; -interface BooltiHTTPErrorOptions { +interface BooltiHttpErrorOptions { errorTraceId: string; type: keyof typeof ERROR_CODE; detail: string; } -class BooltiHTTPError extends HTTPError { +export interface BooltiHttpErrorParams { + response: Response; + request: Request; + options: NormalizedOptions; + customOptions?: BooltiHttpErrorOptions; +} + +export class BooltiHttpError extends HTTPError { public errorTraceId?: string; public type?: keyof typeof ERROR_CODE; public detail?: string; public status: number; - constructor( - response: Response, - request: Request, - options: NormalizedOptions, - customOptions?: BooltiHTTPErrorOptions, - ) { + constructor({ request, response, options, customOptions }: BooltiHttpErrorParams) { super(response, request, options); - this.name = 'BooltiHTTPError'; + this.name = 'BooltiHttpError'; this.errorTraceId = customOptions?.errorTraceId; this.type = customOptions?.type; this.detail = customOptions?.detail; this.status = response.status; } } - -export function isBooltiHTTPError(error: Error): error is BooltiHTTPError { - return error.name === 'BooltiHTTPError'; -} - -export default BooltiHTTPError; diff --git a/packages/api/src/QueryClientProvider.tsx b/packages/api/src/QueryClientProvider.tsx index cb4a6c5a..fa9eaf9a 100644 --- a/packages/api/src/QueryClientProvider.tsx +++ b/packages/api/src/QueryClientProvider.tsx @@ -1,8 +1,6 @@ import { QueryClient, QueryClientProvider as BaseQueryClientProvider } from '@tanstack/react-query'; import { useState } from 'react'; -import BooltiHTTPError from './BooltiHTTPError'; - export function QueryClientProvider({ children }: React.PropsWithChildren) { const [queryClient] = useState( () => @@ -12,12 +10,7 @@ export function QueryClientProvider({ children }: React.PropsWithChildren) { refetchOnWindowFocus: false, retry: false, staleTime: 5000, - useErrorBoundary: (error) => { - // 인증 관련 에러일 때만 ErrorBoundary를 사용한다. - return ( - error instanceof BooltiHTTPError && (error.status === 401 || error.status === 403) - ); - }, + useErrorBoundary: true, }, }, }), diff --git a/packages/api/src/constants/errorCode.ts b/packages/api/src/constants/errorCode.ts index 8e84f5ac..49e0bd67 100644 --- a/packages/api/src/constants/errorCode.ts +++ b/packages/api/src/constants/errorCode.ts @@ -11,4 +11,12 @@ export const ERROR_CODE = { type: 'TOKEN_REFRESH_FAILED', status: 400, }, + UNAUTHROIZED: { + type: 'UNAUTHROIZED', + status: 401, + }, + FORBIDDEN: { + type: 'FORBIDDEN', + status: 403, + }, }; diff --git a/packages/api/src/constants/index.ts b/packages/api/src/constants/index.ts index b8e54998..15c54041 100644 --- a/packages/api/src/constants/index.ts +++ b/packages/api/src/constants/index.ts @@ -1,4 +1,4 @@ import { ERROR_CODE } from './errorCode'; -import { LOCAL_STORAGE, COOKIES } from './storages'; +import { LOCAL_STORAGE } from './storages'; -export { ERROR_CODE, LOCAL_STORAGE, COOKIES }; +export { ERROR_CODE, LOCAL_STORAGE }; diff --git a/packages/api/src/constants/storages.ts b/packages/api/src/constants/storages.ts index 7f234569..be4ab2e3 100644 --- a/packages/api/src/constants/storages.ts +++ b/packages/api/src/constants/storages.ts @@ -2,8 +2,3 @@ export const LOCAL_STORAGE = { ACCESS_TOKEN: 'accessToken', REFRESH_TOKEN: 'refreshToken', }; - -export const COOKIES = { - ACCESS_TOKEN: 'x-access-token', - REFRESH_TOKEN: 'x-refresh-token', -}; diff --git a/packages/api/src/fetcher.ts b/packages/api/src/fetcher.ts index c062a614..12c62486 100644 --- a/packages/api/src/fetcher.ts +++ b/packages/api/src/fetcher.ts @@ -1,7 +1,6 @@ import type { Options, ResponsePromise } from 'ky'; import ky, { HTTPError } from 'ky'; -import { isBooltiHTTPError } from './BooltiHTTPError'; import { LOCAL_STORAGE } from './constants'; import { checkIsWebView, isWebViewBridgeAvailable, requestToken } from '@boolti/bridge'; @@ -72,25 +71,16 @@ export const instance = ky.create({ } request.headers.set('Authorization', `Bearer ${newAccessToken}`); - return ky(request, options); } } catch (e) { if (e instanceof HTTPError && e.response.url.includes('/login/refresh')) { window.localStorage.removeItem(LOCAL_STORAGE.ACCESS_TOKEN); window.localStorage.removeItem(LOCAL_STORAGE.REFRESH_TOKEN); - window.dispatchEvent( - new StorageEvent('storage', { - key: LOCAL_STORAGE.REFRESH_TOKEN, - newValue: undefined, - }), - ); - window.dispatchEvent( - new StorageEvent('storage', { - key: LOCAL_STORAGE.ACCESS_TOKEN, - newValue: undefined, - }), - ); + } + + if (e instanceof Error) { + console.warn(`[fether.ts] ${e.name} (${e.message})`); } } } @@ -102,16 +92,7 @@ export const instance = ky.create({ }); export async function resultify(response: ResponsePromise) { - try { - return await response.json(); - } catch (error) { - if (error instanceof Error && isBooltiHTTPError(error)) { - console.error('[BooltiHTTPError] errorTraceId:', error.errorTraceId); - console.error('[BooltiHTTPError] type', error.type); - console.error('[BooltiHTTPError] detail', error.detail); - } - throw error; - } + return await response.json(); } export const fetcher = { diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 9e3c40d3..31bf04aa 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,14 +1,12 @@ import { useQueryClient } from '@tanstack/react-query'; - -import BooltiHTTPError from './BooltiHTTPError'; +export * from './BooltiHttpError'; export { QueryClientProvider } from './QueryClientProvider'; -export { BooltiHTTPError }; - export * from './constants'; export * from './mutations'; export * from './queries'; -export { queryKeys } from './queryKey'; +export * from './utils'; export type * from './types'; +export { queryKeys } from './queryKey'; export { useQueryClient }; diff --git a/packages/api/src/utils/index.ts b/packages/api/src/utils/index.ts new file mode 100644 index 00000000..ae58ac75 --- /dev/null +++ b/packages/api/src/utils/index.ts @@ -0,0 +1,13 @@ +import { HTTPError } from 'ky'; +import { ERROR_CODE } from '../constants'; +import { BooltiHttpError } from '../BooltiHttpError'; + +export const checkIsHttpError = (error: Error): error is HTTPError => error instanceof HTTPError; + +export const checkIsAuthError = (error: HTTPError) => + error.response.status === ERROR_CODE.UNAUTHROIZED.status || + error.response.status === ERROR_CODE.FORBIDDEN.status; + +export function checkIsBooltiHttpError(error: Error): error is BooltiHttpError { + return error.name === 'BooltiHttpError'; +} diff --git a/yarn.lock b/yarn.lock index 483b7538..2c5529b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5486,7 +5486,6 @@ __metadata: date-fns: "npm:^3.3.1" framer-motion: "npm:^11.2.10" jotai: "npm:^2.8.3" - js-cookie: "npm:^3.0.5" jwt-decode: "npm:^4.0.0" lodash.debounce: "npm:^4.0.8" qrcode.react: "npm:^3.1.0" @@ -5494,6 +5493,7 @@ __metadata: react-daum-postcode: "npm:^3.1.3" react-dom: "npm:^18.2.0" react-dropzone: "npm:^14.2.3" + react-error-boundary: "npm:^4.1.2" react-hook-form: "npm:^7.50.0" react-intersection-observer: "npm:^9.8.0" react-pdf: "npm:^9.0.0" @@ -9406,13 +9406,6 @@ __metadata: languageName: node linkType: hard -"js-cookie@npm:^3.0.5": - version: 3.0.5 - resolution: "js-cookie@npm:3.0.5" - checksum: 10c0/04a0e560407b4489daac3a63e231d35f4e86f78bff9d792011391b49c59f721b513411cd75714c418049c8dc9750b20fcddad1ca5a2ca616c3aca4874cce5b3a - languageName: node - linkType: hard - "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" @@ -11886,6 +11879,17 @@ __metadata: languageName: node linkType: hard +"react-error-boundary@npm:^4.1.2": + version: 4.1.2 + resolution: "react-error-boundary@npm:4.1.2" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + react: ">=16.13.1" + checksum: 10c0/0737e5259bed40ce14eb0823b3c7b152171921f2179e604f48f3913490cdc594d6c22d43d7abb4ffb1512c832850228db07aa69d3b941db324953a5e393cb399 + languageName: node + linkType: hard + "react-fast-compare@npm:^3.2.2": version: 3.2.2 resolution: "react-fast-compare@npm:3.2.2"