diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 863d005c..cd8ba15f 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -20,6 +20,7 @@
"lightweight-charts": "^4.2.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-error-boundary": "^4.1.2",
"react-lottie-player": "^2.1.0",
"react-router-dom": "^6.28.0",
"socket.io-client": "^4.8.1",
diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index 161ab1ba..704369ad 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -1,15 +1,29 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
+import { Suspense } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
import { RouterProvider } from 'react-router-dom';
+import { Error } from './components/errors/error';
+import { Loader } from './components/ui/loader';
import { router } from './routes';
-const App = () => {
- const queryClient = new QueryClient();
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ throwOnError: true,
+ },
+ },
+});
+const App = () => {
return (
-
-
+ }>
+ }>
+
+
+
+
);
};
diff --git a/packages/frontend/src/apis/config/index.ts b/packages/frontend/src/apis/config/index.ts
index a828a8b9..dd73147e 100644
--- a/packages/frontend/src/apis/config/index.ts
+++ b/packages/frontend/src/apis/config/index.ts
@@ -1,4 +1,5 @@
import axios, { AxiosError } from 'axios';
+import { ErrorResponse } from '@/apis/queries/errorSchema';
export const instance = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
@@ -10,9 +11,10 @@ instance.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const status = error.response?.status;
+ const { message } = error.response?.data as ErrorResponse;
if (status === 400) {
- alert('잘못된 요청이에요.');
+ alert(message);
}
if (status === 403) {
diff --git a/packages/frontend/src/apis/queries/alarm/schema.ts b/packages/frontend/src/apis/queries/alarm/schema.ts
index 1b99af75..57de2c16 100644
--- a/packages/frontend/src/apis/queries/alarm/schema.ts
+++ b/packages/frontend/src/apis/queries/alarm/schema.ts
@@ -22,7 +22,7 @@ export const PostCreateAlarmRequestSchema = z.object({
stockId: z.string(),
targetPrice: z.number().optional(),
targetVolume: z.number().optional(),
- alarmDate: z.string().datetime(),
+ alarmExpiredDate: z.string().datetime().nullish(),
});
export type PostCreateAlarmRequest = z.infer<
@@ -33,8 +33,8 @@ export const AlarmInfoSchema = z.object({
alarmId: z.number(),
stockId: z.string(),
targetPrice: z.number().nullable(),
- targetVolume: z.string().nullable(),
- alarmDate: z.string().datetime(),
+ targetVolume: z.number().nullable(),
+ alarmExpiredDate: z.string().datetime().nullable(),
});
export const AlarmResponseSchema = z.array(AlarmInfoSchema);
diff --git a/packages/frontend/src/apis/queries/alarm/useGetStockAlarm.ts b/packages/frontend/src/apis/queries/alarm/useGetStockAlarm.ts
index 8cf3d059..3bfd91fa 100644
--- a/packages/frontend/src/apis/queries/alarm/useGetStockAlarm.ts
+++ b/packages/frontend/src/apis/queries/alarm/useGetStockAlarm.ts
@@ -20,5 +20,7 @@ export const useGetStockAlarm = ({
queryKey: ['getStockAlarm', stockId],
queryFn: () => getStockAlarm({ stockId }),
enabled: isLoggedIn,
+ staleTime: 1000 * 60 * 5,
+ select: (data) => data.reverse(),
});
};
diff --git a/packages/frontend/src/apis/queries/alarm/usePostCreateAlarm.ts b/packages/frontend/src/apis/queries/alarm/usePostCreateAlarm.ts
index aab745f9..7e1fa8e3 100644
--- a/packages/frontend/src/apis/queries/alarm/usePostCreateAlarm.ts
+++ b/packages/frontend/src/apis/queries/alarm/usePostCreateAlarm.ts
@@ -10,10 +10,10 @@ const postCreateAlarm = ({
stockId,
targetPrice,
targetVolume,
- alarmDate,
+ alarmExpiredDate,
}: PostCreateAlarmRequest) =>
post({
- params: { stockId, targetPrice, targetVolume, alarmDate },
+ params: { stockId, targetPrice, targetVolume, alarmExpiredDate },
schema: AlarmInfoSchema,
url: '/api/alarm',
});
@@ -26,9 +26,9 @@ export const usePostCreateAlarm = () => {
stockId,
targetPrice,
targetVolume,
- alarmDate,
+ alarmExpiredDate,
}: PostCreateAlarmRequest) =>
- postCreateAlarm({ stockId, targetPrice, targetVolume, alarmDate }),
+ postCreateAlarm({ stockId, targetPrice, targetVolume, alarmExpiredDate }),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ['getStockAlarm'] }),
});
diff --git a/packages/frontend/src/apis/queries/auth/useGetLoginStatus.ts b/packages/frontend/src/apis/queries/auth/useGetLoginStatus.ts
index b7323f2f..7ec5b5d4 100644
--- a/packages/frontend/src/apis/queries/auth/useGetLoginStatus.ts
+++ b/packages/frontend/src/apis/queries/auth/useGetLoginStatus.ts
@@ -12,6 +12,6 @@ export const useGetLoginStatus = () => {
return useQuery({
queryKey: ['loginStatus'],
queryFn: getLoginStatus,
- staleTime: 1000 * 60 * 5,
+ staleTime: 1000 * 60 * 3,
});
};
diff --git a/packages/frontend/src/apis/queries/chat/useGetChatList.ts b/packages/frontend/src/apis/queries/chat/useGetChatList.ts
index 960f228c..5c0817b4 100644
--- a/packages/frontend/src/apis/queries/chat/useGetChatList.ts
+++ b/packages/frontend/src/apis/queries/chat/useGetChatList.ts
@@ -1,4 +1,4 @@
-import { useInfiniteQuery } from '@tanstack/react-query';
+import { useSuspenseInfiniteQuery } from '@tanstack/react-query';
import { GetChatListRequest } from './schema';
import { get } from '@/apis/utils/get';
import { ChatDataResponse, ChatDataResponseSchema } from '@/sockets/schema';
@@ -26,7 +26,7 @@ export const useGetChatList = ({
pageSize,
order,
}: GetChatListRequest) => {
- return useInfiniteQuery({
+ return useSuspenseInfiniteQuery({
queryKey: ['chatList', stockId, order],
queryFn: ({ pageParam }) =>
getChatList({
diff --git a/packages/frontend/src/apis/queries/chat/usePostChatLike.ts b/packages/frontend/src/apis/queries/chat/usePostChatLike.ts
index 7cf53563..d2ce80c6 100644
--- a/packages/frontend/src/apis/queries/chat/usePostChatLike.ts
+++ b/packages/frontend/src/apis/queries/chat/usePostChatLike.ts
@@ -1,4 +1,4 @@
-import { useMutation } from '@tanstack/react-query';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
GetChatLikeResponseSchema,
type GetChatLikeRequest,
@@ -14,8 +14,10 @@ const postChatLike = ({ chatId }: GetChatLikeRequest) =>
});
export const usePostChatLike = () => {
+ const queryClient = useQueryClient();
return useMutation({
mutationKey: ['chatLike'],
mutationFn: ({ chatId }: GetChatLikeRequest) => postChatLike({ chatId }),
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['chatList'] }),
});
};
diff --git a/packages/frontend/src/apis/queries/errorSchema.ts b/packages/frontend/src/apis/queries/errorSchema.ts
new file mode 100644
index 00000000..fcac6a03
--- /dev/null
+++ b/packages/frontend/src/apis/queries/errorSchema.ts
@@ -0,0 +1,20 @@
+import { z } from 'zod';
+
+export const ErrorResponseSchema = z.object({
+ message: z.string(),
+ error: z.string(),
+ statusCode: z.number(),
+});
+
+export const AxiosErrorSchema = z.object({
+ response: z.object({
+ data: ErrorResponseSchema,
+ status: z.number(),
+ statusText: z.string(),
+ }),
+ request: z.any().optional(),
+ message: z.string(),
+});
+
+export type ErrorResponse = z.infer;
+export type AxiosError = z.infer;
diff --git a/packages/frontend/src/apis/queries/stock-detail/useGetStockDetail.ts b/packages/frontend/src/apis/queries/stock-detail/useGetStockDetail.ts
index 66398d18..fb3ed3b9 100644
--- a/packages/frontend/src/apis/queries/stock-detail/useGetStockDetail.ts
+++ b/packages/frontend/src/apis/queries/stock-detail/useGetStockDetail.ts
@@ -1,4 +1,4 @@
-import { useQuery } from '@tanstack/react-query';
+import { useSuspenseQuery } from '@tanstack/react-query';
import {
GetStockResponseSchema,
type GetStockRequest,
@@ -13,9 +13,8 @@ const getStockDetail = ({ stockId }: GetStockRequest) =>
});
export const useGetStockDetail = ({ stockId }: GetStockRequest) => {
- return useQuery({
+ return useSuspenseQuery({
queryKey: ['stockDetail', stockId],
queryFn: () => getStockDetail({ stockId }),
- enabled: !!stockId,
});
};
diff --git a/packages/frontend/src/apis/queries/stocks/index.ts b/packages/frontend/src/apis/queries/stocks/index.ts
index b462ee5f..8af03948 100644
--- a/packages/frontend/src/apis/queries/stocks/index.ts
+++ b/packages/frontend/src/apis/queries/stocks/index.ts
@@ -1,5 +1,5 @@
export * from './schema';
-export * from './useGetTopViews';
export * from './useGetStocksByPrice';
export * from './useGetSearchStocks';
export * from './useGetStocksPriceSeries';
+export * from './useStockQueries';
diff --git a/packages/frontend/src/apis/queries/stocks/useGetSearchStocks.ts b/packages/frontend/src/apis/queries/stocks/useGetSearchStocks.ts
index d654c018..d1fa5bb4 100644
--- a/packages/frontend/src/apis/queries/stocks/useGetSearchStocks.ts
+++ b/packages/frontend/src/apis/queries/stocks/useGetSearchStocks.ts
@@ -19,6 +19,7 @@ export const useGetSearchStocks = (name: string) => {
await new Promise((resolve) => setTimeout(resolve, 500));
return getSearchStocks(name);
},
+ retry: 0,
enabled: false,
});
};
diff --git a/packages/frontend/src/apis/queries/stocks/useGetStockIndex.ts b/packages/frontend/src/apis/queries/stocks/useGetStockIndex.ts
deleted file mode 100644
index de88bd1b..00000000
--- a/packages/frontend/src/apis/queries/stocks/useGetStockIndex.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-import { z } from 'zod';
-import { StockIndexSchema, type StockIndexResponse } from './schema';
-import { get } from '@/apis/utils/get';
-
-const getStockIndex = () =>
- get({
- schema: z.array(StockIndexSchema),
- url: `/api/stock/index`,
- });
-
-export const useGetStockIndex = () => {
- return useQuery({
- queryKey: ['stockIndex'],
- queryFn: getStockIndex,
- });
-};
diff --git a/packages/frontend/src/apis/queries/stocks/useGetStocksByPrice.ts b/packages/frontend/src/apis/queries/stocks/useGetStocksByPrice.ts
index 84759632..428c54fc 100644
--- a/packages/frontend/src/apis/queries/stocks/useGetStocksByPrice.ts
+++ b/packages/frontend/src/apis/queries/stocks/useGetStocksByPrice.ts
@@ -1,4 +1,4 @@
-import { useQuery } from '@tanstack/react-query';
+import { useSuspenseQuery } from '@tanstack/react-query';
import {
GetStockListResponseSchema,
type GetStockListRequest,
@@ -14,7 +14,7 @@ const getStockByPrice = ({ limit, type }: GetStockListRequest) =>
});
export const useGetStocksByPrice = ({ limit, type }: GetStockListRequest) => {
- return useQuery({
+ return useSuspenseQuery({
queryKey: ['stocks', limit, type],
queryFn: () => getStockByPrice({ limit, type }),
});
diff --git a/packages/frontend/src/apis/queries/stocks/useGetStocksPriceSeries.ts b/packages/frontend/src/apis/queries/stocks/useGetStocksPriceSeries.ts
index 6321e15a..3d9c85b4 100644
--- a/packages/frontend/src/apis/queries/stocks/useGetStocksPriceSeries.ts
+++ b/packages/frontend/src/apis/queries/stocks/useGetStocksPriceSeries.ts
@@ -41,8 +41,12 @@ export const useGetStocksPriceSeries = ({
: undefined,
initialPageParam: { lastStartTime },
select: (data) => ({
- pages: [...data.pages].reverse(),
- pageParams: [...data.pageParams].reverse(),
+ priceDtoList: [...data.pages]
+ .reverse()
+ .flatMap((page) => page.priceDtoList),
+ volumeDtoList: [...data.pages]
+ .reverse()
+ .flatMap((page) => page.volumeDtoList),
}),
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000,
diff --git a/packages/frontend/src/apis/queries/stocks/useGetTopViews.ts b/packages/frontend/src/apis/queries/stocks/useGetTopViews.ts
deleted file mode 100644
index a61e4c96..00000000
--- a/packages/frontend/src/apis/queries/stocks/useGetTopViews.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-import { z } from 'zod';
-import {
- GetStockListResponseSchema,
- GetStockTopViewsResponse,
- type GetStockListRequest,
-} from './schema';
-import { get } from '@/apis/utils/get';
-
-const getTopViews = ({ limit }: Partial) =>
- get[]>({
- schema: z.array(GetStockListResponseSchema.partial()),
- url: `/api/stock/topViews`,
- params: { limit },
- });
-
-export const useGetTopViews = ({ limit }: Partial) => {
- return useQuery({
- queryKey: ['stocks', 'topViews'],
- queryFn: () => getTopViews({ limit }),
- });
-};
diff --git a/packages/frontend/src/apis/queries/stocks/useStockQueries.ts b/packages/frontend/src/apis/queries/stocks/useStockQueries.ts
new file mode 100644
index 00000000..c4d62ef1
--- /dev/null
+++ b/packages/frontend/src/apis/queries/stocks/useStockQueries.ts
@@ -0,0 +1,42 @@
+import { useSuspenseQueries } from '@tanstack/react-query';
+import { z } from 'zod';
+import {
+ GetStockListRequest,
+ GetStockListResponseSchema,
+ GetStockTopViewsResponse,
+ StockIndexResponse,
+ StockIndexSchema,
+} from './schema';
+import { get } from '@/apis/utils/get';
+
+interface StockQueriesProps {
+ viewsLimit: GetStockListRequest['limit'];
+}
+
+const getStockIndex = () =>
+ get({
+ schema: z.array(StockIndexSchema),
+ url: `/api/stock/index`,
+ });
+
+const getTopViews = ({ limit }: Partial) =>
+ get[]>({
+ schema: z.array(GetStockListResponseSchema.partial()),
+ url: `/api/stock/topViews`,
+ params: { limit },
+ });
+
+export const useStockQueries = ({ viewsLimit }: StockQueriesProps) => {
+ return useSuspenseQueries({
+ queries: [
+ {
+ queryKey: ['stockIndex'],
+ queryFn: getStockIndex,
+ },
+ {
+ queryKey: ['topViews'],
+ queryFn: () => getTopViews({ limit: viewsLimit }),
+ },
+ ],
+ });
+};
diff --git a/packages/frontend/src/apis/queries/user/useGetUserInfo.ts b/packages/frontend/src/apis/queries/user/useGetUserInfo.ts
index dbe88895..efc97f89 100644
--- a/packages/frontend/src/apis/queries/user/useGetUserInfo.ts
+++ b/packages/frontend/src/apis/queries/user/useGetUserInfo.ts
@@ -1,4 +1,4 @@
-import { useQuery } from '@tanstack/react-query';
+import { useSuspenseQuery } from '@tanstack/react-query';
import { GetUserInfoSchema, type GetUserInfo } from './schema';
import { get } from '@/apis/utils/get';
@@ -9,7 +9,7 @@ const getUserInfo = () =>
});
export const useGetUserInfo = () => {
- return useQuery({
+ return useSuspenseQuery({
queryKey: ['userInfo'],
queryFn: getUserInfo,
});
diff --git a/packages/frontend/src/apis/queries/user/useGetUserStock.ts b/packages/frontend/src/apis/queries/user/useGetUserStock.ts
index 3c7c1e9a..f81fce97 100644
--- a/packages/frontend/src/apis/queries/user/useGetUserStock.ts
+++ b/packages/frontend/src/apis/queries/user/useGetUserStock.ts
@@ -1,4 +1,4 @@
-import { useQuery } from '@tanstack/react-query';
+import { useSuspenseQuery } from '@tanstack/react-query';
import {
GetUserStockResponseSchema,
type GetUserStockResponse,
@@ -12,7 +12,7 @@ const getUserStock = () =>
});
export const useGetUserStock = () => {
- return useQuery({
+ return useSuspenseQuery({
queryKey: ['userStock'],
queryFn: getUserStock,
});
diff --git a/packages/frontend/src/components/errors/error.tsx b/packages/frontend/src/components/errors/error.tsx
new file mode 100644
index 00000000..18e20dae
--- /dev/null
+++ b/packages/frontend/src/components/errors/error.tsx
@@ -0,0 +1,30 @@
+import Lottie from 'react-lottie-player';
+import { useNavigate } from 'react-router-dom';
+import { Button } from '../ui/button';
+import error from '@/components/lottie/error-loading.json';
+import { cn } from '@/utils/cn';
+
+interface ErrorProps {
+ className?: string;
+}
+
+export const Error = ({ className }: ErrorProps) => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ 에러가 발생했어요. 주춤주춤 팀을 찾아주세요.
+
+
+
+
+
+
+ );
+};
diff --git a/packages/frontend/src/components/layouts/Sidebar.tsx b/packages/frontend/src/components/layouts/Sidebar.tsx
index fcabf362..2589cc47 100644
--- a/packages/frontend/src/components/layouts/Sidebar.tsx
+++ b/packages/frontend/src/components/layouts/Sidebar.tsx
@@ -2,7 +2,7 @@ import { useContext, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import logoCharacter from '/logoCharacter.png';
import logoTitle from '/logoTitle.png';
-import { Alarm } from './alarm';
+// import { Alarm } from './alarm';
import { MenuList } from './MenuList';
import { Search } from './search';
import { BOTTOM_MENU_ITEMS, TOP_MENU_ITEMS } from '@/constants/menuItems';
@@ -101,7 +101,7 @@ export const Sidebar = () => {
)}
>
{showTabs.search && }
- {showTabs.alarm && }
+ {/* {showTabs.alarm && } */}
);
diff --git a/packages/frontend/src/components/lottie/error-loading.json b/packages/frontend/src/components/lottie/error-loading.json
new file mode 100644
index 00000000..33c5df00
--- /dev/null
+++ b/packages/frontend/src/components/lottie/error-loading.json
@@ -0,0 +1 @@
+{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":80,"w":500,"h":500,"nm":"exclamação animation","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"exclamation","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":7,"s":[258.4,231.36,0],"to":null,"ti":null},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[258.4,225,0],"to":null,"ti":null},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":13,"s":[258.4,231.36,0],"to":[0,1.06,0],"ti":[0,1.06,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41,"s":[258.4,231.36,0],"to":null,"ti":null},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":44,"s":[258.4,225,0],"to":null,"ti":null},{"t":47,"s":[258.4,231.36,0]}],"ix":2},"a":{"a":0,"k":[16.613,83.692,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":7,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[93,93,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":13,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":41,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":44,"s":[93,93,100]},{"t":47,"s":[90,90,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[9.037,16.363],[0,16.363],[-16.363,9.038],[-9.037,-16.363],[16.363,-9.037]],"o":[[0,16.363],[-9.037,16.363],[-16.363,-9.037],[9.037,-16.363],[16.363,9.038]],"v":[[0,16.363],[0,16.363],[-16.363,0],[0,-16.363],[16.363,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"fl","c":{"a":0,"k":[0.9961,0.9608,0.7412,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[16.613,150.771],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[8.134,59.999],[0,59.999],[-14.727,53.405],[-14.727,-45.271],[-8.133,-59.999],[14.727,-53.405],[14.727,45.271]],"o":[[0,59.999],[-8.133,59.999],[-14.727,45.271],[-14.727,-53.405],[8.134,-59.999],[14.727,-45.271],[14.727,53.405]],"v":[[0,59.999],[0,59.999],[-14.727,45.271],[-14.727,-45.271],[0,-59.999],[14.727,-45.271],[14.727,45.271]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"fl","c":{"a":0,"k":[0.9961,0.9608,0.7412,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[16.613,60.249],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":80,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":2,"ty":4,"nm":"stroke circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[258.4,231.36,0],"ix":2},"a":{"a":0,"k":[146,146,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-77.872,-141],[141,-77.872],[77.872,141],[-141,77.872]],"o":[[77.872,-141],[141,77.872],[-77.872,141],[-141,-77.872]],"v":[[0,-141],[141,0],[0,141],[-141,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[146,146],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":30,"s":[90,90]},{"t":78,"s":[120,120]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[90]},{"t":79,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":90,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":3,"ty":4,"nm":"stroke circle 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[258.4,231.36,0],"ix":2},"a":{"a":0,"k":[146,146,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-77.872,-141],[141,-77.872],[77.872,141],[-141,77.872]],"o":[[77.872,-141],[141,77.872],[-77.872,141],[-141,-77.872]],"v":[[0,-141],[141,0],[0,141],[-141,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"st","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[146,146],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[90,90]},{"t":48,"s":[120,120]}],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[90]},{"t":49,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":90,"st":0,"bm":0,"completed":true},{"ddd":0,"ind":4,"ty":4,"nm":"circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[258.4,231.36,0],"ix":2},"a":{"a":0,"k":[130.82,130.82,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-72.112,-130.57],[130.57,-72.112],[72.111,130.57],[-130.57,72.111]],"o":[[72.111,-130.57],[130.57,72.111],[-72.112,130.57],[-130.57,-72.112]],"v":[[0,-130.57],[130.57,0],[0,130.57],[-130.57,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false,"_render":true},{"ty":"fl","c":{"a":0,"k":[1,0.5569,0.2275,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false,"_render":true},{"ty":"tr","p":{"a":0,"k":[130.82,130.82],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform","_render":true}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false,"_render":true}],"ip":0,"op":90,"st":0,"bm":0,"completed":true}],"markers":[],"__complete":true}
\ No newline at end of file
diff --git a/packages/frontend/src/components/ui/alarm/Alarm.tsx b/packages/frontend/src/components/ui/alarm/Alarm.tsx
index 12d6d403..339c009e 100644
--- a/packages/frontend/src/components/ui/alarm/Alarm.tsx
+++ b/packages/frontend/src/components/ui/alarm/Alarm.tsx
@@ -4,7 +4,7 @@ import Flag from '@/assets/flag.svg?react';
export interface AlarmProps {
option: string;
goalPrice: number | string;
- alarmDate: string;
+ alarmDate: string | null;
}
export const Alarm = ({ option, goalPrice, alarmDate }: AlarmProps) => {
@@ -16,7 +16,7 @@ export const Alarm = ({ option, goalPrice, alarmDate }: AlarmProps) => {
- {alarmDate.slice(0, 10)}
+ 기한: {alarmDate ? alarmDate.slice(0, 10) : '무기한'}
);
diff --git a/packages/frontend/src/components/ui/loader/Loader.tsx b/packages/frontend/src/components/ui/loader/Loader.tsx
index 2ed870df..848880b7 100644
--- a/packages/frontend/src/components/ui/loader/Loader.tsx
+++ b/packages/frontend/src/components/ui/loader/Loader.tsx
@@ -2,12 +2,12 @@ import Lottie from 'react-lottie-player';
import loading from '@/components/lottie/loading-animation.json';
interface LoaderProps {
- className: string;
+ className?: string;
}
export const Loader = ({ className }: LoaderProps) => {
return (
-
+
);
diff --git a/packages/frontend/src/constants/menuItems.tsx b/packages/frontend/src/constants/menuItems.tsx
index 33e07767..60793beb 100644
--- a/packages/frontend/src/constants/menuItems.tsx
+++ b/packages/frontend/src/constants/menuItems.tsx
@@ -1,4 +1,3 @@
-import Bell from '@/assets/bell.svg?react';
import Home from '@/assets/home.svg?react';
import Search from '@/assets/search.svg?react';
import Stock from '@/assets/stock.svg?react';
@@ -15,7 +14,7 @@ export const TOP_MENU_ITEMS: MenuSection[] = [
text: '주식',
path: '/stocks/005930',
},
- { id: 4, icon:
, text: '알림' },
+ // { id: 4, icon:
, text: '알림' },
];
export const BOTTOM_MENU_ITEMS: MenuSection[] = [
diff --git a/packages/frontend/src/pages/my-page/AlarmInfo.tsx b/packages/frontend/src/pages/my-page/AlarmInfo.tsx
index 2ffec478..ad6eec13 100644
--- a/packages/frontend/src/pages/my-page/AlarmInfo.tsx
+++ b/packages/frontend/src/pages/my-page/AlarmInfo.tsx
@@ -5,18 +5,18 @@ import { LoginContext } from '@/contexts/login';
export const AlarmInfo = () => {
return (
-
+
);
};
const AlarmInfoContents = () => {
const { isLoggedIn } = useContext(LoginContext);
- const { data } = useGetAlarm({
- isLoggedIn,
- });
+ const { data } = useGetAlarm({ isLoggedIn });
if (!isLoggedIn) {
return (
@@ -39,7 +39,7 @@ const AlarmInfoContents = () => {
key={alarm.alarmId}
option={alarm.targetPrice ? '목표가' : '거래가'}
goalPrice={alarm.targetPrice ?? alarm.targetVolume!}
- alarmDate={alarm.alarmDate}
+ alarmDate={alarm.alarmExpiredDate}
/>
));
};
diff --git a/packages/frontend/src/pages/my-page/StockInfo.tsx b/packages/frontend/src/pages/my-page/StockInfo.tsx
index 86ebd1de..8303fed6 100644
--- a/packages/frontend/src/pages/my-page/StockInfo.tsx
+++ b/packages/frontend/src/pages/my-page/StockInfo.tsx
@@ -8,7 +8,7 @@ import { LoginContext } from '@/contexts/login';
export const StockInfo = () => {
return (
-
+
@@ -44,7 +44,7 @@ const StockInfoContents = () => {
}
return (
-
+
{data?.userStocks.map((stock) => (
{
@@ -30,7 +30,7 @@ export const AddAlarmForm = ({ className }: AddAlarmFormProps) => {
const [alarmInfo, setAlarmInfo] = useState({
option: ALARM_OPTIONS[0].name,
value: 0,
- endDate: '',
+ endDate: null,
});
const handleSubmit = (event: FormEvent) => {
@@ -41,26 +41,24 @@ export const AddAlarmForm = ({ className }: AddAlarmFormProps) => {
return;
}
- if (!alarmInfo.endDate) {
- alert('알림을 받을 기한을 선택해주세요.');
- return;
- }
-
subscribeAlarm();
const { option, value, endDate } = alarmInfo;
const requestData: PostCreateAlarmRequest = {
stockId,
[option]: value,
- alarmDate: endDate,
+ alarmExpiredDate: endDate,
};
mutate(requestData, {
onSuccess: () => {
alert('알림이 등록되었어요!');
- setAlarmInfo({ option: ALARM_OPTIONS[0].name, value: 0, endDate: '' });
+ setAlarmInfo({
+ option: ALARM_OPTIONS[0].name,
+ value: 0,
+ endDate: null,
+ });
},
- onError: () => alert('예기치 못한 문제가 발생했어요. 다시 시도해주세요.'),
});
};
@@ -77,8 +75,13 @@ export const AddAlarmForm = ({ className }: AddAlarmFormProps) => {
className="text-dark-gray flex h-full flex-col items-center justify-between gap-16"
>
-
- 언제 알림을 보낼까요?
+
+
+
언제 알림을 보낼까요?
+
+ 알림을 받고 싶은 도달 가격을 설정하세요.
+
+
-
- 언제까지 알림을 보낼까요?
+
+
+
언제까지 알림을 보낼까요?
+
+ 알림 종료 일자를 선택하세요.
+
미 선택시 무기한으로 설정됩니다.
+
+
setAlarmInfo((prev) => ({ ...prev, endDate: e.target.value }))
}
diff --git a/packages/frontend/src/pages/stock-detail/ChatPanel.tsx b/packages/frontend/src/pages/stock-detail/ChatPanel.tsx
index 8b621514..0e0f5a2e 100644
--- a/packages/frontend/src/pages/stock-detail/ChatPanel.tsx
+++ b/packages/frontend/src/pages/stock-detail/ChatPanel.tsx
@@ -1,4 +1,12 @@
-import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import {
+ Suspense,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router-dom';
import { TextArea } from './components';
import { ChatMessage } from './components/ChatMessage';
@@ -153,49 +161,59 @@ export const ChatPanel = ({ isOwnerStock }: ChatPanelProps) => {
- {chatData.length ? (
- <>
- {chatData.slice(0, INITIAL_VISIBLE_CHATS).map((chat) => (
- handleLikeClick(chat.id)}
- />
- ))}
- {chatData.slice(INITIAL_VISIBLE_CHATS).map((chat, index) => (
-
- {!isOwnerStock && (
-
- {index === 0 && (
-
- 주식 소유자만
- 확인할 수 있습니다.
-
+
채팅을 불러오는데 실패했어요.
+ }
+ >
+ }>
+ {chatData.length ? (
+ <>
+ {chatData.slice(0, INITIAL_VISIBLE_CHATS).map((chat) => (
+ handleLikeClick(chat.id)}
+ />
+ ))}
+ {chatData.slice(INITIAL_VISIBLE_CHATS).map((chat, index) => (
+
+ {!isOwnerStock && (
+
+ {index === 0 && (
+
+ 주식 소유자만
+ 확인할 수 있어요.
+
+ )}
+
)}
+
handleLikeClick(chat.id)}
+ />
- )}
- handleLikeClick(chat.id)}
- />
-
- ))}
- >
- ) : (
-
채팅이 없어요.
- )}
+ ))}
+ >
+ ) : (
+
채팅이 없어요.
+ )}
+
+
{isFetching ?
:
}
diff --git a/packages/frontend/src/pages/stock-detail/NotificationPanel.tsx b/packages/frontend/src/pages/stock-detail/NotificationPanel.tsx
index e839cb22..4a374142 100644
--- a/packages/frontend/src/pages/stock-detail/NotificationPanel.tsx
+++ b/packages/frontend/src/pages/stock-detail/NotificationPanel.tsx
@@ -44,7 +44,7 @@ const NotificationContents = () => {
key={alarm.alarmId}
option={alarm.targetPrice ? '목표가' : '거래가'}
goalPrice={alarm.targetPrice ?? alarm.targetVolume!}
- alarmDate={alarm.alarmDate}
+ alarmDate={alarm.alarmExpiredDate}
/>
));
};
diff --git a/packages/frontend/src/pages/stock-detail/StockDetail.tsx b/packages/frontend/src/pages/stock-detail/StockDetail.tsx
index 9c183021..1d200127 100644
--- a/packages/frontend/src/pages/stock-detail/StockDetail.tsx
+++ b/packages/frontend/src/pages/stock-detail/StockDetail.tsx
@@ -1,27 +1,26 @@
+import { lazy, Suspense } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router-dom';
import { StockDetailHeader } from './components';
-import {
- AddAlarmForm,
- ChatPanel,
- NotificationPanel,
- StockMetricsPanel,
- TradingChart,
-} from '.';
+import { AddAlarmForm, ChatPanel, NotificationPanel, TradingChart } from '.';
import {
useGetOwnership,
useGetStockDetail,
} from '@/apis/queries/stock-detail';
+import { Loader } from '@/components/ui/loader';
+
+const StockMetricsPanel = lazy(() => import('./StockMetricsPanel'));
export const StockDetail = () => {
const { stockId = '' } = useParams();
const { data: stockDetail } = useGetStockDetail({ stockId });
- const { eps, high52w, low52w, marketCap, per, name } = stockDetail || {};
+ const { eps, high52w, low52w, marketCap, per, name } = stockDetail;
const { data: userOwnerStock } = useGetOwnership({ stockId });
- if (!stockDetail || !userOwnerStock) {
- return
데이터가 없습니다.
;
+ if (!userOwnerStock) {
+ return
주식 소유 여부를 불러오지 못했습니다.
;
}
return (
@@ -34,13 +33,17 @@ export const StockDetail = () => {
-
+ 상세 정보를 불러오지 못했어요.}>
+ }>
+
+
+
diff --git a/packages/frontend/src/pages/stock-detail/StockMetricsPanel.tsx b/packages/frontend/src/pages/stock-detail/StockMetricsPanel.tsx
index 50b36656..6c1a6ca3 100644
--- a/packages/frontend/src/pages/stock-detail/StockMetricsPanel.tsx
+++ b/packages/frontend/src/pages/stock-detail/StockMetricsPanel.tsx
@@ -12,7 +12,7 @@ interface RealTimeStockData {
volume: number;
}
-export const StockMetricsPanel = ({
+const StockMetricsPanel = ({
eps,
high52w,
low52w,
@@ -75,3 +75,5 @@ export const StockMetricsPanel = ({
);
};
+
+export default StockMetricsPanel;
diff --git a/packages/frontend/src/pages/stock-detail/TradingChart.tsx b/packages/frontend/src/pages/stock-detail/TradingChart.tsx
index 0d94db21..8b2b15d9 100644
--- a/packages/frontend/src/pages/stock-detail/TradingChart.tsx
+++ b/packages/frontend/src/pages/stock-detail/TradingChart.tsx
@@ -23,14 +23,9 @@ export const TradingChart = () => {
timeunit,
});
- const allPriceData = data?.pages.flatMap((page) => page.priceDtoList) ?? [];
- const allVolumeData = data?.pages.flatMap((page) => page.volumeDtoList) ?? [];
-
- const chart = useChart({
- priceData: allPriceData,
- volumeData: allVolumeData,
- containerRef,
- });
+ const { priceDtoList: priceData = [], volumeDtoList: volumeData = [] } =
+ data || {};
+ const chart = useChart({ priceData, volumeData, containerRef });
const fetchGraphData = useCallback(
async (logicalRange: LogicalRange | null) => {
diff --git a/packages/frontend/src/pages/stocks/StockRankingTable.tsx b/packages/frontend/src/pages/stocks/StockRankingTable.tsx
index 2d61aa1a..92a8cb36 100644
--- a/packages/frontend/src/pages/stocks/StockRankingTable.tsx
+++ b/packages/frontend/src/pages/stocks/StockRankingTable.tsx
@@ -1,13 +1,15 @@
-import { useState } from 'react';
+import { Suspense, useState } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
import { Link } from 'react-router-dom';
import { usePostStockView } from '@/apis/queries/stock-detail';
import { useGetStocksByPrice } from '@/apis/queries/stocks';
import DownArrow from '@/assets/down-arrow.svg?react';
+import { Loader } from '@/components/ui/loader';
import { cn } from '@/utils/cn';
const LIMIT = 10;
-export const StockRankingTable = () => {
+const StockRankingTable = () => {
const [sortType, setSortType] = useState<'increase' | 'decrease'>('increase');
const { data } = useGetStocksByPrice({ limit: LIMIT, type: sortType });
@@ -49,46 +51,52 @@ export const StockRankingTable = () => {
- {data ? (
- data.result.map((stock, index) => (
-
-
-
- {index + 1}
-
- mutate({ stockId: stock.id })}
- className="display-bold14 hover:text-orange cursor-pointer text-ellipsis hover:underline"
- aria-label={stock.name}
- >
- {stock.name}
-
- |
- {stock.currentPrice?.toLocaleString()}원 |
- = 0 ? 'text-red' : 'text-blue',
- )}
+ 종목 정보를 불러오는데 실패했어요.
+ }
+ >
+ }>
+ {data.result.map((stock, index) => (
+
- {stock.changeRate}%
-
-
- {stock.volume?.toLocaleString()}원
- |
-
- {stock.marketCap?.toLocaleString()}주
- |
-
- ))
- ) : (
- 종목 정보를 불러오는데 실패했어요.
- )}
+
+
+ {index + 1}
+
+ mutate({ stockId: stock.id })}
+ className="display-bold14 hover:text-orange cursor-pointer text-ellipsis hover:underline"
+ aria-label={stock.name}
+ >
+ {stock.name}
+
+ |
+ {stock.currentPrice?.toLocaleString()}원 |
+ = 0 ? 'text-red' : 'text-blue',
+ )}
+ >
+ {stock.changeRate}%
+ |
+
+ {stock.volume?.toLocaleString()}원
+ |
+
+ {stock.marketCap?.toLocaleString()}주
+ |
+ |
+ ))}
+
+
);
};
+
+export default StockRankingTable;
diff --git a/packages/frontend/src/pages/stocks/Stocks.tsx b/packages/frontend/src/pages/stocks/Stocks.tsx
index 064b5889..5b9a9885 100644
--- a/packages/frontend/src/pages/stocks/Stocks.tsx
+++ b/packages/frontend/src/pages/stocks/Stocks.tsx
@@ -1,18 +1,20 @@
+import { lazy, Suspense } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
import { useNavigate } from 'react-router-dom';
import { StockIndexCard } from './components/StockIndexCard';
import { StockInfoCard } from './components/StockInfoCard';
-import { StockRankingTable } from './StockRankingTable';
import { usePostStockView } from '@/apis/queries/stock-detail';
-import { useGetTopViews } from '@/apis/queries/stocks';
-import { useGetStockIndex } from '@/apis/queries/stocks/useGetStockIndex';
+import { useStockQueries } from '@/apis/queries/stocks';
+import { Loader } from '@/components/ui/loader';
+
+const StockRankingTable = lazy(() => import('./StockRankingTable'));
const LIMIT = 5;
export const Stocks = () => {
const navigate = useNavigate();
- const { data: stockIndex } = useGetStockIndex();
- const { data: topViews } = useGetTopViews({ limit: LIMIT });
+ const [stockIndex, topViews] = useStockQueries({ viewsLimit: LIMIT });
const { mutate } = usePostStockView();
return (
@@ -22,48 +24,54 @@ export const Stocks = () => {
지금 시장, 이렇게 움직이고 있어요.
- {stockIndex ? (
-
- {stockIndex?.map((info) => (
-
- ))}
-
- ) : (
- 지수 정보를 불러오는 데 실패했어요.
- )}
+ 지수 정보를 불러오는데 실패했어요.
}
+ >
+ }>
+
+ {stockIndex.data.map((info) => (
+
+ ))}
+
+
+
+
이 종목은 어떠신가요?
-
- {topViews ? (
- topViews.map((stock, index) => (
-
{
- mutate({ stockId: stock.id ?? '' });
- navigate(`/stocks/${stock.id}`);
- }}
- />
- ))
- ) : (
- 종목 정보를 불러오는데 실패했어요.
- )}
-
+ 종목 정보를 불러오는데 실패했어요.}
+ >
+ }>
+
+ {topViews.data.map((stock, index) => (
+ {
+ mutate({ stockId: stock.id ?? '' });
+ navigate(`/stocks/${stock.id}`);
+ }}
+ />
+ ))}
+
+
+
+
지금 가장 활발한 종목이에요.
diff --git a/packages/frontend/src/routes/index.tsx b/packages/frontend/src/routes/index.tsx
index 5a649e29..ac6ce782 100644
--- a/packages/frontend/src/routes/index.tsx
+++ b/packages/frontend/src/routes/index.tsx
@@ -1,4 +1,5 @@
import { createBrowserRouter } from 'react-router-dom';
+import { Error } from '@/components/errors/error';
import { Layout } from '@/components/layouts';
import { LoginProvider } from '@/contexts/login';
import { ThemeProvider } from '@/contexts/theme';
@@ -38,6 +39,10 @@ export const router = createBrowserRouter([
path: '/login',
element: ,
},
+ {
+ path: '*',
+ element: ,
+ },
],
},
],
diff --git a/yarn.lock b/yarn.lock
index c0bc1f39..099e8c9e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7444,6 +7444,13 @@ react-docgen@^7.0.0:
loose-envify "^1.1.0"
scheduler "^0.23.2"
+react-error-boundary@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.1.2.tgz#bc750ad962edb8b135d6ae922c046051eb58f289"
+ integrity sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+
react-is@^17.0.1:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"