From 1ae8c30568b914bb290d26af04687c1892f4bc21 Mon Sep 17 00:00:00 2001 From: seoyeon_jang Date: Sun, 13 Aug 2023 04:23:28 +0900 Subject: [PATCH 1/2] =?UTF-8?q?:sparkles:=20404=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/NotFound/index.tsx | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/pages/NotFound/index.tsx diff --git a/src/pages/NotFound/index.tsx b/src/pages/NotFound/index.tsx new file mode 100644 index 0000000..dce08eb --- /dev/null +++ b/src/pages/NotFound/index.tsx @@ -0,0 +1,50 @@ +import { useCallback } from "react"; + +import { Box, Button } from "@mui/material"; +import { useNavigate } from "react-router"; +import styled from "styled-components"; + +const ContentWrapper = styled.main` + position: absolute; + top: 56px; + width: 100%; + height: calc(100% - 56px); + display: flex; + flex-direction: column; + gap: 3rem; + justify-content: center; + align-items: center; + font-size: 20px; + font-weight: 500; + + .MuiButton-root { + height: 40px; + width: 60%; + border-radius: 15.867px; + background: var(--orange, #ffbc10); + color: var(--black-2, #3a3a3a); + font-family: SUIT; + font-size: 14.733px; + font-style: normal; + font-weight: 700; + line-height: 150%; /* 22.1px */ + letter-spacing: -0.227px; + } +`; + +const NotFound = () => { + const navigate = useNavigate(); + + const handleClickGoHome = useCallback(() => { + navigate("/", { replace: true }); + }, []); + + return ( + + 🚧404🚧 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” νŽ˜μ΄μ§€μ—μš”πŸ˜’ + + + ); +}; + +export default NotFound; From 8bd079c537cf441dcf51a2b45a566ffe1369187a Mon Sep 17 00:00:00 2001 From: seoyeon_jang Date: Sun, 13 Aug 2023 05:37:01 +0900 Subject: [PATCH 2/2] =?UTF-8?q?:sparkles:=20error=20boundary=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/_http.ts | 6 +-- src/components/ErrorBoundary.tsx | 56 ++++++++++++++++++++++++ src/components/ErrorFallback.tsx | 73 ++++++++++++++++++++++++++++++++ src/interface/error.ts | 35 +++++++++++++++ src/pages/NotFound/index.tsx | 1 + src/routes/index.tsx | 41 +++++++++++------- 6 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 src/components/ErrorBoundary.tsx create mode 100644 src/components/ErrorFallback.tsx create mode 100644 src/interface/error.ts diff --git a/src/api/_http.ts b/src/api/_http.ts index 6114a43..450bb1b 100644 --- a/src/api/_http.ts +++ b/src/api/_http.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import Axios, { AxiosRequestConfig } from "axios"; -interface LawLowResponse { +export interface LawLowResponse { success: boolean; data?: T; statusCode?: number; - errorMessage?: string; - errorDetail?: string; + message?: string[]; + detail?: string[]; } class Http { diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..6b3e4b2 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable react/destructuring-assignment */ +import { Component, ReactNode } from "react"; + +import { AxiosError } from "axios"; + +import { + CustomError, + ErrorBoundaryProps, + ErrorBoundaryState, +} from "@/interface/error"; + +interface Props extends ErrorBoundaryProps { + children: ReactNode; +} +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { + error: null, + }; + this.resetErrorBoundary = this.resetErrorBoundary.bind(this); + } + + static getDerivedStateFromError(error: Error | AxiosError) { + return { error, shouldRethrow: false, shouldShowFallbackUI: true }; + } + + resetErrorBoundary() { + this.setState({ + error: null, + }); + } + + render() { + const { fallbackComponent: FallbackComponent, children } = this.props; + const { error } = this.state; + + if (error) { + return ( + + ); + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/src/components/ErrorFallback.tsx b/src/components/ErrorFallback.tsx new file mode 100644 index 0000000..024d4fd --- /dev/null +++ b/src/components/ErrorFallback.tsx @@ -0,0 +1,73 @@ +import { useCallback, useState } from "react"; + +import { Button, Typography } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import styled from "styled-components"; + +import { ErrorFallbackProps } from "@/interface/error"; + +const ContentWrapper = styled.main` + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 3rem; + + .msg { + font-family: SUIT; + font-size: 20px; + font-weight: 500; + } + + .MuiButton-root { + height: 40px; + width: 60vw; + border-radius: 15.867px; + background: var(--orange, #ffbc10); + color: var(--black-2, #3a3a3a); + font-family: SUIT; + font-size: 14.733px; + font-style: normal; + font-weight: 700; + line-height: 150%; /* 22.1px */ + letter-spacing: -0.227px; + } +`; + +const ERROR_MAP: { [key: string]: string } = { + "q should not be empty": "κ²€μƒ‰μ–΄λŠ” λΉ„μ–΄ 있으면 μ•ˆλΌμš”.😒", +}; + +const ErrorFallback = ({ error, resetErrorBoundary }: ErrorFallbackProps) => { + const [msg] = useState(() => { + if (error.name === "AxiosError") { + // eslint-disable-next-line no-unsafe-optional-chaining + const m = error.response?.data.message ?? ([] as string[]); + const mArr = m + .map((x) => + x in ERROR_MAP ? ERROR_MAP[x as keyof typeof ERROR_MAP] : [], + ) + .flat(); + if (mArr.length > 0) return mArr; + } + return ["원인 λͺ¨λ₯Ό μ—λŸ¬κ°€ λ°œμƒν–ˆμ–΄μš”.😒"]; + }); + + const navigate = useNavigate(); + + const handleClickGoHome = useCallback(() => { + navigate("/"); + resetErrorBoundary(); + }, []); + + return ( + + {msg} + + + ); +}; + +export default ErrorFallback; diff --git a/src/interface/error.ts b/src/interface/error.ts new file mode 100644 index 0000000..a9d234b --- /dev/null +++ b/src/interface/error.ts @@ -0,0 +1,35 @@ +import { ComponentType } from "react"; + +import { AxiosError } from "axios"; + +import { LawLowResponse } from "@/api/_http"; + +export interface CommonErrorProps { + statusCode: Pick["statusCode"]; + message: Pick["message"]; + onReset?: () => void; +} + +export interface CustomError extends Error { + name: "Error"; +} + +export type ErrorBoundaryState = + | { + error: null; + } + | { + error: CustomError; + } + | { + error: AxiosError; + }; + +export interface ErrorFallbackProps { + error: CustomError | AxiosError; + resetErrorBoundary: () => void; +} + +export interface ErrorBoundaryProps { + fallbackComponent: ComponentType; +} diff --git a/src/pages/NotFound/index.tsx b/src/pages/NotFound/index.tsx index dce08eb..222a22c 100644 --- a/src/pages/NotFound/index.tsx +++ b/src/pages/NotFound/index.tsx @@ -14,6 +14,7 @@ const ContentWrapper = styled.main` gap: 3rem; justify-content: center; align-items: center; + font-family: SUIT; font-size: 20px; font-weight: 500; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 98025f3..9985fb6 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,26 +1,35 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { Suspense } from "react"; + +import { + // useQueryErrorResetBoundary, + QueryErrorResetBoundary, +} from "@tanstack/react-query"; import { createBrowserRouter } from "react-router-dom"; -import { styled } from "styled-components"; -import AppRoutes from "./AppRoutes"; +import ErrorBoundary from "@/components/ErrorBoundary"; +import ErrorFallback from "@/components/ErrorFallback"; -const TodoContainer = styled.section` - display: flex; - height: 100%; - width: 100%; - justify-content: center; - align-items: center; - font-size: 50px; - font-weight: 500; -`; +import AppRoutes from "./AppRoutes"; -const TodoPage = () => { - return βš™οΈμž‘μ—…μ€‘βš™οΈ; +const ErrorLayer = () => { + return ( + + + {() => ( + + + + )} + + + ); }; const router = createBrowserRouter([ { path: "/", - element: , + element: , children: [ { path: "", @@ -68,8 +77,8 @@ const router = createBrowserRouter([ { path: "*", async lazy() { - const NotFound = await TodoPage; - return { Component: NotFound }; + const NotFound = await import("@pages/NotFound"); + return { Component: NotFound.default }; }, }, ],