diff --git a/src/Router.tsx b/src/Router.tsx index 8c15694..052674c 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -8,6 +8,8 @@ import profileLoader from "./pages/ProfilePage/loader/profileLoader"; import NoticeDetailSkeleton from "./components/NoticeDetailSkeleton"; import PageErrorElement from "./components/PageErrorElement"; +import ProtectedRoute from "./components/ProtectedRoute"; +import { loginProtectCondition } from "./constants/router"; import { ROUTES } from "./constants/router"; import AuthLayout from "./layouts/AuthLayout"; import MainLayout from "./layouts/MainLayout"; @@ -45,27 +47,27 @@ const NotFoundPage = lazy(() => import("@/pages/NotFoundPage")); const authRoutes: RouteObject[] = [ { path: ROUTES.AUTH.SIGNUP, - Component: SignupPage, + element: , }, { path: ROUTES.AUTH.SIGNIN, - Component: SigninPage, + element: , }, ]; const shopRoutes: RouteObject[] = [ { path: ROUTES.SHOP.ROOT, - Component: ShopPage, + element: , }, { path: ROUTES.SHOP.REGISTER, - Component: ShopRegisterPage, + element: , handle: { hideFooter: true }, }, { path: ROUTES.SHOP.EDIT, - Component: ShopEditPage, + element: , handle: { hideFooter: true }, }, ]; @@ -73,17 +75,30 @@ const shopRoutes: RouteObject[] = [ const profileRoutes: RouteObject[] = [ { path: ROUTES.PROFILE.ROOT, - Component: ProfilePage, + element: ( + [ + loginProtectCondition(isLoggedIn), + { + isPass: user?.type === "employee", + redirectPath: ROUTES.SHOP.ROOT, + message: "알바생 계정으로만 이용 가능한 기능입니다.", + }, + ]} + > + + + ), loader: profileLoader, }, { path: ROUTES.PROFILE.REGISTER, - Component: ProfileRegisterPage, + element: , handle: { hideFooter: true }, }, { path: ROUTES.PROFILE.EDIT, - Component: ProfileEditPage, + element: , handle: { hideFooter: true }, }, ]; @@ -91,31 +106,57 @@ const profileRoutes: RouteObject[] = [ const noticeRoutes: RouteObject[] = [ { path: ROUTES.NOTICE.ROOT, - Component: NoticeListPage, + element: , }, { path: ROUTES.NOTICE.SEARCH, - Component: NoticeSearchPage, + element: , }, { path: ROUTES.NOTICE.REGISTER, - Component: NoticeRegisterPage, + element: , handle: { hideFooter: true }, }, { path: ROUTES.NOTICE.EDIT, - Component: NoticeEditPage, + element: , handle: { hideFooter: true }, }, { path: ROUTES.NOTICE.NOTICE_ID.EMPLOYER, - Component: NoticeEmployerPage, + element: ( + [ + loginProtectCondition(isLoggedIn), + { + isPass: user?.type === "employer", + redirectPath: ROUTES.PROFILE.ROOT, + message: "사장님 계정으로만 이용 가능한 기능입니다.", + }, + ]} + > + + + ), loader: noticeEmployerLoader, hydrateFallbackElement: , }, { path: ROUTES.NOTICE.NOTICE_ID.EMPLOYEE, - Component: NoticeEmployeePage, + element: ( + [ + loginProtectCondition(isLoggedIn), + { + isPass: user?.type === "employee", + redirectPath: ROUTES.SHOP.ROOT, + message: "알바생 계정으로만 이용 가능한 기능입니다.", + }, + ]} + > + + + ), loader: noticeEmployeeLoader, hydrateFallbackElement: , }, @@ -125,12 +166,12 @@ const appRoutes: RouteObject[] = [ ...shopRoutes, ...profileRoutes, ...noticeRoutes, - { path: "*", Component: NotFoundPage }, + { path: "*", element: }, ]; export const router = createBrowserRouter([ { - Component: AuthLayout, + element: , children: [ { children: authRoutes, @@ -139,7 +180,7 @@ export const router = createBrowserRouter([ ], }, { - Component: MainLayout, + element: , children: [ { children: appRoutes, diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..2c4098d --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -0,0 +1,60 @@ +import { ReactNode, useLayoutEffect } from "react"; + +import { useNavigate } from "react-router-dom"; + +import { User, useUserStore } from "@/hooks/useUserStore"; +import { useModalStore } from "@/store/useModalStore"; + +interface ProtectedRouteConditionType { + isPass: boolean; + redirectPath: string; + message: string; +} + +interface ProtectedRouteParamType { + user: User | null; + isLoggedIn: boolean; +} + +interface ProtectedRouteProps { + children: ReactNode; + conditions: ( + params: ProtectedRouteParamType, + ) => ProtectedRouteConditionType[]; +} + +function ProtectedRoute({ children, conditions }: ProtectedRouteProps) { + const navigate = useNavigate(); + const user = useUserStore((state) => state.user); + const isLoggedIn = useUserStore((state) => state.isLoggedIn); + const openModal = useModalStore((state) => state.openModal); + + useLayoutEffect(() => { + if (user === undefined) return; + const receivedConditions = conditions({ user, isLoggedIn }); + + for (const { isPass, redirectPath, message } of receivedConditions) { + if (!isPass) { + if (message) { + openModal({ + type: "alert", + iconType: "warning", + message, + onClose: () => navigate(redirectPath), + }); + } else { + navigate(redirectPath); + } + break; + } + } + }, [user, isLoggedIn, conditions, openModal, navigate]); + + if (user === undefined) { + return null; + } + + return children; +} + +export default ProtectedRoute; diff --git a/src/constants/router.ts b/src/constants/router.ts index 5b86a64..b4dcc36 100644 --- a/src/constants/router.ts +++ b/src/constants/router.ts @@ -1,4 +1,4 @@ -const ROUTES = { +export const ROUTES = { AUTH: { SIGNUP: "/signup", SIGNIN: "/signin", @@ -26,4 +26,9 @@ const ROUTES = { notFound: "*", } as const; -export { ROUTES }; +export const loginProtectCondition = (isLoggedIn: boolean) => + ({ + isPass: isLoggedIn, + redirectPath: ROUTES.AUTH.SIGNIN, + message: "로그인 후에 이용 가능한 기능입니다.", + }) as const; diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index 6245db9..20225af 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -7,7 +7,6 @@ import useUserApplications from "./hooks/useUserApplications"; import EmptyStateCard from "@/components/EmptyStateCard"; import { ROUTES } from "@/constants/router"; -import { UserApplicationList } from "@/types/application"; import { UserItem } from "@/types/user"; const LIMIT = 5; @@ -16,14 +15,17 @@ const PAGE_LIMIT = 7; export default function ProfilePage() { const { userInfo } = useLoaderData<{ userInfo: UserItem; - count: number; - userApplications: UserApplicationList[]; }>(); + const navigate = useNavigate(); const { isLoading, totalCount, userApplications } = useUserApplications({ - userId: userInfo.id, + userId: userInfo?.id, }); + if (!userInfo) { + return null; + } + return ( <>
diff --git a/src/pages/ProfilePage/hooks/useUserApplications.ts b/src/pages/ProfilePage/hooks/useUserApplications.ts index c4d4b0d..ef424ed 100644 --- a/src/pages/ProfilePage/hooks/useUserApplications.ts +++ b/src/pages/ProfilePage/hooks/useUserApplications.ts @@ -24,26 +24,31 @@ const useUserApplications = ({ >([]); const page = Number(searchParams.get("page")) || 1; - const fetchUserApplication = async () => { - setIsLoading(true); - const userApplications = await getUserApplications( - userId, - (page - 1) * offset, - limit, - ); - - const nextUserApplications = userApplications.data.items.map( - ({ item }) => item, - ); - - setTotalCount(userApplications.data.count); - setUserApplications(nextUserApplications); - setIsLoading(false); - }; - useEffect(() => { + if (!userId) return; + + const fetchUserApplication = async () => { + try { + setIsLoading(true); + const userApplications = await getUserApplications( + userId, + (page - 1) * offset, + limit, + ); + + const nextUserApplications = userApplications.data.items.map( + ({ item }) => item, + ); + + setTotalCount(userApplications.data.count); + setUserApplications(nextUserApplications); + } finally { + setIsLoading(false); + } + }; + fetchUserApplication(); - }, [page]); + }, [userId, page, offset, limit]); // 의존성 보완 return { userApplications, isLoading, totalCount }; }; diff --git a/src/pages/ProfilePage/loader/profileLoader.ts b/src/pages/ProfilePage/loader/profileLoader.ts index 1ed7e38..82953f0 100644 --- a/src/pages/ProfilePage/loader/profileLoader.ts +++ b/src/pages/ProfilePage/loader/profileLoader.ts @@ -3,6 +3,11 @@ import { useUserStore } from "@/hooks/useUserStore"; const profileLoader = async () => { const userId = useUserStore.getState().user?.id; + + if (!userId) { + return {}; + } + const userInfo = await getUser(userId ?? ""); if (userInfo.status === 200) {