Skip to content

Commit 6054594

Browse files
authored
[UNI-145] feat : 에러 바운더리 생성 및 POST 에러 핸들링 로직 (#88)
* [UNI-145] feat : 커스텀 ErrorBoundary 구현 및 적용 * [UNI-145] feat : 커스텀 에러 타입 선언 및 Fetch 함수에서 커스텀 에러 throw 적용 * [UNI-145] feat : useMutationError 커스텀 훅 구현 및 에러 테스트 페이지 생성
1 parent 340cac5 commit 6054594

File tree

6 files changed

+225
-18
lines changed

6 files changed

+225
-18
lines changed

uniro_frontend/src/App.tsx

+21-16
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import useNetworkStatus from "./hooks/useNetworkStatus";
1616
import ErrorPage from "./pages/error";
1717
import { Suspense } from "react";
1818
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
19+
import ErrorBoundary from "./components/error/ErrorBoundary";
20+
import Errortest from "./pages/errorTest";
1921

2022
const queryClient = new QueryClient();
2123

@@ -24,22 +26,25 @@ function App() {
2426
useNetworkStatus();
2527
return (
2628
<QueryClientProvider client={queryClient}>
27-
<Suspense key={location.key} fallback={fallback}>
28-
<Routes>
29-
<Route path="/demo" element={<Demo />} />
30-
<Route path="/" element={<LandingPage />} />
31-
<Route path="/university" element={<UniversitySearchPage />} />
32-
<Route path="/building" element={<BuildingSearchPage />} />
33-
<Route path="/map" element={<MapPage />} />
34-
<Route path="/form" element={<ReportForm />} />
35-
<Route path="/result" element={<NavigationResultPage />} />
36-
<Route path="/report/route" element={<ReportRoutePage />} />
37-
<Route path="/report/risk" element={<ReportRiskPage />} />
38-
/** 에러 페이지 */
39-
<Route path="/error" element={<ErrorPage />} />
40-
<Route path="/error/offline" element={<OfflinePage />} />
41-
</Routes>
42-
</Suspense>
29+
<ErrorBoundary fallback={<ErrorPage />}>
30+
<Suspense key={location.key} fallback={fallback}>
31+
<Routes>
32+
<Route path="/demo" element={<Demo />} />
33+
<Route path="/" element={<LandingPage />} />
34+
<Route path="/university" element={<UniversitySearchPage />} />
35+
<Route path="/building" element={<BuildingSearchPage />} />
36+
<Route path="/map" element={<MapPage />} />
37+
<Route path="/form" element={<ReportForm />} />
38+
<Route path="/result" element={<NavigationResultPage />} />
39+
<Route path="/report/route" element={<ReportRoutePage />} />
40+
<Route path="/report/risk" element={<ReportRiskPage />} />
41+
/** 에러 페이지 */
42+
<Route path="/error" element={<ErrorPage />} />
43+
<Route path="/error/offline" element={<OfflinePage />} />
44+
<Route path="/error/test" element={<Errortest />} />
45+
</Routes>
46+
</Suspense>
47+
</ErrorBoundary>
4348
<ReactQueryDevtools initialIsOpen={false} />
4449
</QueryClientProvider>
4550
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React, { Component, ReactNode } from 'react';
2+
3+
interface ErrorBoundaryProps {
4+
fallback: ReactNode;
5+
}
6+
7+
interface ErrorBoundaryState {
8+
hasError: boolean;
9+
}
10+
11+
export default class ErrorBoundary extends Component<
12+
React.PropsWithChildren<ErrorBoundaryProps>,
13+
ErrorBoundaryState
14+
> {
15+
constructor(props: React.PropsWithChildren<ErrorBoundaryProps>) {
16+
super(props);
17+
this.state = { hasError: false };
18+
}
19+
20+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
21+
return { hasError: true };
22+
}
23+
24+
render() {
25+
if (this.state.hasError) {
26+
return this.props.fallback;
27+
}
28+
29+
return this.props.children;
30+
}
31+
}

uniro_frontend/src/constant/error.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export class NotFoundError extends Error {
2+
constructor(message: string) {
3+
super(message);
4+
this.name = "Not Found";
5+
}
6+
}
7+
8+
export class BadRequestError extends Error {
9+
constructor(message: string) {
10+
super(message);
11+
this.name = "Bad Request";
12+
}
13+
}
14+
15+
export enum ERROR_STATUS {
16+
NOT_FOUND = 404,
17+
BAD_REQUEST = 400,
18+
INTERNAL_ERROR = 500,
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { QueryClient, useMutation, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
2+
import { NotFoundError, BadRequestError, ERROR_STATUS } from "../constant/error";
3+
import React, { useCallback, useEffect, useState } from "react";
4+
5+
type Fallback = {
6+
[K in Exclude<ERROR_STATUS, ERROR_STATUS.INTERNAL_ERROR>]: {
7+
mainTitle: string;
8+
subTitle: string;
9+
};
10+
};
11+
12+
export default function useMutationError<TData, TError, TVariables, TContext>(
13+
options: UseMutationOptions<TData, TError, TVariables, TContext>,
14+
queryClient?: QueryClient,
15+
fallback?: Fallback,
16+
): [React.FC, UseMutationResult<TData, TError, TVariables, TContext>] {
17+
const [isOpen, setOpen] = useState<boolean>(false);
18+
const [title, setTitle] = useState({
19+
mainTitle: "",
20+
subTitle: "",
21+
});
22+
const result = useMutation<TData, TError, TVariables, TContext>(options, queryClient);
23+
24+
const { isError, error } = result;
25+
26+
useEffect(() => {
27+
if (isError) {
28+
setOpen(isError);
29+
if (error instanceof NotFoundError && fallback) {
30+
setTitle({ ...fallback[404] });
31+
} else if (error instanceof BadRequestError && fallback) {
32+
setTitle({ ...fallback[400] });
33+
} else {
34+
throw error;
35+
}
36+
}
37+
}, [error, isError]);
38+
39+
const close = useCallback(() => {
40+
setOpen(false);
41+
}, []);
42+
43+
const BaseFallback = (
44+
<div className="fixed inset-0 flex items-center justify-center bg-[rgba(0,0,0,0.2)]">
45+
<div className="w-full max-w-[365px] flex flex-col bg-gray-100 rounded-400 overflow-hidden">
46+
<div className="flex flex-col justify-center space-y-1 py-[25px]">
47+
<p className="text-kor-body1 font-bold text-system-red">{title.mainTitle}</p>
48+
<div className="space-y-0">
49+
<p className="text-kor-body3 font-regular text-gray-700">{title.subTitle}</p>
50+
</div>
51+
</div>
52+
<button
53+
onClick={close}
54+
className="h-[58px] border-t-[1px] border-gray-200 text-kor-body2 font-semibold active:bg-gray-200"
55+
>
56+
확인
57+
</button>
58+
</div>
59+
</div>
60+
);
61+
62+
const Modal: React.FC = () => <>{isOpen && BaseFallback}</>;
63+
64+
return [Modal, result];
65+
}
+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import useMutationError from "../hooks/useMutationError";
2+
import { RouteId } from "../data/types/route";
3+
import { CautionIssueType, DangerIssueType } from "../data/types/enum";
4+
import { postFetch } from "../utils/fetch/fetch";
5+
import { postReportRoute } from "../api/route";
6+
7+
const postReport = (
8+
univId: number,
9+
routeId: RouteId,
10+
body: { dangerTypes: DangerIssueType[]; cautionTypes: CautionIssueType[] },
11+
): Promise<boolean> => {
12+
return postFetch<void, string>(`/${univId}/route/risk/${routeId}`, body);
13+
};
14+
15+
export default function Errortest() {
16+
const [Modal400, result400] = useMutationError(
17+
{
18+
//@ts-expect-error 강제 에러 발생
19+
mutationFn: () => postReport(1001, 1, { cautionTypes: ["TEST"], dangerTypes: [] }),
20+
},
21+
undefined,
22+
{
23+
400: { mainTitle: "400 제목", subTitle: "400 부제목" },
24+
404: { mainTitle: "404 제목", subTitle: "404 부제목" },
25+
},
26+
);
27+
28+
const [Modal404, result404] = useMutationError(
29+
{
30+
mutationFn: () => postReport(1, 1, { cautionTypes: [], dangerTypes: [] }),
31+
},
32+
undefined,
33+
{
34+
400: { mainTitle: "400 제목", subTitle: "400 부제목" },
35+
404: { mainTitle: "404 제목", subTitle: "404 부제목" },
36+
},
37+
);
38+
39+
const [Modal500, result500] = useMutationError(
40+
{
41+
//@ts-expect-error 강제 에러 발생
42+
mutationFn: () => postReportRoute(1001, {}),
43+
},
44+
undefined,
45+
{
46+
400: { mainTitle: "400 제목", subTitle: "400 부제목" },
47+
404: { mainTitle: "404 제목", subTitle: "404 부제목" },
48+
},
49+
);
50+
51+
const { mutate: mutate400 } = result400;
52+
const { mutate: mutate404 } = result404;
53+
const { mutate: mutate500 } = result500;
54+
55+
return (
56+
<div>
57+
<div className="rounded-sm border border-dashed border-[#9747FF] flex flex-col justify-start space-y-5 p-5">
58+
<button className="border border-system-red" onClick={mutate400}>
59+
400 발생
60+
</button>
61+
<button className="border border-system-red" onClick={mutate404}>
62+
404 발생
63+
</button>
64+
<button className="border border-system-red" onClick={mutate500}>
65+
500 발생
66+
</button>
67+
</div>
68+
<Modal400 />
69+
<Modal404 />
70+
<Modal500 />
71+
</div>
72+
);
73+
}

uniro_frontend/src/utils/fetch/fetch.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { BadRequestError, NotFoundError } from "../../constant/error";
2+
13
export default function Fetch() {
24
const baseURL = import.meta.env.VITE_REACT_SERVER_BASE_URL;
35

@@ -11,7 +13,13 @@ export default function Fetch() {
1113
});
1214

1315
if (!response.ok) {
14-
throw new Error(`${response.status}-${response.statusText}`);
16+
if (response.status === 400) {
17+
throw new BadRequestError("Bad Request");
18+
} else if (response.status === 404) {
19+
throw new NotFoundError("Not Found");
20+
} else {
21+
throw new Error("UnExpected Error");
22+
}
1523
}
1624

1725
return response.json();
@@ -27,7 +35,13 @@ export default function Fetch() {
2735
});
2836

2937
if (!response.ok) {
30-
throw new Error(`${response.status}-${response.statusText}`);
38+
if (response.status === 400) {
39+
throw new BadRequestError("Bad Request");
40+
} else if (response.status === 404) {
41+
throw new NotFoundError("Not Found");
42+
} else {
43+
throw new Error("UnExpected Error");
44+
}
3145
}
3246

3347
return response.ok;

0 commit comments

Comments
 (0)