diff --git a/.pnp.cjs b/.pnp.cjs index ac7f3e3d..d83667df 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -33,6 +33,10 @@ const RAW_RUNTIME_STATE = "name": "@boolti/api",\ "reference": "workspace:packages/api"\ },\ + {\ + "name": "@boolti/bridge",\ + "reference": "workspace:packages/bridge"\ + },\ {\ "name": "@boolti/eslint-config",\ "reference": "workspace:packages/config-eslint"\ @@ -54,6 +58,7 @@ const RAW_RUNTIME_STATE = "ignorePatternData": "(^(?:\\\\.yarn\\\\/sdks(?:\\\\/(?!\\\\.{1,2}(?:\\\\/|$))(?:(?:(?!(?:^|\\\\/)\\\\.{1,2}(?:\\\\/|$)).)*?)|$))$)",\ "fallbackExclusionList": [\ ["@boolti/api", ["workspace:packages/api"]],\ + ["@boolti/bridge", ["workspace:packages/bridge"]],\ ["@boolti/eslint-config", ["workspace:packages/config-eslint"]],\ ["@boolti/icon", ["workspace:packages/icon"]],\ ["@boolti/typescript-config", ["workspace:packages/config-typescript"]],\ @@ -2832,6 +2837,14 @@ const RAW_RUNTIME_STATE = ["regenerator-runtime", "npm:0.14.1"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.26.0", {\ + "packageLocation": "./.yarn/cache/@babel-runtime-npm-7.26.0-9afa3c4ef6-12c01357e0.zip/node_modules/@babel/runtime/",\ + "packageDependencies": [\ + ["@babel/runtime", "npm:7.26.0"],\ + ["regenerator-runtime", "npm:0.14.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@babel/template", [\ @@ -2928,6 +2941,7 @@ const RAW_RUNTIME_STATE = "packageLocation": "./packages/api/",\ "packageDependencies": [\ ["@boolti/api", "workspace:packages/api"],\ + ["@boolti/bridge", "workspace:packages/bridge"],\ ["@boolti/eslint-config", "workspace:packages/config-eslint"],\ ["@boolti/typescript-config", "workspace:packages/config-typescript"],\ ["@emotion/react", "virtual:de80dc576383b2386358abc0e9fe49c00e3397fe355a0337462b73ab3115c2e557eb85784ee0fe776394cc11dd020b4e84dbbd75acf72ee6d54415d82d21f5c5#npm:11.11.3"],\ @@ -2945,6 +2959,21 @@ const RAW_RUNTIME_STATE = "linkType": "SOFT"\ }]\ ]],\ + ["@boolti/bridge", [\ + ["workspace:packages/bridge", {\ + "packageLocation": "./packages/bridge/",\ + "packageDependencies": [\ + ["@boolti/bridge", "workspace:packages/bridge"],\ + ["@boolti/eslint-config", "workspace:packages/config-eslint"],\ + ["@boolti/typescript-config", "workspace:packages/config-typescript"],\ + ["@types/react", "npm:18.2.48"],\ + ["@types/react-dom", "npm:18.2.18"],\ + ["typescript", "patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7"],\ + ["uuid", "npm:11.0.3"]\ + ],\ + "linkType": "SOFT"\ + }]\ + ]],\ ["@boolti/eslint-config", [\ ["workspace:packages/config-eslint", {\ "packageLocation": "./packages/config-eslint/",\ @@ -8687,6 +8716,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["admin", "workspace:apps/admin"],\ ["@boolti/api", "workspace:packages/api"],\ + ["@boolti/bridge", "workspace:packages/bridge"],\ ["@boolti/eslint-config", "workspace:packages/config-eslint"],\ ["@boolti/icon", "workspace:packages/icon"],\ ["@boolti/typescript-config", "workspace:packages/config-typescript"],\ @@ -8708,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"],\ @@ -8716,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"],\ @@ -8724,6 +8754,7 @@ const RAW_RUNTIME_STATE = ["react-tooltip", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:5.26.3"],\ ["the-new-css-reset", "npm:1.11.2"],\ ["typescript", "patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7"],\ + ["vconsole", "npm:3.15.1"],\ ["vite", "virtual:9845906954fdbefbb879db24fa8772d77a945dca59f459806df47a5b67245d4bc6502880b373cca7201062c81bea9f13f699f52de2004c037e79dbdbd5d97fb3#npm:5.0.11"]\ ],\ "linkType": "SOFT"\ @@ -10032,6 +10063,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["copy-text-to-clipboard", [\ + ["npm:3.2.0", {\ + "packageLocation": "./.yarn/cache/copy-text-to-clipboard-npm-3.2.0-46c47374b9-d60fdadc59.zip/node_modules/copy-text-to-clipboard/",\ + "packageDependencies": [\ + ["copy-text-to-clipboard", "npm:3.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["copy-to-clipboard", [\ ["npm:3.3.3", {\ "packageLocation": "./.yarn/cache/copy-to-clipboard-npm-3.3.3-6964e6cfad-3ebf5e8ee0.zip/node_modules/copy-to-clipboard/",\ @@ -10042,6 +10082,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["core-js", [\ + ["npm:3.39.0", {\ + "packageLocation": "./.yarn/unplugged/core-js-npm-3.39.0-4c420e59a7/node_modules/core-js/",\ + "packageDependencies": [\ + ["core-js", "npm:3.39.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["core-js-compat", [\ ["npm:3.35.1", {\ "packageLocation": "./.yarn/cache/core-js-compat-npm-3.35.1-1088e0320e-c3b872e1f9.zip/node_modules/core-js-compat/",\ @@ -13199,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/",\ @@ -14102,6 +14142,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["mutation-observer", [\ + ["npm:1.0.3", {\ + "packageLocation": "./.yarn/cache/mutation-observer-npm-1.0.3-fa3b236d74-2f010fdec4.zip/node_modules/mutation-observer/",\ + "packageDependencies": [\ + ["mutation-observer", "npm:1.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["nan", [\ ["npm:2.20.0", {\ "packageLocation": "./.yarn/unplugged/nan-npm-2.20.0-5b5be83e88/node_modules/nan/",\ @@ -16574,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/",\ @@ -19233,6 +19305,13 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["uuid", [\ + ["npm:11.0.3", {\ + "packageLocation": "./.yarn/cache/uuid-npm-11.0.3-abcb5b16c0-cee762fc76.zip/node_modules/uuid/",\ + "packageDependencies": [\ + ["uuid", "npm:11.0.3"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:9.0.1", {\ "packageLocation": "./.yarn/cache/uuid-npm-9.0.1-39a8442bc6-1607dd32ac.zip/node_modules/uuid/",\ "packageDependencies": [\ @@ -19261,6 +19340,19 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["vconsole", [\ + ["npm:3.15.1", {\ + "packageLocation": "./.yarn/cache/vconsole-npm-3.15.1-329eac4d95-1e62132b71.zip/node_modules/vconsole/",\ + "packageDependencies": [\ + ["vconsole", "npm:3.15.1"],\ + ["@babel/runtime", "npm:7.26.0"],\ + ["copy-text-to-clipboard", "npm:3.2.0"],\ + ["core-js", "npm:3.39.0"],\ + ["mutation-observer", "npm:1.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["vite", [\ ["npm:5.0.11", {\ "packageLocation": "./.yarn/cache/vite-npm-5.0.11-d5457a8b86-74a3ddc6d4.zip/node_modules/vite/",\ diff --git a/README.md b/README.md index 5b9b98b1..f8f7f0a7 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,4 @@ - `packages/config-typescript`: 각 패키지에서 공통적으로 사용될 TypeScript 관련 설정이 포함된 패키지입니다. - `packages/icon`: 공통적으로 사용될 아이콘 컴포넌트가 포함된 패키지입니다. - `packages/ui`: 공통적으로 사용될 디자인 컴포넌트가 포함된 패키지입니다. +- `packages/ui`: 공통적으로 사용될 웹뷰 브릿지가 포함된 패키지입니다. diff --git a/apps/admin/package.json b/apps/admin/package.json index cc43cfd1..dffe1138 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -6,13 +6,14 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives", "lint:fix": "TIMING=1 eslint . --ext ts,tsx --fix", "type-check": "tsc --noEmit", "preview": "vite preview" }, "dependencies": { "@boolti/api": "*", + "@boolti/bridge": "*", "@boolti/icon": "*", "@boolti/ui": "*", "@dnd-kit/core": "^6.1.0", @@ -27,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", @@ -35,13 +35,15 @@ "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", "react-router-dom": "^6.21.3", "react-select": "^5.8.0", "react-tooltip": "^5.26.3", - "the-new-css-reset": "^1.11.2" + "the-new-css-reset": "^1.11.2", + "vconsole": "^3.15.1" }, "devDependencies": { "@boolti/eslint-config": "*", diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index af0f7402..7e2dc8a5 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -40,9 +40,13 @@ import ShowTicketPage from './pages/ShowTicketPage'; import ShowReservationPage from './pages/ShowReservationPage'; import ShowSettlementPage from './pages/ShowSettlementPage'; import ShowEnterancePage from './pages/ShowEnterancePage'; +import { initVConsole } from './utils/vConsole'; +import { checkIsWebView } from '@boolti/bridge'; setDefaultOptions({ locale: ko }); +initVConsole(); + const publicRoutes = [ { element: ( @@ -97,7 +101,7 @@ const publicRoutes = [ const PrivateRoute = () => { const { isLogin } = useAuthAtom(); - if (!isLogin()) { + if (!isLogin() && !checkIsWebView()) { return ; } 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..f2adf7aa 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 { + CustomHttpError, + CustomHttpErrorParams, + 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: CustomHttpErrorParams['customOptions']; + try { + const body = await error.response.json(); + customOptions = { + errorTraceId: body.errorTraceId, + type: body.type, + detail: body.detail, + }; + } catch { + throw new CustomHttpError({ + 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..f19ae8b3 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 { checkIsCustomHttpError } from '@boolti/api'; import { useEffect } from 'react'; import { Navigate, useRouteError } from 'react-router-dom'; import { PATH } from '~/constants/routes'; @@ -7,10 +7,10 @@ const GlobalErrorBoundary = () => { const error = useRouteError(); useEffect(() => { - if (error instanceof Error && isBooltiHTTPError(error)) { - const errorMessage = '[BooltiHTTPError] errorTraceId:' + error.errorTraceId + '\n'; - '[BooltiHTTPError] type' + error.type + '\n'; - '[BooltiHTTPError] detail' + error.detail; + if (error instanceof Error && checkIsCustomHttpError(error)) { + const errorMessage = '[CustomHttpError] errorTraceId:' + error.errorTraceId + '\n'; + '[CustomHttpError] type' + error.type + '\n'; + '[CustomHttpError] detail' + error.detail; console.error(errorMessage); return; } diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx index 4fd25558..2b2e8825 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowBasicInfoFormContent.tsx @@ -12,7 +12,7 @@ import { ShowBasicInfoFormInputs } from './types'; import { useBodyScrollLock } from '~/hooks/useBodyScrollLock'; const MAX_IMAGE_COUNT = 3; -const MIN_DATE = format(add(new Date(), { days: 1 }), 'yyyy-MM-dd') +const MIN_DATE = format(add(new Date(), { days: 1 }), 'yyyy-MM-dd'); interface ShowBasicInfoFormContentProps { form: UseFormReturn; @@ -32,7 +32,13 @@ const ShowBasicInfoFormContent = ({ const { open, close, isOpen } = useDialog(); const detailAddressInputRef = useRef(null); - const { control, setValue, formState: { errors }, setError, clearErrors } = form; + const { + control, + setValue, + formState: { errors }, + setError, + clearErrors, + } = form; const { getRootProps, getInputProps } = useDropzone({ accept: { @@ -76,8 +82,11 @@ const ShowBasicInfoFormContent = ({ 공연 포스터 - 원하시는 노출 순서대로 이미지를 업로드해주세요.  - 표준 종이규격(A, B)의 이미지를 권장합니다.
+ + 원하시는 노출 순서대로 이미지를 업로드해주세요.  + + 표준 종이규격(A, B)의 이미지를 권장합니다. +
(최소 1장, 최대 {MAX_IMAGE_COUNT}장 업로드 가능 / jpg, png 형식)
@@ -142,7 +151,7 @@ const ShowBasicInfoFormContent = ({ if (!value) { setError('name', { type: 'required', message: '필수 입력사항입니다.' }); - return + return; } }} value={value ?? ''} @@ -174,7 +183,7 @@ const ShowBasicInfoFormContent = ({ if (new Date(event.target.value) < new Date(MIN_DATE)) { setError('date', { type: 'min', message: '오늘 이후부터 선택 가능합니다.' }); - return + return; } }} onBlur={() => { @@ -182,7 +191,7 @@ const ShowBasicInfoFormContent = ({ if (!value) { setError('date', { type: 'required', message: '필수 입력사항입니다.' }); - return + return; } }} placeholder={value} @@ -219,8 +228,11 @@ const ShowBasicInfoFormContent = ({ onBlur(); if (!value) { - setError('startTime', { type: 'required', message: '필수 입력사항입니다.' }); - return + setError('startTime', { + type: 'required', + message: '필수 입력사항입니다.', + }); + return; } }} value={value} @@ -255,8 +267,11 @@ const ShowBasicInfoFormContent = ({ onBlur(); if (!value) { - setError('runningTime', { type: 'required', message: '필수 입력사항입니다.' }); - return + setError('runningTime', { + type: 'required', + message: '필수 입력사항입니다.', + }); + return; } }} value={value ?? ''} @@ -294,7 +309,7 @@ const ShowBasicInfoFormContent = ({ if (!value) { setError('placeName', { type: 'required', message: '필수 입력사항입니다.' }); - return + return; } }} value={value ?? ''} @@ -356,8 +371,11 @@ const ShowBasicInfoFormContent = ({ onBlur(); if (!value) { - setError('placeDetailAddress', { type: 'required', message: '필수 입력사항입니다.' }); - return + setError('placeDetailAddress', { + type: 'required', + message: '필수 입력사항입니다.', + }); + return; } }} value={value ?? ''} diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx index 8747d04a..499c2caa 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowDetailInfoFormContent.tsx @@ -9,10 +9,15 @@ interface ShowDetailInfoFormContentProps { disabled?: boolean; } -const phoneNumberRegExp = /^\d{3}-\d{3,4}-\d{4}$/ +const phoneNumberRegExp = /^\d{3}-\d{3,4}-\d{4}$/; const ShowDetailInfoFormContent = ({ form, disabled }: ShowDetailInfoFormContentProps) => { - const { control, formState: { errors }, setError, clearErrors } = form; + const { + control, + formState: { errors }, + setError, + clearErrors, + } = form; return ( @@ -70,10 +75,10 @@ const ShowDetailInfoFormContent = ({ form, disabled }: ShowDetailInfoFormContent rules={{ required: true, validate: (fieldValue) => { - if (!fieldValue) return '필수 입력사항입니다.' + if (!fieldValue) return '필수 입력사항입니다.'; - return true - } + return true; + }, }} render={({ field: { onChange, onBlur, value } }) => ( { onChange(event); - clearErrors('hostName') + clearErrors('hostName'); }} onBlur={() => { onBlur(); @@ -117,26 +122,33 @@ const ShowDetailInfoFormContent = ({ form, disabled }: ShowDetailInfoFormContent required disabled={disabled} onChange={(event) => { - if (event.target.value.length > 13) return + if (event.target.value.length > 13) return; event.target.value = event.target.value .replace(/[^0-9]/g, '') - .replace(/^(\d{0,3})(\d{0,4})(\d{0,4})$/g, '$1-$2-$3').replace(/(-{1,2})$/g, '') + .replace(/^(\d{0,3})(\d{0,4})(\d{0,4})$/g, '$1-$2-$3') + .replace(/(-{1,2})$/g, ''); onChange(event); - clearErrors('hostPhoneNumber') + clearErrors('hostPhoneNumber'); }} onBlur={() => { onBlur(); if (!value) { - setError('hostPhoneNumber', { type: 'required', message: '필수 입력사항입니다.' }); - return + setError('hostPhoneNumber', { + type: 'required', + message: '필수 입력사항입니다.', + }); + return; } if (!phoneNumberRegExp.test(value)) { - setError('hostPhoneNumber', { type: 'pattern', message: '유효한 전화번호 형식이 아닙니다.' }); - return + setError('hostPhoneNumber', { + type: 'pattern', + message: '유효한 전화번호 형식이 아닙니다.', + }); + return; } }} value={value ?? ''} diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts b/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts index 5cf5719f..7d04170a 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts +++ b/apps/admin/src/components/ShowInfoFormContent/ShowInfoFormContent.styles.ts @@ -89,7 +89,7 @@ const ShowInfoFormResponsiveRowColumn = styled.div` &:last-of-type { margin-bottom: 0; } -` +`; const ShowInfoFormContent = styled.div` flex: 1; @@ -137,7 +137,7 @@ const ShowInfoFormButtonContainer = styled.div` gap: 8px; `; -const ShowInfoFormButton = styled(Button) ` +const ShowInfoFormButton = styled(Button)` width: ${({ width }) => width}; `; @@ -305,7 +305,7 @@ const TextArea = styled.textarea` padding: 12px; border: 1px solid ${({ theme, hasError }) => - hasError ? `${theme.palette.status.error} !important` : theme.palette.grey.g20}; + hasError ? `${theme.palette.status.error} !important` : theme.palette.grey.g20}; border-radius: 4px; background-color: ${({ theme }) => theme.palette.grey.w}; color: ${({ theme }) => theme.palette.grey.g90}; @@ -562,7 +562,7 @@ const MobileTicketAction = styled.div` width: 24px; height: 24px; stroke: ${({ theme, disabled }) => - disabled ? theme.palette.grey.g40 : theme.palette.grey.g90}; + disabled ? theme.palette.grey.g40 : theme.palette.grey.g90}; } } } diff --git a/apps/admin/src/components/ShowInfoFormContent/ShowTicketInfoFormContent.tsx b/apps/admin/src/components/ShowInfoFormContent/ShowTicketInfoFormContent.tsx index 884b9ff5..30be7382 100644 --- a/apps/admin/src/components/ShowInfoFormContent/ShowTicketInfoFormContent.tsx +++ b/apps/admin/src/components/ShowInfoFormContent/ShowTicketInfoFormContent.tsx @@ -21,64 +21,77 @@ const ShowTicketInfoFormContent = ({ salesStartTime, disabled, }: ShowTicketInfoFormContentProps) => { - const { watch, control, formState: { errors }, setError, clearErrors } = form; - - const minStartDate = format(showCreatedAt ?? new Date(), 'yyyy-MM-dd') + const { + watch, + control, + formState: { errors }, + setError, + clearErrors, + } = form; + + const minStartDate = format(showCreatedAt ?? new Date(), 'yyyy-MM-dd'); const minEndDate = format( - watch('startDate') || - (salesStartTime ? new Date(salesStartTime) : new Date()), + watch('startDate') || (salesStartTime ? new Date(salesStartTime) : new Date()), 'yyyy-MM-dd', - ) + ); const maxDate = format( sub(showDate ? new Date(showDate) : new Date(), { days: 1 }), 'yyyy-MM-dd', - ) - - const validateStartDate = useCallback((value: string) => { - if (!value) { - setError('startDate', { type: 'required', message: '필수 입력사항입니다.' }); - return - } - - if (new Date(value) > new Date(maxDate)) { - setError('startDate', { type: 'max', message: '공연일 이전까지 선택 가능합니다.' }); - return - } - - if (new Date(value) < new Date(minStartDate)) { - const message = showCreatedAt ? `공연 등록일부터 선택 가능합니다. (${format(showCreatedAt, 'yy.MM.dd')})` : '오늘부터 선택 가능합니다.'; - setError('startDate', { type: 'min', message }); - return - } - - clearErrors('startDate') - }, [clearErrors, maxDate, minStartDate, setError, showCreatedAt]) - - const validateEndDate = useCallback((value: string) => { - if (!value) { - setError('endDate', { type: 'required', message: '필수 입력사항입니다.' }); - return - } - - if (new Date(value) > new Date(maxDate)) { - setError('endDate', { type: 'max', message: '공연일 이전까지 선택 가능합니다.' }); - return - } - - if (new Date(value) < new Date(minEndDate)) { - setError('endDate', { type: 'min', message: '시작일부터 선택 가능합니다.' }); - return - } - - clearErrors('endDate') - }, [clearErrors, maxDate, minEndDate, setError]) + ); + + const validateStartDate = useCallback( + (value: string) => { + if (!value) { + setError('startDate', { type: 'required', message: '필수 입력사항입니다.' }); + return; + } + + if (new Date(value) > new Date(maxDate)) { + setError('startDate', { type: 'max', message: '공연일 이전까지 선택 가능합니다.' }); + return; + } + + if (new Date(value) < new Date(minStartDate)) { + const message = showCreatedAt + ? `공연 등록일부터 선택 가능합니다. (${format(showCreatedAt, 'yy.MM.dd')})` + : '오늘부터 선택 가능합니다.'; + setError('startDate', { type: 'min', message }); + return; + } + + clearErrors('startDate'); + }, + [clearErrors, maxDate, minStartDate, setError, showCreatedAt], + ); + + const validateEndDate = useCallback( + (value: string) => { + if (!value) { + setError('endDate', { type: 'required', message: '필수 입력사항입니다.' }); + return; + } + + if (new Date(value) > new Date(maxDate)) { + setError('endDate', { type: 'max', message: '공연일 이전까지 선택 가능합니다.' }); + return; + } + + if (new Date(value) < new Date(minEndDate)) { + setError('endDate', { type: 'min', message: '시작일부터 선택 가능합니다.' }); + return; + } + + clearErrors('endDate'); + }, + [clearErrors, maxDate, minEndDate, setError], + ); useEffect(() => { if (!watch('startDate') || !watch('endDate')) return; validateStartDate(watch('startDate')); validateEndDate(watch('endDate')); - }, [validateEndDate, validateStartDate, watch]) + }, [validateEndDate, validateStartDate, watch]); return ( diff --git a/apps/admin/src/pages/HomePage/HomePage.styles.ts b/apps/admin/src/pages/HomePage/HomePage.styles.ts index 27500ca6..d340bca8 100644 --- a/apps/admin/src/pages/HomePage/HomePage.styles.ts +++ b/apps/admin/src/pages/HomePage/HomePage.styles.ts @@ -49,8 +49,8 @@ const Container = styled.main` `; const BannerContainer = styled.div` - border-bottom: 1px solid #C5E1FF; -` + border-bottom: 1px solid #c5e1ff; +`; const Banner = styled.div` max-width: ${({ theme }) => theme.breakpoint.desktop}; diff --git a/apps/admin/src/pages/ShowAddCompletePage/index.tsx b/apps/admin/src/pages/ShowAddCompletePage/index.tsx index c4887f74..b8b455a1 100644 --- a/apps/admin/src/pages/ShowAddCompletePage/index.tsx +++ b/apps/admin/src/pages/ShowAddCompletePage/index.tsx @@ -1,12 +1,12 @@ import { ArrowLeftIcon } from '@boolti/icon'; import { Button } from '@boolti/ui'; +import { checkIsWebView } from '@boolti/bridge'; import { useNavigate } from 'react-router-dom'; import congratulationSvgUrl from '~/assets/svg/congratulation.svg'; import { PATH } from '~/constants/routes'; import Styled from './ShowAddCompletePage.styles'; -import { checkIsWebView } from '~/utils/webview'; const ShowAddCompletePage = () => { const navigate = useNavigate(); diff --git a/apps/admin/src/pages/ShowAddPage/index.tsx b/apps/admin/src/pages/ShowAddPage/index.tsx index f4bd519c..384746e0 100644 --- a/apps/admin/src/pages/ShowAddPage/index.tsx +++ b/apps/admin/src/pages/ShowAddPage/index.tsx @@ -19,27 +19,31 @@ import ShowSalesTicketFormContent, { SalesTicket, } from '~/components/ShowInfoFormContent/ShowSalesTicketFormContent'; import ShowTicketInfoFormContent from '~/components/ShowInfoFormContent/ShowTicketInfoFormContent'; -import { ShowBasicInfoFormInputs, ShowDetailInfoFormInputs, ShowSalesInfoFormInputs } from '~/components/ShowInfoFormContent/types'; +import { + ShowBasicInfoFormInputs, + ShowDetailInfoFormInputs, + ShowSalesInfoFormInputs, +} from '~/components/ShowInfoFormContent/types'; import { PATH } from '~/constants/routes'; import Styled from './ShowAddPage.styles'; import ShowCastInfoFormContent from '~/components/ShowInfoFormContent/ShowCastInfoFormContent'; import { TempShowCastInfoFormInput } from '~/components/ShowCastInfoFormDialogContent'; -import { checkIsWebView } from '~/utils/webview'; +import { checkIsWebView, isWebViewBridgeAvailable, navigateToShowDetail } from '@boolti/bridge'; const stepItems = [ { key: 'basic', title: '기본 정보' }, { key: 'detail', title: '상세 정보' }, { key: 'sales', title: '판매 정보' }, -] +]; interface ShowAddPageProps { - step: 'basic' | 'detail' | 'sales' + step: 'basic' | 'detail' | 'sales'; } const ShowAddPage = ({ step }: ShowAddPageProps) => { const navigate = useNavigate(); - const isWebView = checkIsWebView(window.navigator.userAgent); + const isWebView = checkIsWebView(); const [imageFiles, setImageFiles] = useState([]); const [castTeamList, setCastTeamList] = useState([]); @@ -73,7 +77,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { const showImageInfo = await uploadShowImageMutation.mutateAsync(imageFiles); // 공연 생성 - await addShowMutation.mutateAsync({ + const showId = await addShowMutation.mutateAsync({ name: showBasicInfoForm.getValues('name'), images: showImageInfo, date: `${showBasicInfoForm.getValues('date')}T${showBasicInfoForm.getValues('startTime')}:00.000Z`, @@ -111,6 +115,11 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { })) as ShowCastTeamCreateOrUpdateRequest[], }); + if (isWebView && isWebViewBridgeAvailable()) { + navigateToShowDetail({ showId }); + return; + } + navigate(PATH.SHOW_ADD_COMPLETE); }; @@ -156,15 +165,13 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { - ) + ); const detailStepContent = ( <> - { - (!showBasicInfoForm.formState.isDirty || !showBasicInfoForm.formState.isValid) && ( - - ) - } + {(!showBasicInfoForm.formState.isDirty || !showBasicInfoForm.formState.isValid) && ( + + )} 공연의 상세 정보를 입력해 주세요.
@@ -199,8 +206,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { colorTheme="primary" size="bold" disabled={ - !showDetailInfoForm.formState.isDirty || - !showDetailInfoForm.formState.isValid + !showDetailInfoForm.formState.isDirty || !showDetailInfoForm.formState.isValid } > 다음으로 @@ -208,7 +214,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { - ) + ); const salesStepContent = ( <> @@ -256,8 +262,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { description={ <> 초청 티켓 이용을 원하시면 티켓을 생성해주세요. -
* 초청 코드는 공연 등록 후{' '} - 공연 관리 > 티켓 관리 +
* 초청 코드는 공연 등록 후 공연 관리 > 티켓 관리 에서 확인할 수 있습니다. } @@ -288,7 +293,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { variant="main" checked={isTermsAccepted} onChange={(event) => { - setIsTermsAccepted(event.target.checked) + setIsTermsAccepted(event.target.checked); }} /> 정책 확인 및 약관 동의 @@ -298,37 +303,40 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { - + [필수] 공연 등록 및 관리 이용 약관 - + 보기 - + [필수] 수수료 정책 - + 보기 - + [필수] 환불 정책 - + 보기 @@ -363,7 +371,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { - ) + ); const showAddPageContent = ( <> @@ -374,7 +382,7 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { {step === 'detail' && detailStepContent} {step === 'sales' && salesStepContent} - ) + ); return ( @@ -411,14 +419,10 @@ const ShowAddPage = ({ step }: ShowAddPageProps) => { 공연 등록 - - {showAddPageContent} - + {showAddPageContent} - - {showAddPageContent} - + {showAddPageContent} ); }; diff --git a/apps/admin/src/utils/vConsole.ts b/apps/admin/src/utils/vConsole.ts new file mode 100644 index 00000000..3991c965 --- /dev/null +++ b/apps/admin/src/utils/vConsole.ts @@ -0,0 +1,17 @@ +import { checkIsWebView } from '@boolti/bridge'; +import type vConsole from 'vconsole'; + +let vConsoleObject: vConsole | undefined; + +export const initVConsole = async () => { + if (checkIsWebView()) { + const { default: vConsole } = await import('vconsole'); + vConsoleObject = new vConsole({}); + } +}; + +export const destroyVConsole = () => { + if (vConsoleObject) { + vConsoleObject.destroy(); + } +}; diff --git a/apps/admin/src/utils/webview.ts b/apps/admin/src/utils/webview.ts deleted file mode 100644 index 917e7f86..00000000 --- a/apps/admin/src/utils/webview.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const WEBVIEW_REGEX = /BOOLTI\/(ANDROID|IOS)/; -export const OS_REGEX = /(?<=BOOLTI\/).*/; - -export const checkIsWebView = (userAgent: string) => WEBVIEW_REGEX.test(userAgent); - -export const getWebViewOS = (userAgent: string) => { - const regexResult = OS_REGEX.exec(userAgent); - return regexResult === null ? undefined : regexResult[0]; -}; - -export const checkIsAndroid = (userAgent: string) => { - if (!checkIsWebView(userAgent)) return false; - return getWebViewOS(userAgent) === 'ANDROID'; -}; - -export const checkIsIOS = (userAgent: string) => { - if (!checkIsWebView(userAgent)) return false; - return getWebViewOS(userAgent) === 'IOS'; -}; diff --git a/apps/preview/package.json b/apps/preview/package.json index 7d75aed5..a2b993c6 100644 --- a/apps/preview/package.json +++ b/apps/preview/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "TIMING=1 eslint . --ext ts,tsx --max-warnings 0", + "lint": "TIMING=1 eslint . --ext ts,tsx", "lint:fix": "TIMING=1 eslint . --ext ts,tsx --fix", "type-check": "tsc --noEmit", "preview": "vite preview" diff --git a/apps/preview/src/pages/ShowPreviewPage/index.tsx b/apps/preview/src/pages/ShowPreviewPage/index.tsx index b9d069be..869bdc1f 100644 --- a/apps/preview/src/pages/ShowPreviewPage/index.tsx +++ b/apps/preview/src/pages/ShowPreviewPage/index.tsx @@ -13,22 +13,31 @@ setDefaultOptions({ locale: ko }); const getDynamicLink = (showId: number) => { return `https://boolti.page.link/?link=https://preview.boolti.in/show/${showId}&apn=com.nexters.boolti&ibi=com.nexters.boolti&isi=6476589322`; -} +}; const getPreviewLink = (showId: number) => { - return `${window.location.origin}/show/${showId}` -} - -const getShareText = (show: { id: number, title: string, date: Date, placeName: string, streetAddress: string, detailAddress: string }) => { - return `공연 정보를 공유드려요! - -- 공연명 : ${show.title} -- 일시 : ${format(show.date, 'yyyy.MM.dd (E) / HH:mm -', { locale: ko })} -- 장소 : ${show.placeName} / ${show.streetAddress}, ${show.detailAddress} + return `${window.location.origin}/show/${showId}`; +}; -공연 상세 정보 ▼ -${getPreviewLink(show.id)}` -} +const getShareText = (show: { + id: number; + title: string; + date: Date; + placeName: string; + streetAddress: string; + detailAddress: string; +}) => { + return ( + '공연 정보를 공유드려요!\n' + + '\n' + + `- 공연명 : ${show.title}\n` + + `- 일시 : ${format(show.date, 'yyyy.MM.dd (E) / HH:mm -', { locale: ko })}\n` + + `- 장소 : ${show.placeName} / ${show.streetAddress}, ${show.detailAddress}\n` + + '\n' + + '공연 상세 정보 ▼\n' + + `${getPreviewLink(show.id)}` + ); +}; const ShowPreviewPage = () => { const loaderData = useLoaderData() as @@ -62,7 +71,14 @@ const ShowPreviewPage = () => { const shareButtonClickHandler = async () => { if (navigator.share) { await navigator.share({ - text: getShareText({ id, title, date: new Date(date), placeName, streetAddress, detailAddress }), + text: getShareText({ + id, + title, + date: new Date(date), + placeName, + streetAddress, + detailAddress, + }), }); } else { await navigator.clipboard.writeText(getPreviewLink(id)); diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 244cb0c4..61355e13 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -6,7 +6,7 @@ "main": ".storybook/main.ts", "types": ".storybook/main.ts", "scripts": { - "lint": "TIMING=1 eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "TIMING=1 eslint . --ext ts,tsx --report-unused-disable-directives", "lint:fix": "TIMING=1 eslint . --ext ts,tsx --fix", "type-check": "tsc --noEmit", "storybook": "storybook dev -p 6006", diff --git a/apps/super-admin/package.json b/apps/super-admin/package.json index 0a2abdf5..6faadd0a 100644 --- a/apps/super-admin/package.json +++ b/apps/super-admin/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "TIMING=1 eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "TIMING=1 eslint . --ext ts,tsx --report-unused-disable-directives", "lint:fix": "TIMING=1 eslint . --ext ts,tsx --fix", "type-check": "tsc --noEmit", "preview": "vite preview" diff --git a/apps/super-admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx b/apps/super-admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx index 7dd711e4..56800a1b 100644 --- a/apps/super-admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx +++ b/apps/super-admin/src/components/ErrorBoundary/AuthErrorBoundary.tsx @@ -1,4 +1,4 @@ -import { BooltiHTTPError, LOCAL_STORAGE } from '@boolti/api'; +import { CustomHttpError, LOCAL_STORAGE } from '@boolti/api'; import React from 'react'; import { Navigate } from 'react-router-dom'; @@ -9,7 +9,7 @@ interface AuthErrorBoundaryProps { } interface AuthErrorBoundaryState { - status: BooltiHTTPError['status'] | null; + status: CustomHttpError['status'] | null; } const initialState: AuthErrorBoundaryState = { @@ -20,7 +20,7 @@ class AuthErrorBoundary extends React.Component @@ -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 9793e6d3..12c62486 100644 --- a/packages/api/src/fetcher.ts +++ b/packages/api/src/fetcher.ts @@ -1,8 +1,8 @@ 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'; const API_URL = import.meta.env.VITE_BASE_API_URL; const IS_SUPER_ADMIN = import.meta.env.VITE_IS_SUPER_ADMIN === 'true'; @@ -52,32 +52,35 @@ export const instance = ky.create({ async (request, options, response) => { // access token이 만료되었을 때, refresh token으로 새로운 access token을 발급받는다. if (!response.ok && response.status === 401 && !request.url.includes('logout')) { + let newAccessToken: string | undefined = undefined, + newRefreshToken: string | undefined = undefined; try { - const { accessToken, refreshToken } = (await postRefreshToken()) ?? {}; - if (accessToken && refreshToken) { - window.localStorage.setItem(LOCAL_STORAGE.ACCESS_TOKEN, accessToken); - window.localStorage.setItem(LOCAL_STORAGE.REFRESH_TOKEN, refreshToken); + if (checkIsWebView() && isWebViewBridgeAvailable()) { + newAccessToken = (await requestToken()).data.token; + } else { + const { accessToken, refreshToken } = (await postRefreshToken()) ?? {}; + newAccessToken = accessToken; + newRefreshToken = refreshToken; + } + + if (newAccessToken) { + window.localStorage.setItem(LOCAL_STORAGE.ACCESS_TOKEN, newAccessToken); - request.headers.set('Authorization', `Bearer ${accessToken}`); + if (newRefreshToken) { + window.localStorage.setItem(LOCAL_STORAGE.REFRESH_TOKEN, newRefreshToken); + } + 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})`); } } } @@ -89,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..0d61d32d 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 './CustomHttpError'; 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..3b41e73e --- /dev/null +++ b/packages/api/src/utils/index.ts @@ -0,0 +1,13 @@ +import { HTTPError } from 'ky'; +import { ERROR_CODE } from '../constants'; +import { CustomHttpError } from '../CustomHttpError'; + +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 checkIsCustomHttpError(error: Error): error is CustomHttpError { + return error.name === 'CustomHttpError'; +} diff --git a/packages/bridge/package.json b/packages/bridge/package.json new file mode 100644 index 00000000..7271e5ba --- /dev/null +++ b/packages/bridge/package.json @@ -0,0 +1,23 @@ +{ + "name": "@boolti/bridge", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives", + "lint:fix": "TIMING=1 eslint . --ext ts,tsx --fix", + "type-check": "tsc --noEmit" + }, + "devDependencies": { + "@boolti/eslint-config": "*", + "@boolti/typescript-config": "*", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "typescript": "^5.2.2" + }, + "dependencies": { + "uuid": "^11.0.3" + } +} diff --git a/packages/bridge/src/commands/index.ts b/packages/bridge/src/commands/index.ts new file mode 100644 index 00000000..cd1fe555 --- /dev/null +++ b/packages/bridge/src/commands/index.ts @@ -0,0 +1,3 @@ +export * from './navigateBack'; +export * from './navigateToShowDetail'; +export * from './requestToken'; diff --git a/packages/bridge/src/commands/messageListeners.ts b/packages/bridge/src/commands/messageListeners.ts new file mode 100644 index 00000000..3a1096db --- /dev/null +++ b/packages/bridge/src/commands/messageListeners.ts @@ -0,0 +1,12 @@ +import { ResponseListener } from '../types'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const messageListeners: Map> = new Map(); + +export const subscribe = (id: string, listener: ResponseListener) => { + messageListeners.set(id, listener); +}; + +export const unsubscribe = (id: string) => { + messageListeners.delete(id); +}; diff --git a/packages/bridge/src/commands/navigateBack.ts b/packages/bridge/src/commands/navigateBack.ts new file mode 100644 index 00000000..f66ca412 --- /dev/null +++ b/packages/bridge/src/commands/navigateBack.ts @@ -0,0 +1,5 @@ +import { sendCommand } from './sendCommand'; + +export const navigateBack = () => { + return sendCommand({ command: 'NAVIGATE_BACK' }); +}; diff --git a/packages/bridge/src/commands/navigateToShowDetail.ts b/packages/bridge/src/commands/navigateToShowDetail.ts new file mode 100644 index 00000000..97721164 --- /dev/null +++ b/packages/bridge/src/commands/navigateToShowDetail.ts @@ -0,0 +1,7 @@ +import { sendCommand } from './sendCommand'; + +export type NavigateToShowDetailRequestData = { showId: number }; + +export const navigateToShowDetail = (data: NavigateToShowDetailRequestData) => { + return sendCommand({ command: 'NAVIGATE_TO_SHOW_DETAIL', data }); +}; diff --git a/packages/bridge/src/commands/requestToken.ts b/packages/bridge/src/commands/requestToken.ts new file mode 100644 index 00000000..52d1fdf1 --- /dev/null +++ b/packages/bridge/src/commands/requestToken.ts @@ -0,0 +1,7 @@ +import { sendCommand } from './sendCommand'; + +export type RequestTokenResponseData = { token: string }; + +export const requestToken = () => { + return sendCommand({ command: 'REQUEST_TOKEN' }); +}; diff --git a/packages/bridge/src/commands/sendCommand.ts b/packages/bridge/src/commands/sendCommand.ts new file mode 100644 index 00000000..30397231 --- /dev/null +++ b/packages/bridge/src/commands/sendCommand.ts @@ -0,0 +1,87 @@ +import { BRIDGE } from '../constants'; +import { Command, PostMessageFn, ResponseListener, WebviewCommand } from '../types'; +import { messageListeners, subscribe, unsubscribe } from './messageListeners'; +import { getTimeStamp, getUuid, hasAndroidPostMessage, hasWebkitPostMessage } from '../utils'; + +const getPostMessageFn = (): PostMessageFn | null => { + if (hasAndroidPostMessage()) { + return (jsonMessage) => { + window.boolti?.postMessage?.(jsonMessage); + }; + } + + if (hasWebkitPostMessage()) { + return (jsonMessage) => { + window.webkit?.messageHandlers?.boolti?.postMessage?.(jsonMessage); + }; + } + + return null; +}; + +export const sendCommand = ( + request: { + command: WebviewCommand; + data?: RequestData; + }, + timeout: number = 1_000, +): Promise> => { + const postMessage = getPostMessageFn(); + const id = getUuid(); + const timestamp = getTimeStamp(); + const command = { id, timestamp, ...request }; + const message = JSON.stringify(command, undefined, 2); + + console.log('[sendCommand.ts] SEND:', message); + + if (!postMessage) { + console.warn('[sendCommand.ts] NOT WEBVIEW:', command); + return Promise.reject(command); + } + + setTimeout(() => { + postMessage(message); + }, 0); + + return new Promise((resolve, reject) => { + const listener: ResponseListener = (response) => { + if (response.id === command.id) { + resolve(response); + unsubscribe(id); + } + }; + + subscribe(id, listener); + + if (timeout) { + setTimeout(() => { + console.warn('[sendCommand.ts] TIMEOUT:', command); + unsubscribe(id); + reject(command); + }, timeout); + } + }); +}; + +window[BRIDGE] = window[BRIDGE] || { + postMessage: (command: Command) => { + const message = JSON.stringify(command); + + console.log('[sendCommand.ts] RCVD:', message); + + try { + const messageListener = messageListeners.get(command.id); + + if (messageListener) { + messageListener(command); + } + } catch (error) { + console.warn(`[sendCommand.ts] NOT RCVD: ${message}`); + if (error instanceof Error) { + console.warn( + `[sendCommand.ts] NOT RCVD: ${JSON.stringify({ name: error.name, message: error.message })}`, + ); + } + } + }, +}; diff --git a/packages/bridge/src/constants.ts b/packages/bridge/src/constants.ts new file mode 100644 index 00000000..668210c9 --- /dev/null +++ b/packages/bridge/src/constants.ts @@ -0,0 +1,3 @@ +export const BRIDGE = '__boolti__webview__bridge__'; +export const WEBVIEW_REGEX = /BOOLTI\/(ANDROID|IOS)/; +export const OS_REGEX = /(?<=BOOLTI\/).*/; diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts new file mode 100644 index 00000000..011fc090 --- /dev/null +++ b/packages/bridge/src/index.ts @@ -0,0 +1,3 @@ +export * from './commands'; +export * from './types'; +export * from './utils'; diff --git a/packages/bridge/src/types.ts b/packages/bridge/src/types.ts new file mode 100644 index 00000000..8debf81f --- /dev/null +++ b/packages/bridge/src/types.ts @@ -0,0 +1,34 @@ +export type WebviewCommand = 'NAVIGATE_TO_SHOW_DETAIL' | 'NAVIGATE_BACK' | 'REQUEST_TOKEN'; + +export type BaseCommand = { + id: string; + timestamp: string; +}; + +export type Command = Data extends undefined + ? BaseCommand & { command: WebviewCommand } + : BaseCommand & { command: WebviewCommand; data: Data }; + +export type ResponseListener = (message: Command) => void; + +export type PostMessageFn = (message: string) => void; + +declare global { + interface Window { + webkit?: { + messageHandlers?: { + boolti: { + postMessage?: PostMessageFn; + }; + }; + }; + + boolti?: { + postMessage?: PostMessageFn; + }; + + __boolti__webview__bridge__: { + postMessage: PostMessageFn; + }; + } +} diff --git a/packages/bridge/src/utils.ts b/packages/bridge/src/utils.ts new file mode 100644 index 00000000..25f479b0 --- /dev/null +++ b/packages/bridge/src/utils.ts @@ -0,0 +1,40 @@ +import { OS_REGEX, WEBVIEW_REGEX } from './constants'; +import { v4 as uuidv4 } from 'uuid'; + +export const getUserAgent = () => window.navigator.userAgent; + +export const checkIsWebView = (userAgent: string = window.navigator.userAgent) => + WEBVIEW_REGEX.test(userAgent); + +export const getWebViewOS = (userAgent: string = window.navigator.userAgent) => { + const regexResult = OS_REGEX.exec(userAgent); + return regexResult === null ? undefined : regexResult[0]; +}; + +export const checkIsAndroid = (userAgent: string = window.navigator.userAgent) => { + if (!checkIsWebView(userAgent)) return false; + return getWebViewOS(userAgent) === 'ANDROID'; +}; + +export const checkIsIOS = (userAgent: string = window.navigator.userAgent) => { + if (!checkIsWebView(userAgent)) return false; + return getWebViewOS(userAgent) === 'IOS'; +}; + +export const getTimeStamp = () => new Date().valueOf().toString(); + +export const getUuid = () => uuidv4(); + +export const hasAndroidPostMessage = () => + !!(typeof window !== 'undefined' && window.boolti && window.boolti.postMessage); + +export const hasWebkitPostMessage = () => + !!( + typeof window !== 'undefined' && + window.webkit && + window.webkit.messageHandlers && + window.webkit.messageHandlers.boolti && + window.webkit.messageHandlers.boolti.postMessage + ); + +export const isWebViewBridgeAvailable = () => hasAndroidPostMessage() || hasWebkitPostMessage(); diff --git a/packages/bridge/tsconfig.json b/packages/bridge/tsconfig.json new file mode 100644 index 00000000..aa3c4850 --- /dev/null +++ b/packages/bridge/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@boolti/typescript-config/vite.json", + "compilerOptions": { + "moduleResolution": "node", + "jsxImportSource": "@emotion/react", + }, + "include": ["src"], +} diff --git a/packages/icon/package.json b/packages/icon/package.json index 0842ebfe..bd080f1e 100644 --- a/packages/icon/package.json +++ b/packages/icon/package.json @@ -6,8 +6,9 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "lint": "TIMING=1 eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "lint:fix": "TIMING=1 eslint . --ext ts,tsx --fix" + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives", + "lint:fix": "TIMING=1 eslint . --ext ts,tsx --fix", + "type-check": "tsc --noEmit" }, "dependencies": { "@emotion/react": "^11.11.3", diff --git a/packages/icon/src/components/Discord.tsx b/packages/icon/src/components/Discord.tsx index 2c8937ee..12740da3 100644 --- a/packages/icon/src/components/Discord.tsx +++ b/packages/icon/src/components/Discord.tsx @@ -1,7 +1,22 @@ export const Discord = () => ( - + - + @@ -9,14 +24,26 @@ export const Discord = () => ( - - + + - + - -) +); diff --git a/packages/ui/package.json b/packages/ui/package.json index 07a9198e..af5f4a03 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -6,8 +6,9 @@ "main": "src/index.ts", "types": "src/index.ts", "scripts": { - "lint": "TIMING=1 eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "lint:fix": "TIMING=1 eslint . --ext ts,tsx --fix" + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives", + "lint:fix": "TIMING=1 eslint . --ext ts,tsx --fix", + "type-check": "tsc --noEmit" }, "dependencies": { "@boolti/icon": "*", diff --git a/packages/ui/src/components/ShowPreview/ShowInfoDetail.tsx b/packages/ui/src/components/ShowPreview/ShowInfoDetail.tsx index 1857a574..30b858f5 100644 --- a/packages/ui/src/components/ShowPreview/ShowInfoDetail.tsx +++ b/packages/ui/src/components/ShowPreview/ShowInfoDetail.tsx @@ -57,7 +57,10 @@ const ShowInfoDetail = ({ 일시 - {date} / {startTime}{runningTime}분 + + {date} / {startTime} + + {runningTime}분 @@ -111,10 +114,7 @@ const ShowInfoDetail = ({ 내용 {hasNoticePage && ( - + 전체보기 )} diff --git a/packages/ui/src/components/ShowPreview/ShowPreview.styles.ts b/packages/ui/src/components/ShowPreview/ShowPreview.styles.ts index 995c3499..55c89ccb 100644 --- a/packages/ui/src/components/ShowPreview/ShowPreview.styles.ts +++ b/packages/ui/src/components/ShowPreview/ShowPreview.styles.ts @@ -50,7 +50,7 @@ const ShowPreviewNavbar = styled.div` const LogoLink = styled.a` display: flex; align-items: center; - cursor: ${({ href }) => href ? 'pointer' : 'default'}; + cursor: ${({ href }) => (href ? 'pointer' : 'default')}; svg { width: 53px; @@ -243,7 +243,7 @@ const ShowInfoDescriptionBadge = styled.div` border-radius: 999px; position: relative; top: -1.5px; -` +`; const ShowInfoBox = styled.div` height: 56px; diff --git a/packages/ui/src/components/ShowPreview/index.tsx b/packages/ui/src/components/ShowPreview/index.tsx index 0e5a6549..b520ce19 100644 --- a/packages/ui/src/components/ShowPreview/index.tsx +++ b/packages/ui/src/components/ShowPreview/index.tsx @@ -53,7 +53,7 @@ const ShowPreview = ({ containerRef, onClickLink, onClickLinkMobile, - onClickShareButton + onClickShareButton, }: ShowPreviewProps) => { const { images, name } = show; @@ -119,7 +119,7 @@ const ShowPreview = ({ onClickLink={onClickLink} onClickLinkMobile={onClickLinkMobile} onClickViewNotice={() => { - containerScrollTop.current = containerRef?.current?.scrollTop ?? null + containerScrollTop.current = containerRef?.current?.scrollTop ?? null; containerRef?.current?.scrollTo(0, 0); setNoticeOpen(true); }} diff --git a/yarn.lock b/yarn.lock index 3ab0c8b9..2c5529b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1554,6 +1554,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.17.2": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/12c01357e0345f89f4f7e8c0e81921f2a3e3e101f06e8eaa18a382b517376520cd2fa8c237726eb094dab25532855df28a7baaf1c26342b52782f6936b07c287 + languageName: node + linkType: hard + "@babel/runtime@npm:^7.20.13": version: 7.24.7 resolution: "@babel/runtime@npm:7.24.7" @@ -1663,6 +1672,7 @@ __metadata: version: 0.0.0-use.local resolution: "@boolti/api@workspace:packages/api" dependencies: + "@boolti/bridge": "npm:*" "@boolti/eslint-config": "npm:*" "@boolti/typescript-config": "npm:*" "@emotion/react": "npm:^11.11.3" @@ -1679,6 +1689,19 @@ __metadata: languageName: unknown linkType: soft +"@boolti/bridge@npm:*, @boolti/bridge@workspace:packages/bridge": + version: 0.0.0-use.local + resolution: "@boolti/bridge@workspace:packages/bridge" + dependencies: + "@boolti/eslint-config": "npm:*" + "@boolti/typescript-config": "npm:*" + "@types/react": "npm:^18.2.43" + "@types/react-dom": "npm:^18.2.17" + typescript: "npm:^5.2.2" + uuid: "npm:^11.0.3" + languageName: unknown + linkType: soft + "@boolti/eslint-config@npm:*, @boolti/eslint-config@workspace:packages/config-eslint": version: 0.0.0-use.local resolution: "@boolti/eslint-config@workspace:packages/config-eslint" @@ -5441,6 +5464,7 @@ __metadata: resolution: "admin@workspace:apps/admin" dependencies: "@boolti/api": "npm:*" + "@boolti/bridge": "npm:*" "@boolti/eslint-config": "npm:*" "@boolti/icon": "npm:*" "@boolti/typescript-config": "npm:*" @@ -5462,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" @@ -5470,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" @@ -5478,6 +5502,7 @@ __metadata: react-tooltip: "npm:^5.26.3" the-new-css-reset: "npm:^1.11.2" typescript: "npm:^5.2.2" + vconsole: "npm:^3.15.1" vite: "npm:^5.0.8" languageName: unknown linkType: soft @@ -6602,6 +6627,13 @@ __metadata: languageName: node linkType: hard +"copy-text-to-clipboard@npm:^3.0.1": + version: 3.2.0 + resolution: "copy-text-to-clipboard@npm:3.2.0" + checksum: 10c0/d60fdadc59d526e19d56ad23cec2b292d33c771a5091621bd322d138804edd3c10eb2367d46ec71b39f5f7f7116a2910b332281aeb36a5b679199d746a8a5381 + languageName: node + linkType: hard + "copy-to-clipboard@npm:^3.3.3": version: 3.3.3 resolution: "copy-to-clipboard@npm:3.3.3" @@ -6620,6 +6652,13 @@ __metadata: languageName: node linkType: hard +"core-js@npm:^3.11.0": + version: 3.39.0 + resolution: "core-js@npm:3.39.0" + checksum: 10c0/f7602069b6afb2e3298eec612a5c1e0c3e6a458930fbfc7a4c5f9ac03426507f49ce395eecdd2d9bae9024f820e44582b67ffe16f2272395af26964f174eeb6b + languageName: node + linkType: hard + "core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" @@ -9367,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" @@ -10157,6 +10189,13 @@ __metadata: languageName: node linkType: hard +"mutation-observer@npm:^1.0.3": + version: 1.0.3 + resolution: "mutation-observer@npm:1.0.3" + checksum: 10c0/2f010fdec4b860a6576558013bcaa691c4912e287ea1dc99ea3b9360b52586267b291e7a2a88c0f2a9b399b4ef1e116ce8c0f839f88d2d7c9b4323fb0badc321 + languageName: node + linkType: hard + "nan@npm:^2.17.0": version: 2.20.0 resolution: "nan@npm:2.20.0" @@ -11840,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" @@ -13987,6 +14037,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^11.0.3": + version: 11.0.3 + resolution: "uuid@npm:11.0.3" + bin: + uuid: dist/esm/bin/uuid + checksum: 10c0/cee762fc76d949a2ff9205770334699e0043d52bb766472593a25f150077c9deed821230251ea3d6ab3943a5ea137d2826678797f1d5f6754c7ce5ce27e9f7a6 + languageName: node + linkType: hard + "uuid@npm:^9.0.0": version: 9.0.1 resolution: "uuid@npm:9.0.1" @@ -14013,6 +14072,18 @@ __metadata: languageName: node linkType: hard +"vconsole@npm:^3.15.1": + version: 3.15.1 + resolution: "vconsole@npm:3.15.1" + dependencies: + "@babel/runtime": "npm:^7.17.2" + copy-text-to-clipboard: "npm:^3.0.1" + core-js: "npm:^3.11.0" + mutation-observer: "npm:^1.0.3" + checksum: 10c0/1e62132b719e324eb7d533c94f38e9db288a9d0c9e85c8752ba742adee4e5925df10d4b43d05ba0cd264d99c32817f9f9c8f24fe391a7d8837469bd318d1b2ac + languageName: node + linkType: hard + "vite-compatible-readable-stream@npm:^3.6.1": version: 3.6.1 resolution: "vite-compatible-readable-stream@npm:3.6.1"