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"