diff --git a/src/Router.tsx b/src/Router.tsx index 73428f7..2039714 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,8 +1,12 @@ import { lazy } from "react"; -import { createBrowserRouter, Outlet, RouteObject } from "react-router-dom"; +import { createBrowserRouter, RouteObject } from "react-router-dom"; + +import profileLoader from "./pages/ProfilePage/loader/profileLoader"; import { ROUTES } from "./constants/router"; +import AuthLayout from "./layouts/AuthLayout"; +import MainLayout from "./layouts/MainLayout"; const SignupPage = lazy(() => import("@/pages/SignupPage")); const SigninPage = lazy(() => import("@/pages/SigninPage")); @@ -51,6 +55,7 @@ const profileRoutes: RouteObject[] = [ { path: ROUTES.PROFILE.ROOT, Component: ProfilePage, + loader: profileLoader, }, { path: ROUTES.PROFILE.REGISTER, @@ -93,11 +98,11 @@ const appRoutes: RouteObject[] = [ export const router = createBrowserRouter([ { - Component: Outlet, + Component: AuthLayout, children: authRoutes, }, { - Component: Outlet, + Component: MainLayout, children: appRoutes, }, ]); diff --git a/src/components/Table.tsx b/src/components/Table.tsx index c0d7093..fda7fc3 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -23,7 +23,7 @@ function Table({ ...props }: TableProps) { return ( -
+
{/* ───── 스크롤이 필요한 영역 ───── */}
void; @@ -11,7 +11,7 @@ interface MainLayoutProps { } export default function MainLayout({ - isLoggedIn, + isLoggedIn = false, userNavLabel, hasAlarm, onLogout, @@ -19,19 +19,17 @@ export default function MainLayout({ }: MainLayoutProps) { return (
-
-
+
-
- -
-
+
+ +
diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index 473488c..3c91d1b 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -1,3 +1,77 @@ +import { useLoaderData, useNavigate } from "react-router-dom"; + +import ProfileCard from "./components/ProfileCard"; +import UserApplicationTable from "./components/UserApplicationTable"; +import UserApplicationTableSkeleton from "./components/UserApplicationTableSkeleton"; +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; +const PAGE_LIMIT = 7; + export default function ProfilePage() { - return
ProfilePage
; + const { userInfo } = useLoaderData<{ + userInfo: UserItem; + count: number; + userApplications: UserApplicationList[]; + }>(); + const navigate = useNavigate(); + const { isLoading, totalCount, userApplications } = useUserApplications(); + + return ( + <> +
+
+
+

내 프로필

+ {userInfo && ( + navigate(ROUTES.PROFILE.EDIT)} + /> + )} +
+ {!userInfo && ( + navigate(ROUTES.PROFILE.REGISTER)} + /> + )} +
+
+ + {userInfo && ( +
+
+

신청 내역

+ + {isLoading && } + + {!isLoading && userApplications.length === 0 && ( + navigate(ROUTES.NOTICE.ROOT)} + /> + )} + + {!isLoading && userApplications.length > 0 && ( + + )} +
+
+ )} + + ); } diff --git a/src/pages/ProfilePage/components/ProfileCard.tsx b/src/pages/ProfilePage/components/ProfileCard.tsx index 84ad519..f94c955 100644 --- a/src/pages/ProfilePage/components/ProfileCard.tsx +++ b/src/pages/ProfilePage/components/ProfileCard.tsx @@ -3,14 +3,28 @@ import { MouseEvent } from "react"; import { Location, Phone } from "@/assets/icon"; import Button from "@/components/Button"; import { UserSummary } from "@/types/user"; +import { cn } from "@/utils/cn"; interface ProfileCardProps extends UserSummary { onClick?: (e: MouseEvent) => void; + className?: string; } -function ProfileCard({ name, phone, address, bio, onClick }: ProfileCardProps) { +function ProfileCard({ + name, + phone, + address, + bio, + className, + onClick, +}: ProfileCardProps) { return ( -
+
diff --git a/src/pages/ProfilePage/components/UserApplicationTable.tsx b/src/pages/ProfilePage/components/UserApplicationTable.tsx index bcf8e74..a4686b1 100644 --- a/src/pages/ProfilePage/components/UserApplicationTable.tsx +++ b/src/pages/ProfilePage/components/UserApplicationTable.tsx @@ -1,12 +1,12 @@ import Pagination from "@/components/Pagination"; import StatusBadge from "@/components/StatusBadge"; import Table from "@/components/Table"; -import { ApplicationItem } from "@/types/application"; +import { UserApplicationList } from "@/types/application"; import { formatTimeRange } from "@/utils/datetime"; import { numberCommaFormatter } from "@/utils/number"; interface UserApplicationTableProps { - data: ApplicationItem[]; + data: UserApplicationList[]; pageCount: number; pageLimit: number; itemCountPerPage?: number; diff --git a/src/pages/ProfilePage/components/UserApplicationTableSkeleton.tsx b/src/pages/ProfilePage/components/UserApplicationTableSkeleton.tsx new file mode 100644 index 0000000..923319a --- /dev/null +++ b/src/pages/ProfilePage/components/UserApplicationTableSkeleton.tsx @@ -0,0 +1,10 @@ +function UserApplicationTableSkeleton() { + return ( +
+
+
+
+ ); +} + +export default UserApplicationTableSkeleton; diff --git a/src/pages/ProfilePage/hooks/useUserApplications.ts b/src/pages/ProfilePage/hooks/useUserApplications.ts new file mode 100644 index 0000000..fc63576 --- /dev/null +++ b/src/pages/ProfilePage/hooks/useUserApplications.ts @@ -0,0 +1,48 @@ +import { useEffect, useState } from "react"; + +import { useSearchParams } from "react-router-dom"; + +import { getUserApplications } from "@/apis/services/applicationService"; +import { UserApplicationList } from "@/types/application"; + +interface UseUserApplicationsParams { + offset?: number; + limit?: number; +} + +const useUserApplications = (params?: UseUserApplicationsParams) => { + const { offset = 5, limit = 7 } = params ?? {}; + + const [searchParams] = useSearchParams(); + const [isLoading, setIsLoading] = useState(false); + const [totalCount, setTotalCount] = useState(0); + const [userApplications, setUserApplications] = useState< + UserApplicationList[] + >([]); + const page = Number(searchParams.get("page")) || 1; + + const fetchUserApplication = async () => { + setIsLoading(true); + const userApplications = await getUserApplications( + "42859259-b879-408c-8edd-bbaa3a79c674", + (page - 1) * offset, + limit, + ); + + const nextUserApplications = userApplications.data.items.map( + ({ item }) => item, + ); + + setTotalCount(userApplications.data.count); + setUserApplications(nextUserApplications); + setIsLoading(false); + }; + + useEffect(() => { + fetchUserApplication(); + }, [page]); + + return { userApplications, isLoading, totalCount }; +}; + +export default useUserApplications; diff --git a/src/pages/ProfilePage/loader/profileLoader.ts b/src/pages/ProfilePage/loader/profileLoader.ts new file mode 100644 index 0000000..bc7c1af --- /dev/null +++ b/src/pages/ProfilePage/loader/profileLoader.ts @@ -0,0 +1,13 @@ +import { getUser } from "@/apis/services/userService"; + +const profileLoader = async () => { + const userInfo = await getUser("42859259-b879-408c-8edd-bbaa3a79c674"); + + if (userInfo.status === 200) { + return { + userInfo: userInfo.data.item, + }; + } +}; + +export default profileLoader; diff --git a/src/styles/utilities.css b/src/styles/utilities.css index 53a5cb5..a6010be 100644 --- a/src/styles/utilities.css +++ b/src/styles/utilities.css @@ -19,4 +19,32 @@ font-size: 0.875rem; /* 14px */ line-height: 1; } + + @keyframes skeleton { + 0% { + opacity: 1; + background-position: 200% 0; + } + 50% { + opacity: 0.5; + background-position: 0% 0; + } + 100% { + opacity: 1; + background-position: -200% 0; + } + } + + .animate-skeleton { + background-image: linear-gradient( + 90deg, + var(--color-gray-20) 35%, + var(--color-white) 50%, + var(--color-gray-20) 65% + ); + background-size: 200% 100%; + background-repeat: no-repeat; + animation: skeleton 1.2s linear infinite; + background-color: var(--color-gray-20); + } }