From 829f163f4de89086c331757a26f7050f4c395d46 Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Wed, 30 Apr 2025 16:03:53 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20ProfilePage=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Router.tsx | 8 ++ src/pages/ProfilePage/ProfilePage.tsx | 83 ++++++++++++++++++- .../ProfilePage/components/ProfileCard.tsx | 18 +++- .../components/UserApplicationTable.tsx | 4 +- 4 files changed, 108 insertions(+), 5 deletions(-) diff --git a/src/Router.tsx b/src/Router.tsx index 73428f7..bdb573f 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -2,6 +2,7 @@ import { lazy } from "react"; import { createBrowserRouter, Outlet, RouteObject } from "react-router-dom"; +import { getUser } from "./apis/services/userService"; import { ROUTES } from "./constants/router"; const SignupPage = lazy(() => import("@/pages/SignupPage")); @@ -51,6 +52,13 @@ const profileRoutes: RouteObject[] = [ { path: ROUTES.PROFILE.ROOT, Component: ProfilePage, + loader: async () => { + const user = await getUser("42859259-b879-408c-8edd-bbaa3a79c674"); + + if (user.status === 200) { + return user.data.item; + } + }, }, { path: ROUTES.PROFILE.REGISTER, diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index 473488c..75a23f0 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -1,3 +1,84 @@ +import { useEffect, useState } from "react"; + +import { useLoaderData, useNavigate, useSearchParams } from "react-router-dom"; + +import ProfileCard from "./components/ProfileCard"; +import UserApplicationTable from "./components/UserApplicationTable"; + +import { getUserApplications } from "@/apis/services/applicationService"; +import EmptyStateCard from "@/components/EmptyStateCard"; +import { ROUTES } from "@/constants/router"; +import { UserApplicationList } from "@/types/application"; +import { UserItem } from "@/types/user"; + +const OFFSET = 5; +const LIMIT = 7; + export default function ProfilePage() { - return
ProfilePage
; + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const userInfo = useLoaderData(); + const page = Number(searchParams.get("page")) || 1; + const [userApplications, setUserApplications] = useState< + UserApplicationList[] + >([]); + + const fetchUserApplication = async () => { + const userApplications = await getUserApplications( + "42859259-b879-408c-8edd-bbaa3a79c674", + (page - 1) * OFFSET, + LIMIT, + ); + const nextUserApplications = userApplications.data.items.map( + ({ item }) => item, + ); + setUserApplications(nextUserApplications); + }; + + useEffect(() => { + fetchUserApplication(); + }, [page]); + + return ( +
+
+
+

내 프로필

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

신청 내역

+ {userApplications.length === 0 ? ( + navigate(ROUTES.NOTICE.ROOT)} + /> + ) : ( + + )} +
+ )} +
+ ); } 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; From e35c079b51dbe53c04cd7ed03fce90ed5ec1878b Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Wed, 30 Apr 2025 16:07:19 +0900 Subject: [PATCH 2/5] =?UTF-8?q?refactor:=20MainLayout=20props=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Router.tsx | 8 +++++--- src/layouts/MainLayout.tsx | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Router.tsx b/src/Router.tsx index bdb573f..6408b09 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,9 +1,11 @@ import { lazy } from "react"; -import { createBrowserRouter, Outlet, RouteObject } from "react-router-dom"; +import { createBrowserRouter, RouteObject } from "react-router-dom"; import { getUser } from "./apis/services/userService"; 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")); @@ -101,11 +103,11 @@ const appRoutes: RouteObject[] = [ export const router = createBrowserRouter([ { - Component: Outlet, + Component: AuthLayout, children: authRoutes, }, { - Component: Outlet, + Component: MainLayout, children: appRoutes, }, ]); diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 5467e7b..fdf0032 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -3,7 +3,7 @@ import { Outlet } from "react-router-dom"; import Footer from "./Footer"; import Header from "./Header"; interface MainLayoutProps { - isLoggedIn: boolean; + isLoggedIn?: boolean; userNavLabel?: "내 가게" | "내 프로필"; hasAlarm?: boolean; onLogout?: () => void; @@ -11,7 +11,7 @@ interface MainLayoutProps { } export default function MainLayout({ - isLoggedIn, + isLoggedIn = false, userNavLabel, hasAlarm, onLogout, From 06b4b985b0cbd44adb048dbb4c8f83bcc8ec4950 Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Wed, 30 Apr 2025 17:02:15 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20ProfilePage=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Table.tsx | 2 +- src/layouts/MainLayout.tsx | 22 +++---- src/pages/ProfilePage/ProfilePage.tsx | 87 ++++++++++++++++----------- 3 files changed, 63 insertions(+), 48 deletions(-) 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 ( -
+
{/* ───── 스크롤이 필요한 영역 ───── */}
-
-
+
-
- -
-
+
+ +
diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index 75a23f0..39be835 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -12,18 +12,23 @@ import { UserApplicationList } from "@/types/application"; import { UserItem } from "@/types/user"; const OFFSET = 5; -const LIMIT = 7; +const LIMIT = 5; +const PAGE_LIMIT = 7; export default function ProfilePage() { const navigate = useNavigate(); - const [searchParams] = useSearchParams(); const userInfo = useLoaderData(); - const page = Number(searchParams.get("page")) || 1; + + 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, @@ -32,7 +37,9 @@ export default function ProfilePage() { const nextUserApplications = userApplications.data.items.map( ({ item }) => item, ); + setTotalCount(userApplications.data.count); setUserApplications(nextUserApplications); + setIsLoading(false); }; useEffect(() => { @@ -40,45 +47,55 @@ export default function ProfilePage() { }, [page]); return ( -
-
-
-

내 프로필

- {userInfo && ( - navigate(ROUTES.PROFILE.EDIT)} + <> +
+
+
+

내 프로필

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

신청 내역

- {userApplications.length === 0 ? ( - navigate(ROUTES.NOTICE.ROOT)} - /> - ) : ( - - )} +
+
+

신청 내역

+ + {isLoading &&
로딩 중...
} + + {!isLoading && userApplications.length === 0 && ( + navigate(ROUTES.NOTICE.ROOT)} + /> + )} + + {!isLoading && userApplications.length > 0 && ( + + )} +
)} -
+ ); } From c758e8a5dc5075d28f4cbe2fc700cd65320675c0 Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Wed, 30 Apr 2025 17:27:51 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20hook=20=EC=B6=94=EA=B0=80,=20loader?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Router.tsx | 11 ++--- src/pages/ProfilePage/ProfilePage.tsx | 41 ++++------------ .../ProfilePage/hooks/useUserApplications.ts | 48 +++++++++++++++++++ src/pages/ProfilePage/loader/profileLoader.ts | 13 +++++ 4 files changed, 72 insertions(+), 41 deletions(-) create mode 100644 src/pages/ProfilePage/hooks/useUserApplications.ts create mode 100644 src/pages/ProfilePage/loader/profileLoader.ts diff --git a/src/Router.tsx b/src/Router.tsx index 6408b09..2039714 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -2,7 +2,8 @@ import { lazy } from "react"; import { createBrowserRouter, RouteObject } from "react-router-dom"; -import { getUser } from "./apis/services/userService"; +import profileLoader from "./pages/ProfilePage/loader/profileLoader"; + import { ROUTES } from "./constants/router"; import AuthLayout from "./layouts/AuthLayout"; import MainLayout from "./layouts/MainLayout"; @@ -54,13 +55,7 @@ const profileRoutes: RouteObject[] = [ { path: ROUTES.PROFILE.ROOT, Component: ProfilePage, - loader: async () => { - const user = await getUser("42859259-b879-408c-8edd-bbaa3a79c674"); - - if (user.status === 200) { - return user.data.item; - } - }, + loader: profileLoader, }, { path: ROUTES.PROFILE.REGISTER, diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index 39be835..4275c3c 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -1,50 +1,25 @@ -import { useEffect, useState } from "react"; - -import { useLoaderData, useNavigate, useSearchParams } from "react-router-dom"; +import { useLoaderData, useNavigate } from "react-router-dom"; import ProfileCard from "./components/ProfileCard"; import UserApplicationTable from "./components/UserApplicationTable"; +import useUserApplications from "./hooks/useUserApplications"; -import { getUserApplications } from "@/apis/services/applicationService"; import EmptyStateCard from "@/components/EmptyStateCard"; import { ROUTES } from "@/constants/router"; import { UserApplicationList } from "@/types/application"; import { UserItem } from "@/types/user"; -const OFFSET = 5; const LIMIT = 5; const PAGE_LIMIT = 7; export default function ProfilePage() { + const { userInfo } = useLoaderData<{ + userInfo: UserItem; + count: number; + userApplications: UserApplicationList[]; + }>(); const navigate = useNavigate(); - const userInfo = useLoaderData(); - - 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]); + const { isLoading, totalCount, userApplications } = useUserApplications(); return ( <> 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; From 2ec6e77473f831136c987f23f03dc389c93d9c2e Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Wed, 30 Apr 2025 17:59:56 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20UseApplicationTableSkeleton=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84,=20ta?= =?UTF-8?q?ilwind=20=EC=8A=A4=EC=BC=88=EB=A0=88=ED=86=A4=20=EC=95=A0?= =?UTF-8?q?=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=9C=A0=ED=8B=B8?= =?UTF-8?q?=EB=A6=AC=ED=8B=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ProfilePage/ProfilePage.tsx | 3 +- .../UserApplicationTableSkeleton.tsx | 10 +++++++ src/styles/utilities.css | 28 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/pages/ProfilePage/components/UserApplicationTableSkeleton.tsx diff --git a/src/pages/ProfilePage/ProfilePage.tsx b/src/pages/ProfilePage/ProfilePage.tsx index 4275c3c..3c91d1b 100644 --- a/src/pages/ProfilePage/ProfilePage.tsx +++ b/src/pages/ProfilePage/ProfilePage.tsx @@ -2,6 +2,7 @@ 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"; @@ -50,7 +51,7 @@ export default function ProfilePage() {

신청 내역

- {isLoading &&
로딩 중...
} + {isLoading && } {!isLoading && userApplications.length === 0 && ( +
+
+
+ ); +} + +export default UserApplicationTableSkeleton; 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); + } }