Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/Router.tsx
Original file line number Diff line number Diff line change
@@ -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"));
Expand Down Expand Up @@ -51,6 +55,7 @@ const profileRoutes: RouteObject[] = [
{
path: ROUTES.PROFILE.ROOT,
Component: ProfilePage,
loader: profileLoader,
},
{
path: ROUTES.PROFILE.REGISTER,
Expand Down Expand Up @@ -93,11 +98,11 @@ const appRoutes: RouteObject[] = [

export const router = createBrowserRouter([
{
Component: Outlet,
Component: AuthLayout,
children: authRoutes,
},
{
Component: Outlet,
Component: MainLayout,
children: appRoutes,
},
]);
2 changes: 1 addition & 1 deletion src/components/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function Table<T>({
...props
}: TableProps<T>) {
return (
<div className="rounded-lg border-[1px] border-gray-200 text-black overflow-hidden">
<div className="bg-white rounded-lg border-[1px] border-gray-200 text-black overflow-hidden">
{/* ───── 스크롤이 필요한 영역 ───── */}
<div className="overflow-x-auto">
<table
Expand Down
26 changes: 12 additions & 14 deletions src/layouts/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,33 @@ import { Outlet } from "react-router-dom";
import Footer from "./Footer";
import Header from "./Header";
interface MainLayoutProps {
isLoggedIn: boolean;
isLoggedIn?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MainLayout에서는 isLoggedIn을 필수화하는 게 안전한 방식이라고 생각했는데 옵셔널로 바꾸신 이유가 궁금합니다 🧐

Copy link
Collaborator Author

@cozy-ito cozy-ito May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 optional로 변경을 한 것은 임시로 변경한 것입니다
optional로 변경하지 않으면, Router.tsx에서 타입 에러가 발생해서요 😅
나중에 로그인 상태는 결국 전역상태로 관리될 수 밖에 없다고 생각해서 MainLayout props들은 결국 없어질 것이라 예상합니다.

userNavLabel?: "내 가게" | "내 프로필";
hasAlarm?: boolean;
onLogout?: () => void;
onToggleAlarm?: () => void;
}

export default function MainLayout({
isLoggedIn,
isLoggedIn = false,
userNavLabel,
hasAlarm,
onLogout,
onToggleAlarm,
}: MainLayoutProps) {
return (
<div className="w-full min-h-screen flex flex-col">
<div className="w-full max-w-[90rem] mx-auto flex-1 px-4 tablet:px-10 pc:px-20">
<Header
isLoggedIn={isLoggedIn}
userNavLabel={userNavLabel}
hasAlarm={hasAlarm}
onLogout={onLogout}
onToggleAlarm={onToggleAlarm}
/>
<Header
isLoggedIn={isLoggedIn}
userNavLabel={userNavLabel}
hasAlarm={hasAlarm}
onLogout={onLogout}
onToggleAlarm={onToggleAlarm}
/>

<main className="flex-1">
<Outlet />
</main>
</div>
<main className="flex flex-col flex-1">
<Outlet />
</main>

<Footer />
</div>
Expand Down
76 changes: 75 additions & 1 deletion src/pages/ProfilePage/ProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>ProfilePage</div>;
const { userInfo } = useLoaderData<{
userInfo: UserItem;
count: number;
userApplications: UserApplicationList[];
}>();
const navigate = useNavigate();
const { isLoading, totalCount, userApplications } = useUserApplications();

return (
<>
<section>
<div className="xl:w-[60.25rem] mx-auto px-6 py-[3.75rem]">
<div className="flex lg:flex-row flex-col lg:gap-[11.25rem] gap-6 w-full mb-6">
<h2 className="text-[1.75rem] font-bold">내 프로필</h2>
{userInfo && (
<ProfileCard
{...userInfo}
className="flex-1"
onClick={() => navigate(ROUTES.PROFILE.EDIT)}
/>
)}
</div>
{!userInfo && (
<EmptyStateCard
description="내 프로필을 등록하고 원하는 가게에 지원해 보세요"
buttonName="내 프로필 등록하기"
onClick={() => navigate(ROUTES.PROFILE.REGISTER)}
/>
)}
</div>
</section>

{userInfo && (
<section className="flex-1 bg-gray-5">
<div className="xl:w-[60.25rem] mx-auto px-6 pt-[3.75rem]">
<h3 className="mb-8 text-[1.75rem] font-bold">신청 내역</h3>

{isLoading && <UserApplicationTableSkeleton />}

{!isLoading && userApplications.length === 0 && (
<EmptyStateCard
description="아직 신청 내역이 없어요"
buttonName="공고 보러 가기"
onClick={() => navigate(ROUTES.NOTICE.ROOT)}
/>
)}

{!isLoading && userApplications.length > 0 && (
<UserApplicationTable
data={userApplications}
pageCount={totalCount}
itemCountPerPage={LIMIT}
pageLimit={PAGE_LIMIT}
/>
)}
</div>
</section>
)}
</>
);
}
18 changes: 16 additions & 2 deletions src/pages/ProfilePage/components/ProfileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="p-5 md:p-8 bg-red-10 rounded-xl text-black">
<div
className={cn(
"flex-1 p-5 md:p-8 bg-red-10 rounded-xl text-black",
className,
)}
>
<div className="flex mb-2 md:mb-3">
<div className="flex-1">
<span className="text-sm md:text-[1rem] inline-block font-bold leading-5 text-red-40">
Expand Down
4 changes: 2 additions & 2 deletions src/pages/ProfilePage/components/UserApplicationTable.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
10 changes: 10 additions & 0 deletions src/pages/ProfilePage/components/UserApplicationTableSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function UserApplicationTableSkeleton() {
return (
<div className="w-full">
<div className="md:h-[3.125rem] h-[2.5rem] mb-3 animate-skeleton rounded-lg" />
<div className="md:h-[24.875rem] h-[19.625rem] w-full animate-skeleton rounded-lg" />
</div>
);
}

export default UserApplicationTableSkeleton;
48 changes: 48 additions & 0 deletions src/pages/ProfilePage/hooks/useUserApplications.ts
Original file line number Diff line number Diff line change
@@ -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<number>(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;
13 changes: 13 additions & 0 deletions src/pages/ProfilePage/loader/profileLoader.ts
Original file line number Diff line number Diff line change
@@ -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;
28 changes: 28 additions & 0 deletions src/styles/utilities.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}