diff --git a/src/Router.tsx b/src/Router.tsx index 5500ef4..834bb93 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -2,6 +2,8 @@ import { lazy } from "react"; import { createBrowserRouter, RouteObject } from "react-router-dom"; +import ShopInfoPostCardSkeleton from "./pages/NoticeEmployeePage/components/ShopInfoPostCardSkeleton"; +import noticeEmployeeLoader from "./pages/NoticeEmployeePage/loader/noticeEmployeeLoader"; import profileLoader from "./pages/ProfilePage/loader/profileLoader"; import { ROUTES } from "./constants/router"; @@ -23,7 +25,9 @@ const NoticeListPage = lazy(() => import("@/pages/NoticeListPage")); const NoticeRegisterPage = lazy(() => import("@/pages/NoticeRegisterPage")); const NoticeEditPage = lazy(() => import("@/pages/NoticeEditPage")); const NoticeEmployerPage = lazy(() => import("@/pages/NoticeEmployerPage")); -const NoticeEmployeePage = lazy(() => import("@/pages/NoticeEmployeePage")); +const NoticeEmployeePage = lazy( + () => import("@/pages/NoticeEmployeePage/NoticeEmployeePage"), +); const authRoutes: RouteObject[] = [ { @@ -88,6 +92,8 @@ const noticeRoutes: RouteObject[] = [ { path: ROUTES.NOTICE.NOTICE_ID.EMPLOYEE, Component: NoticeEmployeePage, + loader: noticeEmployeeLoader, + hydrateFallbackElement: , }, ]; diff --git a/src/components/Post/Post.tsx b/src/components/Post/Post.tsx index 8567a40..698ea43 100644 --- a/src/components/Post/Post.tsx +++ b/src/components/Post/Post.tsx @@ -1,8 +1,9 @@ -import { cn } from "@/utils/cn"; import { Link } from "react-router-dom"; + +import { Location, Time, ArrowUp, ArrowUpBold } from "@/assets/icon"; +import { cn } from "@/utils/cn"; import { formatTimeRange, isPastDate } from "@/utils/datetime"; import { getPayRateText } from "@/utils/payRate"; -import { Location, Time, ArrowUp, ArrowUpBold } from "@/assets/icon"; interface PostProps { name: string; diff --git a/src/components/Post/PostCard.tsx b/src/components/Post/PostCard.tsx index 0084fb3..960dc61 100644 --- a/src/components/Post/PostCard.tsx +++ b/src/components/Post/PostCard.tsx @@ -1,7 +1,7 @@ +import { Location, Time, ArrowUp } from "@/assets/icon"; import { cn } from "@/utils/cn"; -import { formatTimeRange } from "@/utils/datetime"; +import { formatTimeRange, isPastDate } from "@/utils/datetime"; import { getPayRateText } from "@/utils/payRate"; -import { Location, Time, ArrowUp } from "@/assets/icon"; interface PostCardProps { name: string; @@ -15,6 +15,7 @@ interface PostCardProps { isShopInfo?: boolean; backgroundColor?: string; buttons?: React.ReactNode; + closed?: boolean; } export default function PostCard({ @@ -29,8 +30,14 @@ export default function PostCard({ isShopInfo = false, backgroundColor = "#ffffff", buttons = null, + closed, }: PostCardProps) { const { rateText } = getPayRateText(hourlyPay, originalHourlyPay); + const isPast = isPastDate( + startsAt ?? Date.now().toLocaleString(), + workhour ?? 0, + ); + const isDimmed = closed || isPast; const timeRange = startsAt && workhour !== undefined @@ -45,30 +52,42 @@ export default function PostCard({ backgroundColor === "#ffffff" && "border border-gray-20 shadow-sm", )} > -
+
{name} + {isDimmed && ( +
+

+ {!closed && isPast && "지난 공고"} + {closed && !isPast && "마감 완료"} +

+
+ )}
{!isShopInfo ? ( <>
-

시급

+

+ 시급 +

{hourlyPay?.toLocaleString()}원

- - {rateText} - - + {!closed && ( + + {rateText} + + + )}
-
+
@@ -77,11 +96,11 @@ export default function PostCard({

식당

)} {isShopInfo &&

{name}

} -
+
{address1}
-

+

{description}

diff --git a/src/constants/router.ts b/src/constants/router.ts index cadfff7..2446bc3 100644 --- a/src/constants/router.ts +++ b/src/constants/router.ts @@ -18,8 +18,8 @@ const ROUTES = { REGISTER: "/notice/register", EDIT: "/notice/edit", NOTICE_ID: { - EMPLOYER: `/notice/:noticeId/employer`, - EMPLOYEE: `/notice/:noticeId/employee`, + EMPLOYER: `/notice/:shopId/:noticeId/employer`, + EMPLOYEE: `/notice/:shopId/:noticeId/employee`, }, }, } as const; diff --git a/src/pages/NoticeEmployeePage.tsx b/src/pages/NoticeEmployeePage.tsx deleted file mode 100644 index cd723f4..0000000 --- a/src/pages/NoticeEmployeePage.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function NoticeEmployeePage() { - return
NoticeEmployeePage
; -} diff --git a/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx new file mode 100644 index 0000000..0231b57 --- /dev/null +++ b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx @@ -0,0 +1,34 @@ +import { useLoaderData, useParams } from "react-router-dom"; + +import NoticeDetailInfo from "./components/NoticeDetailInfo"; + +import PostList from "@/components/Post/PostList"; + +export default function NoticeEmployeePage() { + const { noticeInfo, recentNotices } = useLoaderData(); + const { shopId, noticeId } = useParams() as { + shopId: string; + noticeId: string; + }; + + return ( + <> +
+
+ +
+
+ +
+
+

최근에 본 공고

+ +
+
+ + ); +} diff --git a/src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx b/src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx new file mode 100644 index 0000000..b45c0be --- /dev/null +++ b/src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx @@ -0,0 +1,131 @@ +import { + postApplication, + putApplication, +} from "@/apis/services/applicationService"; +import Button from "@/components/Button"; +import PostCard from "@/components/Post/PostCard"; +import { useToast } from "@/hooks/useToast"; +import { useModalStore } from "@/store/useModalStore"; +import { NoticeItem } from "@/types/notice"; +import { cn } from "@/utils/cn"; +import { isPastDate } from "@/utils/datetime"; + +interface NoticeDetailInfoProps { + noticeInfo: NoticeItem; + shopId: string; + noticeId: string; +} + +function NoticeDetailInfo({ + shopId, + noticeId, + noticeInfo, +}: NoticeDetailInfoProps) { + const { openModal } = useModalStore(); + const { showToast } = useToast(); + + const { + hourlyPay, + startsAt, + workhour, + closed, + description, + currentUserApplication, + } = noticeInfo; + + const { + name, + imageUrl, + address1, + originalHourlyPay, + description: shopDescription, + } = noticeInfo.shop!.item; + + const applicationId = currentUserApplication?.item.id; + const applicationStatus = currentUserApplication?.item.status; + const isPast = isPastDate(startsAt, workhour); + const isDisabledNotice = isPast || closed || applicationStatus === "canceled"; + + const applyNotice = async () => { + const result = await postApplication(shopId, noticeId); + + if (result.status === 201) { + showToast("신청 완료!"); + } + }; + + const cancelApplication = () => { + openModal({ + type: "confirm", + confirmText: "취소하기", + cancelText: "아니오", + iconType: "warning", + message: "신청을 취소하시겠어요?", + onConfirm: async () => { + const result = await putApplication( + shopId, + noticeId, + applicationId ?? "", + "canceled", + ); + + if (result.status === 200) { + showToast("취소가 완료 되었습니다."); + } + }, + }); + }; + + return ( + <> +
+ + 식당 + +

{name}

+
+ {noticeInfo && ( + + {applicationStatus === "pending" && "취소하기"} + {applicationStatus === "accepted" && "승낙"} + {applicationStatus === "rejected" && "지원 거절"} + {applicationStatus === "canceled" && + "이미 취소한 지원 공고 입니다."} + {!applicationStatus && isDisabledNotice && "신청 불가"} + {!applicationStatus && !closed && !isPast && "지원하기"} + + } + /> + )} +
+ 공고 설명 +

{description}

+
+ + ); +} + +export default NoticeDetailInfo; diff --git a/src/pages/NoticeEmployeePage/components/ShopInfoPostCardSkeleton.tsx b/src/pages/NoticeEmployeePage/components/ShopInfoPostCardSkeleton.tsx new file mode 100644 index 0000000..b1d66ed --- /dev/null +++ b/src/pages/NoticeEmployeePage/components/ShopInfoPostCardSkeleton.tsx @@ -0,0 +1,52 @@ +import { Location, Time } from "@/assets/icon"; + +function ShopInfoPostCardSkeleton() { + return ( +
+
+
+ + 식당 + +

+

+
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ +
+ 공고 설명 +

+

+
+
+ ); +} + +export default ShopInfoPostCardSkeleton; diff --git a/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts b/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts new file mode 100644 index 0000000..a25318f --- /dev/null +++ b/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts @@ -0,0 +1,53 @@ +import { LoaderFunction } from "react-router-dom"; + +import { getNotice, getNotices } from "@/apis/services/noticeService"; + +interface LoadNoticeParams { + shopId: string; + noticeId: string; +} + +const loadNotice = async ({ shopId, noticeId }: LoadNoticeParams) => { + const noticeResult = await getNotice(shopId, noticeId); + + if (noticeResult.status === 200) { + return noticeResult.data.item; + } +}; + +const loadRecentNotices = async () => { + const recentNoticesResult = await getNotices({ sort: "hour", limit: 6 }); + + if (recentNoticesResult.status === 200) { + const recentNotices = recentNoticesResult.data.items.map(({ item }) => ({ + id: item.id, + name: item.shop?.item.name ?? "", + imageUrl: item.shop?.item.imageUrl ?? "", + address1: item.shop?.item.address1 ?? "", + originalHourlyPay: item.shop?.item.originalHourlyPay ?? 0, + link: `/notice/${item.shop?.item.id}/${item.id}/employee`, + hourlyPay: item.hourlyPay, + startsAt: item.startsAt, + workhour: item.workhour, + closed: item.closed, + })); + + return recentNotices; + } +}; + +const noticeEmployeeLoader: LoaderFunction = async ({ params }) => { + const { shopId, noticeId } = params as { + shopId: string; + noticeId: string; + }; + + const [noticeInfo, recentNotices] = await Promise.all([ + loadNotice({ shopId, noticeId }), + loadRecentNotices(), + ]); + + return { noticeInfo, recentNotices }; +}; + +export default noticeEmployeeLoader;