;
+};
+
+export class BaseError extends Error {
+ currentPath: PathInfo;
+ previousPath: PathInfo;
+ timestamp: string;
+ stack?: string;
+
+ constructor(message: string) {
+ super(message);
+ this.name = this.constructor.name;
+ const { currentPath, previousPath } = getCurrentPath();
+ this.currentPath = this.parsePathInfo(currentPath);
+ this.previousPath = this.parsePathInfo(previousPath);
+ this.timestamp = new Date().toISOString();
+ }
+
+ private parsePathInfo(path: string): PathInfo {
+ try {
+ const url = new URL(path, SITE_URL);
+ return {
+ pathname: url.pathname,
+ params: {},
+ searchParams: Object.fromEntries(url.searchParams.entries()),
+ };
+ } catch {
+ return { pathname: path };
+ }
+ }
+
+ toJSON() {
+ return {
+ name: this.name,
+ message: this.message,
+ timestamp: this.timestamp,
+ currentPath: this.currentPath,
+ previousPath: this.previousPath,
+ stack: this.stack,
+ };
+ }
+
+ toString(): string {
+ return JSON.stringify(this.toJSON(), null, 2);
+ }
+}
diff --git a/services/one-app/src/common/errors/RequestError.ts b/services/one-app/src/common/errors/RequestError.ts
new file mode 100644
index 00000000..0578fa6e
--- /dev/null
+++ b/services/one-app/src/common/errors/RequestError.ts
@@ -0,0 +1,45 @@
+import { BaseError } from './BaseError';
+import { type ErrorCode, RESPONSE_MESSAGES } from '@/common/constants/api';
+
+export type RequestFailedErrorType = {
+ requestBody?: any;
+ status: number;
+ endpoint: string;
+ errorCode: ErrorCode;
+ message: string;
+ method: string;
+};
+
+export class RequestFailedError extends BaseError {
+ requestBody?: any;
+ status: number;
+ endpoint: string;
+ errorCode: ErrorCode;
+ errorMessage: string;
+ method: string;
+
+ constructor({
+ requestBody,
+ status,
+ endpoint,
+ errorCode,
+ message,
+ method,
+ }: RequestFailedErrorType) {
+ super(message);
+ this.name = this.constructor.name;
+ this.status = status;
+ this.method = method;
+ this.endpoint = endpoint;
+ this.errorCode = errorCode;
+ this.requestBody = requestBody;
+ this.errorMessage =
+ RESPONSE_MESSAGES[message as keyof typeof RESPONSE_MESSAGES] || message;
+ }
+}
+
+export function isRequestFailedError(
+ error: unknown,
+): error is RequestFailedError {
+ return error instanceof RequestFailedError;
+}
diff --git a/services/one-app/src/common/errors/RequestGetError.ts b/services/one-app/src/common/errors/RequestGetError.ts
new file mode 100644
index 00000000..448a1123
--- /dev/null
+++ b/services/one-app/src/common/errors/RequestGetError.ts
@@ -0,0 +1,24 @@
+import {
+ RequestFailedError,
+ type RequestFailedErrorType,
+} from './RequestError';
+
+type ErrorHandlingStrategy = 'toast' | 'fallback';
+
+// GET 에러 발생 시 토스트로 에러를 표현할 지 errorBounadry의 fallback으로 에러를 표현할 지
+export type WithErrorHandlingStrategy = P & {
+ errorHandlingStrategy?: ErrorHandlingStrategy;
+};
+
+export class RequestGetError extends RequestFailedError {
+ errorHandlingStrategy;
+
+ constructor({
+ errorHandlingStrategy = 'toast',
+ ...rest
+ }: WithErrorHandlingStrategy) {
+ super(rest);
+
+ this.errorHandlingStrategy = errorHandlingStrategy;
+ }
+}
diff --git a/services/one-app/src/common/hooks/useCurrentPath.ts b/services/one-app/src/common/hooks/useCurrentPath.ts
new file mode 100644
index 00000000..b508380a
--- /dev/null
+++ b/services/one-app/src/common/hooks/useCurrentPath.ts
@@ -0,0 +1,10 @@
+'use client';
+
+import { usePathname, useSearchParams } from 'next/navigation';
+
+export const useCurrentPath = () => {
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+
+ return pathname + searchParams.toString();
+};
diff --git a/services/one-app/src/common/hooks/useDetectOnline.ts b/services/one-app/src/common/hooks/useDetectOnline.ts
new file mode 100644
index 00000000..6044ea3b
--- /dev/null
+++ b/services/one-app/src/common/hooks/useDetectOnline.ts
@@ -0,0 +1,43 @@
+import React, { useCallback, useState, useEffect } from 'react';
+
+export const useDetectOnline = () => {
+ const [isOffline, setIsOffline] = useState(false);
+ const handleNetworkOnline = () => setIsOffline(false);
+ const handleNetworkOffline = () => setIsOffline(true);
+
+ const addNetworkStateEventListener = () => {
+ window.addEventListener('online', handleNetworkOnline);
+ window.addEventListener('offline', handleNetworkOffline);
+ };
+
+ const removeNetworkStateEventListener = () => {
+ window.removeEventListener('online', handleNetworkOnline);
+ window.removeEventListener('offline', handleNetworkOffline);
+ };
+
+ useEffect(() => {
+ addNetworkStateEventListener();
+
+ return removeNetworkStateEventListener;
+ }, []);
+
+ const runIfOnline = useCallback(
+ ({
+ onNowOnline,
+ onNowOffline,
+ }: {
+ onNowOnline: () => void;
+ onNowOffline: () => void;
+ }) => {
+ if (window.navigator.onLine) {
+ onNowOnline();
+ } else {
+ onNowOffline();
+ window.addEventListener('online', onNowOnline, { once: true });
+ }
+ },
+ [],
+ );
+
+ return { isOffline, runIfOnline };
+};
diff --git a/services/one-app/src/common/service/AuthService.ts b/services/one-app/src/common/service/AuthService.ts
index 3797bcda..aab4206b 100644
--- a/services/one-app/src/common/service/AuthService.ts
+++ b/services/one-app/src/common/service/AuthService.ts
@@ -34,7 +34,7 @@ class _AuthService {
expireSession() {
Cookies.remove(CookieKey.ACCESS_TOKEN);
Cookies.remove(CookieKey.REFRESH_TOKEN);
- window.location.replace('/login'); // TODO, 리다이렉트 경로 설정
+ // window.location.replace('/login'); // TODO, 리다이렉트 경로 설정
}
get isLoggedIn() {
diff --git a/services/one-app/src/common/utils/error/captureError.ts b/services/one-app/src/common/utils/error/captureError.ts
new file mode 100644
index 00000000..8814f54e
--- /dev/null
+++ b/services/one-app/src/common/utils/error/captureError.ts
@@ -0,0 +1,40 @@
+import { sendLogToSentry } from './sendLogToSentry';
+
+import { BaseError } from '@/common/errors/BaseError';
+import { RequestFailedError } from '@/common/errors/RequestError';
+import { IS_DEV_ENV } from '@/common/constants/env';
+import { RESPONSE_MESSAGES } from '@/common/constants/api';
+
+export const captureError = async (error: Error) => {
+ // local 환경이면 얼리리턴
+ if (IS_DEV_ENV) return;
+
+ // instanceof 연산자는 객체의 프로토타입 체인을 따라 올라가면서 일치하는 생성자를 찾습니다.
+ // 따라서 (RequestFailedError가 BaseError를 상속하는 클래스여도)
+ // RequestFailedError의 인스턴스는 RequestFailedError의 instanceof 체크에서 먼저 true를 반환하게 됩니다.
+ if (error instanceof RequestFailedError) {
+ switch (error.errorMessage) {
+ case RESPONSE_MESSAGES[102]:
+ sendLogToSentry({ error, level: 'fatal' });
+ break;
+ case RESPONSE_MESSAGES[103]:
+ case RESPONSE_MESSAGES[200]:
+ case RESPONSE_MESSAGES[201]:
+ case RESPONSE_MESSAGES[203]:
+ case RESPONSE_MESSAGES[204]:
+ case RESPONSE_MESSAGES[205]:
+ sendLogToSentry({ error, level: 'info' });
+ break;
+ case RESPONSE_MESSAGES[101]:
+ sendLogToSentry({ error, level: 'warning' });
+ break;
+ default:
+ sendLogToSentry({ error, level: 'error' });
+ break;
+ }
+ } else if (error instanceof BaseError) {
+ sendLogToSentry({ error, level: 'error' });
+ } else {
+ sendLogToSentry({ error, level: 'fatal' });
+ }
+};
diff --git a/services/one-app/src/common/utils/error/sendLogToSentry.ts b/services/one-app/src/common/utils/error/sendLogToSentry.ts
new file mode 100644
index 00000000..74c82903
--- /dev/null
+++ b/services/one-app/src/common/utils/error/sendLogToSentry.ts
@@ -0,0 +1,71 @@
+import * as Sentry from '@sentry/react';
+
+import { BaseError } from '@/common/errors/BaseError';
+import { RequestFailedError } from '@/common/errors/RequestError';
+
+type SentryLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'log';
+type SendLogToSentry = {
+ level?: SentryLevel;
+ error: Error;
+};
+
+/**
+ * level은 아래와 같은 용도에 맞게 지정해줍니다.
+ *
+ * fatal: 앱이 종료될 수 있는 치명적인 오류
+ * error: 특정 기능 실패로 앱 종료까지는 아닌 오류
+ * warning: 잠재적으로 문제가 될 수 있는 오류. 현재는 심각하지 않은 오류
+ * info: 시스템의 정상적인 동작을 나타냄. 중요한 이벤트나 상태 변화 기록용
+ * debug: 디버깅 목적으로 사용됨
+ * log: 일반적인 로그 메세지
+ */
+export const sendLogToSentry = ({
+ level = 'error',
+ error,
+}: SendLogToSentry) => {
+ Sentry.withScope((scope) => {
+ scope.setLevel(level);
+ scope.setTag('environment', process.env.NODE_ENV);
+
+ if (error instanceof BaseError) {
+ const { name, message, currentPath, previousPath } = error;
+ scope.setTags({
+ url: window.location.href,
+ name,
+ message,
+ currentPath: JSON.stringify(currentPath),
+ previousPath: JSON.stringify(previousPath),
+ });
+
+ if (error instanceof RequestFailedError) {
+ const {
+ errorCode,
+ errorMessage,
+ endpoint,
+ status,
+ requestBody,
+ method,
+ } = error;
+ scope.setTags({
+ errorCode,
+ errorMessage,
+ endpoint,
+ status,
+ requestBody: JSON.stringify(requestBody),
+ method,
+ });
+ Sentry.captureMessage(`${errorCode}: ${errorMessage}`);
+ } else {
+ Sentry.captureMessage(`${name}: ${message}`);
+ }
+ } else {
+ const { name, message } = error;
+ scope.setTags({
+ url: window.location.href,
+ name,
+ message,
+ });
+ Sentry.captureMessage(`${name}: ${message}`);
+ }
+ });
+};
diff --git a/services/one-app/src/common/utils/route.ts b/services/one-app/src/common/utils/route.ts
new file mode 100644
index 00000000..edc5ec98
--- /dev/null
+++ b/services/one-app/src/common/utils/route.ts
@@ -0,0 +1,8 @@
+import Cookies from 'js-cookie';
+
+export const getCurrentPath = () => {
+ const currentPath = Cookies.get('currentPath') || '';
+ const previousPath = Cookies.get('previousPath') || '';
+
+ return { previousPath, currentPath };
+};
diff --git a/services/one-app/src/mocks/handlers.ts b/services/one-app/src/mocks/handlers.ts
index 527af3a2..daa4f6a7 100644
--- a/services/one-app/src/mocks/handlers.ts
+++ b/services/one-app/src/mocks/handlers.ts
@@ -1,7 +1,13 @@
-import { authHandlers, lostFoundHandlers, userHandlers } from './units';
+import {
+ authHandlers,
+ userHandlers,
+ lostFoundHandlers,
+ throwErrorHandlers,
+} from './units';
export const handlers = [
...authHandlers,
...userHandlers,
...lostFoundHandlers,
+ ...throwErrorHandlers,
];
diff --git a/services/one-app/src/mocks/units/index.ts b/services/one-app/src/mocks/units/index.ts
index e9922d74..3287ed18 100644
--- a/services/one-app/src/mocks/units/index.ts
+++ b/services/one-app/src/mocks/units/index.ts
@@ -1,3 +1,4 @@
export { default as authHandlers } from './auth';
export { default as userHandlers } from './user';
export { default as lostFoundHandlers } from './lost-found';
+export { default as throwErrorHandlers } from './throw-error';
diff --git a/services/one-app/src/mocks/units/throw-error.ts b/services/one-app/src/mocks/units/throw-error.ts
new file mode 100644
index 00000000..042b7896
--- /dev/null
+++ b/services/one-app/src/mocks/units/throw-error.ts
@@ -0,0 +1,39 @@
+import { HttpResponse, http } from 'msw';
+import {
+ ErrorInfo,
+ ErrorCode,
+ APIResponseCode,
+ RESPONSE_MESSAGES,
+} from '@/common/constants/api';
+import { createSuccessResponse } from '../mock-utils';
+
+const createErrorResponse = (errorInfo: ErrorInfo): HttpResponse => {
+ return HttpResponse.json(
+ { ...errorInfo, result: null },
+ { status: 400 }, // 모든 에러를 400으로 처리하되, 실제 에러 코드는 응답 본문에 포함
+ );
+};
+
+const throwRandomError = () => {
+ const errorCodes = Object.values(APIResponseCode) as ErrorCode[];
+ const randomErrorCode =
+ errorCodes[Math.floor(Math.random() * errorCodes.length)];
+
+ if (randomErrorCode === APIResponseCode.SUCCESS)
+ return createSuccessResponse({});
+
+ return createErrorResponse({
+ code: randomErrorCode,
+ message: RESPONSE_MESSAGES[randomErrorCode],
+ });
+};
+
+const exampleGetErrorHandler = http.get('/api/example-error', () => {
+ return throwRandomError();
+});
+
+const examplePostErrorHandler = http.post('/api/example-error', () => {
+ return throwRandomError();
+});
+
+export default [exampleGetErrorHandler, examplePostErrorHandler];
diff --git a/services/one-app/src/model/Error.ts b/services/one-app/src/model/Error.ts
deleted file mode 100644
index 852007b5..00000000
--- a/services/one-app/src/model/Error.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { HttpStatusCode } from 'axios';
-
-export type ErrorCode =
- | '100'
- | '101'
- | '102'
- | '103'
- | '200'
- | '201'
- | '202'
- | '203'
- | '204'
- | '205';
-
-export type ErrorResponseMessages =
- | 'SUCCESS'
- | 'BAD_REQUEST'
- | 'INTERNAL_SERVER_ERROR'
- | '유효하지 않은 도메인입니다.'
- | '유효하지 않은 ID 토큰입니다.'
- | '유효기간이 만료된 엑세스 토큰입니다.'
- | '유효하지 않은 리프레쉬 토큰입니다.'
- | '유효기간이 만료된 리프레쉬 토큰입니다.'
- | '유효하지 않은 권한 코드입니다.'
- | '유효하지 않은 액세스 토큰입니다.';
-
-export type ErrorCodeInfo = Partial>;
-
-export type APIErrorResponse = {
- code: ErrorCode;
- errors: string;
- message: string;
- status: HttpStatusCode;
- statusText: string;
-};
diff --git a/services/one-app/src/store/appErrorStore.ts b/services/one-app/src/store/appErrorStore.ts
new file mode 100644
index 00000000..8f38247c
--- /dev/null
+++ b/services/one-app/src/store/appErrorStore.ts
@@ -0,0 +1,14 @@
+import { create } from 'zustand';
+
+type State = {
+ appError: Error | null;
+};
+
+type Action = {
+ updateAppError: (appError: State['appError']) => void;
+};
+
+export const useAppErrorStore = create((set) => ({
+ appError: null,
+ updateAppError: (appError) => set(() => ({ appError })),
+}));
diff --git a/services/one-app/src/store/auth.ts b/services/one-app/src/store/authStore.ts
similarity index 52%
rename from services/one-app/src/store/auth.ts
rename to services/one-app/src/store/authStore.ts
index 4083bfc3..0c9268ee 100644
--- a/services/one-app/src/store/auth.ts
+++ b/services/one-app/src/store/authStore.ts
@@ -2,19 +2,24 @@ import { z } from 'zod';
import { create } from 'zustand';
import { LoginResponseSchema } from '@/app/(site)/login/_lib/requestLogin';
-export type TemporaryUserAuthData = Pick<
+type TemporaryUserAuthData = Pick<
z.infer['result'],
'accessToken' | 'refreshToken'
>;
-interface TemporaryAuthState {
+type State = {
auth: TemporaryUserAuthData | null;
- setTempAuth: (authData: TemporaryUserAuthData) => void;
+};
+
+type Action = {
+ setTempAuth: (authData: State['auth']) => void;
reset: () => void;
-}
+};
-export const useTemporaryAuthStore = create((set) => ({
+const useTemporaryAuthStore = create((set) => ({
auth: null,
- setTempAuth: (authData: TemporaryUserAuthData) => set({ auth: authData }),
+ setTempAuth: (authData) => set({ auth: authData }),
reset: () => set({ auth: null }),
}));
+
+export { type TemporaryUserAuthData, useTemporaryAuthStore };
diff --git a/services/one-app/src/store/count.ts b/services/one-app/src/store/count.ts
deleted file mode 100644
index adf9b9c5..00000000
--- a/services/one-app/src/store/count.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { create } from 'zustand';
-
-interface CountState {
- count: number;
- inc(): void;
-}
-
-export const useStore = create((set) => ({
- count: 1,
- inc: () => set((state) => ({ count: state.count + 1 })),
-}));
diff --git a/yarn.lock b/yarn.lock
index 72cddd3b..a78f8976 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -56,6 +56,7 @@ __metadata:
dependencies:
"@faker-js/faker": "npm:^9.0.3"
"@mswjs/http-middleware": "npm:^0.10.2"
+ "@sentry/react": "npm:^8.38.0"
"@tanstack/react-query": "npm:^5.59.16"
"@tanstack/react-query-devtools": "npm:^5.59.16"
"@testing-library/dom": "npm:^10.4.0"
@@ -84,6 +85,7 @@ __metadata:
postcss: "npm:^8"
react: "npm:^18"
react-dom: "npm:^18"
+ react-error-boundary: "npm:^4.0.13"
tailwind-merge: "npm:^2.5.4"
tailwindcss: "npm:^3.3.0"
ts-jest: "npm:^29.1.1"
@@ -6664,6 +6666,108 @@ __metadata:
languageName: node
linkType: hard
+"@sentry-internal/browser-utils@npm:8.38.0":
+ version: 8.38.0
+ resolution: "@sentry-internal/browser-utils@npm:8.38.0"
+ dependencies:
+ "@sentry/core": "npm:8.38.0"
+ "@sentry/types": "npm:8.38.0"
+ "@sentry/utils": "npm:8.38.0"
+ checksum: 10c0/f900a82d412007a323b654a3a78dd6f21713fd31dd032a277bb25f340fc4d552af6d1e3bdf711382df255fea2910cf1f9f160431a9f98dad1b00182435d9eee1
+ languageName: node
+ linkType: hard
+
+"@sentry-internal/feedback@npm:8.38.0":
+ version: 8.38.0
+ resolution: "@sentry-internal/feedback@npm:8.38.0"
+ dependencies:
+ "@sentry/core": "npm:8.38.0"
+ "@sentry/types": "npm:8.38.0"
+ "@sentry/utils": "npm:8.38.0"
+ checksum: 10c0/19acfc0e7afcf6c3fb2405d4311c0d51345d52bb36e46399c1aa2509e653ab81a316af75da17bf0792db38e47aa892a1eca072fca32dee064e9eb1825633f500
+ languageName: node
+ linkType: hard
+
+"@sentry-internal/replay-canvas@npm:8.38.0":
+ version: 8.38.0
+ resolution: "@sentry-internal/replay-canvas@npm:8.38.0"
+ dependencies:
+ "@sentry-internal/replay": "npm:8.38.0"
+ "@sentry/core": "npm:8.38.0"
+ "@sentry/types": "npm:8.38.0"
+ "@sentry/utils": "npm:8.38.0"
+ checksum: 10c0/0b2ecf7a1aed81ce4730cefb4622aaa103db7440264ec35a18a848a59285b5f30736588dde506c3c9f1523225cbadbef972fd3dd7290f0cee76a6b06e53cd600
+ languageName: node
+ linkType: hard
+
+"@sentry-internal/replay@npm:8.38.0":
+ version: 8.38.0
+ resolution: "@sentry-internal/replay@npm:8.38.0"
+ dependencies:
+ "@sentry-internal/browser-utils": "npm:8.38.0"
+ "@sentry/core": "npm:8.38.0"
+ "@sentry/types": "npm:8.38.0"
+ "@sentry/utils": "npm:8.38.0"
+ checksum: 10c0/742217269611c63275a67b8c7a8615fba57849828b11c02b57fd9998a64c0bcc39d9ec50a1d3726e0c8588dcf6fa4f1a5992a4845cb630a1f0f3eb724feca0e3
+ languageName: node
+ linkType: hard
+
+"@sentry/browser@npm:8.38.0":
+ version: 8.38.0
+ resolution: "@sentry/browser@npm:8.38.0"
+ dependencies:
+ "@sentry-internal/browser-utils": "npm:8.38.0"
+ "@sentry-internal/feedback": "npm:8.38.0"
+ "@sentry-internal/replay": "npm:8.38.0"
+ "@sentry-internal/replay-canvas": "npm:8.38.0"
+ "@sentry/core": "npm:8.38.0"
+ "@sentry/types": "npm:8.38.0"
+ "@sentry/utils": "npm:8.38.0"
+ checksum: 10c0/a01f747e53fa5840d6b73409948ec1137fe10372e7ee4903921700604626736e9ced9ff963178531082b56c6307e522080d39b9a09dce4973d7091c8407a7b38
+ languageName: node
+ linkType: hard
+
+"@sentry/core@npm:8.38.0":
+ version: 8.38.0
+ resolution: "@sentry/core@npm:8.38.0"
+ dependencies:
+ "@sentry/types": "npm:8.38.0"
+ "@sentry/utils": "npm:8.38.0"
+ checksum: 10c0/247b346f34ae4d9682daa1438a29193e66c9a0ec892b3ec5edc8fcabb96409edeaa7f9637e32eede457d45af7ed4b83a155b6daa5eab4b54790d5ad79e2268c8
+ languageName: node
+ linkType: hard
+
+"@sentry/react@npm:^8.38.0":
+ version: 8.38.0
+ resolution: "@sentry/react@npm:8.38.0"
+ dependencies:
+ "@sentry/browser": "npm:8.38.0"
+ "@sentry/core": "npm:8.38.0"
+ "@sentry/types": "npm:8.38.0"
+ "@sentry/utils": "npm:8.38.0"
+ hoist-non-react-statics: "npm:^3.3.2"
+ peerDependencies:
+ react: ^16.14.0 || 17.x || 18.x || 19.x
+ checksum: 10c0/ec8ddd693eda3f5b2fcc028e59f4621d2db41b5c6c738cc997ad3819f2cc90a56f379187fb40d4c92288def9774dae76eba7da3af704e1c9375f71dc2aff99aa
+ languageName: node
+ linkType: hard
+
+"@sentry/types@npm:8.38.0":
+ version: 8.38.0
+ resolution: "@sentry/types@npm:8.38.0"
+ checksum: 10c0/615ee4d8732cefc74eb19330662eae297321a992f080efff1f15bf2957c72d211240b17ea8712bbbe88da5b29c1604efa4166ef51e745ebef58609290ef72bfe
+ languageName: node
+ linkType: hard
+
+"@sentry/utils@npm:8.38.0":
+ version: 8.38.0
+ resolution: "@sentry/utils@npm:8.38.0"
+ dependencies:
+ "@sentry/types": "npm:8.38.0"
+ checksum: 10c0/01baeb95a355916502f1d8390f5398bb1a59dbda417e21a9170c4a4b1e2eb05029d2044fd82c6666338a1d497212570a36db92c1e3604a3fcd71bcf9d62451ca
+ languageName: node
+ linkType: hard
+
"@sinclair/typebox@npm:^0.24.1":
version: 0.24.51
resolution: "@sinclair/typebox@npm:0.24.51"
@@ -23541,6 +23645,17 @@ __metadata:
languageName: node
linkType: hard
+"react-error-boundary@npm:^4.0.13":
+ 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-error-overlay@npm:^6.0.11":
version: 6.0.11
resolution: "react-error-overlay@npm:6.0.11"