diff --git a/apps/client/src/app/App.tsx b/apps/client/src/app/App.tsx index 6be0a6f..a921e82 100644 --- a/apps/client/src/app/App.tsx +++ b/apps/client/src/app/App.tsx @@ -1,9 +1,10 @@ +import { ErrorPage, WorkspaceErrorPage } from '@/pages'; import { RouterProvider, createBrowserRouter } from 'react-router-dom'; -import { ToasterWithMax } from '@/shared/ui'; -import { ErrorPage } from '@/pages/ErrorPage/ErrorPage'; -import { lazy, Suspense } from 'react'; -import { Loading } from '@/shared/ui'; +import { Suspense, lazy } from 'react'; + import { Helmet } from 'react-helmet-async'; +import { Loading } from '@/shared/ui'; +import { ToasterWithMax } from '@/shared/ui'; // lazy 로딩 const HomePage = lazy(() => @@ -47,12 +48,13 @@ const router = createBrowserRouter([ BooLock - 작업 공간 + }> ), - errorElement: , + errorElement: , }, { path: '*', diff --git a/apps/client/src/pages/HomePage/HomePage.tsx b/apps/client/src/pages/HomePage/HomePage.tsx index abe0c37..2f7a462 100644 --- a/apps/client/src/pages/HomePage/HomePage.tsx +++ b/apps/client/src/pages/HomePage/HomePage.tsx @@ -1,4 +1,4 @@ -import { Banner, HomeHeader, WorkspaceContainer, WorkspaceModal } from '@/widgets'; +import { Banner, HomeHeader, WorkspaceModal, WorkspaceSection } from '@/widgets'; import { useClassBlockStore, useLoadingStore, useWorkspaceStore } from '@/shared/store'; import { Loading } from '@/shared/ui'; @@ -26,7 +26,7 @@ export const HomePage = () => {
- +
diff --git a/apps/client/src/pages/WorkspaceErrorPage/WorkspaceErrorPage.tsx b/apps/client/src/pages/WorkspaceErrorPage/WorkspaceErrorPage.tsx new file mode 100644 index 0000000..eb99bf2 --- /dev/null +++ b/apps/client/src/pages/WorkspaceErrorPage/WorkspaceErrorPage.tsx @@ -0,0 +1,14 @@ +import { ErrorPage, NotFound } from '@/pages'; + +import toast from 'react-hot-toast'; +import { useRouteError } from 'react-router-dom'; + +export const WorkspaceErrorPage = () => { + const error: any = useRouteError(); + const statusCode = error?.response?.statusCode || error?.status; + if (statusCode === 404) { + toast.error('워크스페이스 정보 불러오기 실패'); + return ; + } + return ; +}; diff --git a/apps/client/src/pages/Workspacepage/WorkspacePage.tsx b/apps/client/src/pages/Workspacepage/WorkspacePage.tsx index 32f7bb1..4078b58 100644 --- a/apps/client/src/pages/Workspacepage/WorkspacePage.tsx +++ b/apps/client/src/pages/Workspacepage/WorkspacePage.tsx @@ -1,10 +1,9 @@ -import { ImageTagModal, CoachMark, WorkspaceContent, WorkspacePageHeader } from '@/widgets'; +import { CoachMark, ImageTagModal, WorkspaceContent, WorkspacePageHeader } from '@/widgets'; +import { useEffect, useLayoutEffect } from 'react'; import { useGetWorkspace, usePreventLeaveWorkspacePage } from '@/shared/hooks'; -import { Loading } from '@/shared/ui'; -import { NotFound } from '@/pages/NotFound/NotFound'; -import { useParams } from 'react-router-dom'; -import { useLayoutEffect, useEffect } from 'react'; + import { useCoachMarkStore } from '@/shared/store/useCoachMarkStore'; +import { useParams } from 'react-router-dom'; /** * @@ -13,7 +12,7 @@ import { useCoachMarkStore } from '@/shared/store/useCoachMarkStore'; */ export const WorkspacePage = () => { const { workspaceId } = useParams(); - const { isPending, isError } = useGetWorkspace(workspaceId as string); + useGetWorkspace(workspaceId as string); usePreventLeaveWorkspacePage(); const { currentStep, isCoachMarkOpen, openCoachMark } = useCoachMarkStore(); const toolboxDiv = document.querySelector('.blocklyToolboxDiv'); @@ -36,14 +35,9 @@ export const WorkspacePage = () => { } }, [currentStep, toolboxDiv]); - if (isError) { - return ; - } - return ( <>
- {isPending && } {isCoachMarkOpen && } diff --git a/apps/client/src/pages/index.ts b/apps/client/src/pages/index.ts index 90faf1f..a4d2149 100644 --- a/apps/client/src/pages/index.ts +++ b/apps/client/src/pages/index.ts @@ -1,3 +1,5 @@ export { HomePage } from './HomePage/HomePage'; export { NotFound } from './NotFound/NotFound'; export { WorkspacePage } from './Workspacepage/WorkspacePage'; +export { ErrorPage } from './ErrorPage/ErrorPage'; +export { WorkspaceErrorPage } from './WorkspaceErrorPage/WorkspaceErrorPage'; diff --git a/apps/client/src/shared/hooks/queries/useGetWorkspace.ts b/apps/client/src/shared/hooks/queries/useGetWorkspace.ts index 23cb790..83e85e7 100644 --- a/apps/client/src/shared/hooks/queries/useGetWorkspace.ts +++ b/apps/client/src/shared/hooks/queries/useGetWorkspace.ts @@ -3,16 +3,15 @@ import { createUserId, getUserId, removeCssClassNamePrefix } from '@/shared/util import { useClassBlockStore, useCssPropsStore, + useImageModalStore, useResetCssStore, useWorkspaceChangeStatusStore, useWorkspaceStore, - useImageModalStore, } from '@/shared/store'; import { WorkspaceApi } from '@/shared/api'; -import toast from 'react-hot-toast'; import { useEffect } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { workspaceKeys } from '@/shared/hooks'; export const useGetWorkspace = (workspaceId: string) => { @@ -24,29 +23,19 @@ export const useGetWorkspace = (workspaceId: string) => { const { resetChangedStatusState } = useWorkspaceChangeStatusStore(); const { setIsResetCssChecked } = useResetCssStore(); const { setInitialImageMap, setInitialImageList } = useImageModalStore(); - const { data, isPending, isError } = useQuery({ + const { data, isPending, isError } = useSuspenseQuery({ queryKey: workspaceKeys.detail(workspaceId), queryFn: () => { + resetChangedStatusState(); return workspaceApi.getWorkspace(userId, workspaceId); }, }); useEffect(() => { - resetChangedStatusState(); - }, []); - - useEffect(() => { - if (isError) { - toast.error('워크스페이스 정보 불러오기 실패'); - return; - } - if (!data) { + if (!isError || !data || !data.workspaceDto) { return; } - if (!data.workspaceDto) { - return; - } setName(data.workspaceDto.name); Object.keys(data.workspaceDto.totalCssPropertyObj).forEach((className) => { createCssClassBlock(className); diff --git a/apps/client/src/shared/hooks/queries/useGetWorkspaceList.ts b/apps/client/src/shared/hooks/queries/useGetWorkspaceList.ts index b8f5539..891de16 100644 --- a/apps/client/src/shared/hooks/queries/useGetWorkspaceList.ts +++ b/apps/client/src/shared/hooks/queries/useGetWorkspaceList.ts @@ -1,6 +1,7 @@ -import { WorkspaceApi } from '@/shared/api'; import { createUserId, getUserId } from '@/shared/utils'; -import { useInfiniteQuery } from '@tanstack/react-query'; + +import { WorkspaceApi } from '@/shared/api'; +import { useSuspenseInfiniteQuery } from '@tanstack/react-query'; import { workspaceKeys } from '@/shared/hooks'; export const useGetWorkspaceList = () => { const workspaceApi = WorkspaceApi(); @@ -12,7 +13,7 @@ export const useGetWorkspaceList = () => { isFetchingNextPage, isError, data: workspaceList, - } = useInfiniteQuery({ + } = useSuspenseInfiniteQuery({ queryKey: workspaceKeys.list(), queryFn: async ({ pageParam }) => { const isNewUser = !getUserId(); diff --git a/apps/client/src/widgets/home/WorkspaceContainer/WorkspaceContainer.stories.tsx b/apps/client/src/widgets/home/WorkspaceContainer/WorkspaceContainer.stories.tsx index 7b22b86..f2b36a3 100644 --- a/apps/client/src/widgets/home/WorkspaceContainer/WorkspaceContainer.stories.tsx +++ b/apps/client/src/widgets/home/WorkspaceContainer/WorkspaceContainer.stories.tsx @@ -6,7 +6,7 @@ const meta: Meta = { title: 'widgets/home/WorkspaceContainer', component: WorkspaceContainer, parameters: { - layout: 'fullscreen', + layout: 'centered', }, tags: ['autodocs'], }; diff --git a/apps/client/src/widgets/home/WorkspaceContainer/WorkspaceContainer.tsx b/apps/client/src/widgets/home/WorkspaceContainer/WorkspaceContainer.tsx index b7d5857..8da8a85 100644 --- a/apps/client/src/widgets/home/WorkspaceContainer/WorkspaceContainer.tsx +++ b/apps/client/src/widgets/home/WorkspaceContainer/WorkspaceContainer.tsx @@ -1,17 +1,16 @@ -import { EmptyWorkspace, WorkspaceGrid, WorkspaceHeader, WorkspaceList } from '@/widgets'; +import { EmptyWorkspace, WorkspaceGrid, WorkspaceList } from '@/widgets'; import { useGetWorkspaceList, useInfiniteScroll, useVirtualScroll } from '@/shared/hooks'; import { SkeletonWorkspaceList } from '@/shared/ui'; import { TWorkspace } from '@/shared/types'; -import { WorkspaceLoadError } from '@/entities'; /** * * @description - * 워크스페이스 헤더와 그리드를 감싸는 컨테이너 컴포넌트 + * 워크스페이스 리스트를 렌더링하는 컨테이너 컴포넌트 */ export const WorkspaceContainer = () => { - const { hasNextPage, fetchNextPage, isPending, isFetchingNextPage, isError, workspaceList } = + const { hasNextPage, fetchNextPage, isPending, isFetchingNextPage, workspaceList } = useGetWorkspaceList(); const { renderedData, offsetY, totalHeight } = useVirtualScroll({ @@ -33,17 +32,8 @@ export const WorkspaceContainer = () => { const nextFetchTargetRef = useInfiniteScroll({ intersectionCallback: fetchCallback }); return ( -
- - {isPending && ( - - - - )} - {isError ? ( - - ) : ( - workspaceList && + <> + {workspaceList && (workspaceList.length === 0 ? ( ) : ( @@ -57,11 +47,10 @@ export const WorkspaceContainer = () => { {isFetchingNextPage && }
- )) - )} + ))} {!isPending && !isFetchingNextPage && hasNextPage && (
)} - + ); }; diff --git a/apps/client/src/widgets/home/WorkspaceSection/WorkspaceSection.stories.tsx b/apps/client/src/widgets/home/WorkspaceSection/WorkspaceSection.stories.tsx new file mode 100644 index 0000000..0ab8538 --- /dev/null +++ b/apps/client/src/widgets/home/WorkspaceSection/WorkspaceSection.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { WorkspaceSection } from './WorkspaceSection'; + +const meta: Meta = { + title: 'widgets/home/WorkspaceSection', + component: WorkspaceSection, + parameters: { + layout: 'fullscreen', + }, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + // propsname: value, + }, +}; diff --git a/apps/client/src/widgets/home/WorkspaceSection/WorkspaceSection.tsx b/apps/client/src/widgets/home/WorkspaceSection/WorkspaceSection.tsx new file mode 100644 index 0000000..7f1184d --- /dev/null +++ b/apps/client/src/widgets/home/WorkspaceSection/WorkspaceSection.tsx @@ -0,0 +1,20 @@ +import { WorkspaceContainer, WorkspaceHeader } from '@/widgets'; + +import { SkeletonWorkspaceList } from '@/shared/ui'; +import { Suspense } from 'react'; + +/** + * + * @description + * 워크스페이스 헤더와 컨테이너를 합친 섹션 컴포넌트 + */ +export const WorkspaceSection = () => { + return ( +
+ + }> + + +
+ ); +}; diff --git a/apps/client/src/widgets/index.ts b/apps/client/src/widgets/index.ts index 68e0015..b4ddcd0 100644 --- a/apps/client/src/widgets/index.ts +++ b/apps/client/src/widgets/index.ts @@ -4,8 +4,9 @@ export { WorkspaceList } from './home/WorkspaceList/WorkspaceList'; export { WorkspaceHeader } from './home/WorkspaceHeader/WorkspaceHeader'; export { EmptyWorkspace } from './home/EmptyWorkspace/EmptyWorkspace'; export { WorkspaceGrid } from './home/WorkspaceGrid/WorkspaceGrid'; -export { WorkspaceContainer } from './home/WorkspaceContainer/WorkspaceContainer'; +export { WorkspaceSection } from './home/WorkspaceSection/WorkspaceSection'; export { WorkspaceModal } from './home/WorkspaceModal/WorkspaceModal'; +export { WorkspaceContainer } from './home/WorkspaceContainer/WorkspaceContainer'; export { PreviewBox } from './workspace/PreviewBox/PreviewBox'; export { CoachMark } from './workspace/CoachMark/CoachMark'; diff --git a/apps/server/src/middlewares/errorMiddleware.ts b/apps/server/src/middlewares/errorMiddleware.ts index a5e8c8b..aaedc57 100644 --- a/apps/server/src/middlewares/errorMiddleware.ts +++ b/apps/server/src/middlewares/errorMiddleware.ts @@ -1,5 +1,12 @@ -import { Request, Response, NextFunction } from 'express'; -import { CustomError } from '../utils/customError'; +import { + BadRequestError, + CustomError, + ForbiddenError, + UnauthorizedError, +} from '../utils/customError'; +import { NextFunction, Request, Response } from 'express'; + +import { NotFound } from '@aws-sdk/client-s3'; import { errorStatus } from '../utils/constants'; // eslint-disable-next-line no-unused-vars @@ -19,4 +26,16 @@ const errorHandlers: { [key: string]: (err: any, res: Response) => void } = { CustomError: (err: CustomError, res: Response) => { res.status(err.statusCode).json({ message: err.message }); }, + NotFoundError: (err: NotFound, res: Response) => { + res.status(errorStatus.HTTP_404_NOT_FOUND).json({ message: err.message }); + }, + BadRequestError: (err: BadRequestError, res: Response) => { + res.status(errorStatus.HTTP_400_BAD_REQUEST).json({ message: err.message }); + }, + UnauthorizedError: (err: UnauthorizedError, res: Response) => { + res.status(errorStatus.HTTP_401_UNAUTHORIZED).json({ message: err.message }); + }, + ForbiddenError: (err: ForbiddenError, res: Response) => { + res.status(errorStatus.HTTP_403_FORBIDDEN).json({ message: err.message }); + }, }; diff --git a/apps/server/src/utils/customError.ts b/apps/server/src/utils/customError.ts index 8d98579..0cb6956 100644 --- a/apps/server/src/utils/customError.ts +++ b/apps/server/src/utils/customError.ts @@ -10,14 +10,14 @@ export class CustomError extends Error { } export class NotFoundError extends CustomError { - constructor(message = 'Bad request') { - super(message, errorStatus.HTTP_400_BAD_REQUEST); + constructor(message = 'Resource not found') { + super(message, errorStatus.HTTP_404_NOT_FOUND); } } export class BadRequestError extends CustomError { - constructor(message = 'Resource not found') { - super(message, errorStatus.HTTP_404_NOT_FOUND); + constructor(message = 'Bad request') { + super(message, errorStatus.HTTP_400_BAD_REQUEST); } }