diff --git a/src/Router.tsx b/src/Router.tsx index ff4eedd..a82666c 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -2,10 +2,11 @@ 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 noticeEmployerLoader from "./pages/NoticeEmployerPage/loader/noticeEmployerLoader"; import profileLoader from "./pages/ProfilePage/loader/profileLoader"; +import NoticeDetailSkeleton from "./components/NoticeDetailSkeleton"; import { ROUTES } from "./constants/router"; import AuthLayout from "./layouts/AuthLayout"; import MainLayout from "./layouts/MainLayout"; @@ -24,7 +25,9 @@ const ShopEditPage = lazy(() => import("@/pages/ShopEditPage")); 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 NoticeEmployerPage = lazy( + () => import("@/pages/NoticeEmployerPage/NoticeEmployerPage"), +); const NoticeEmployeePage = lazy( () => import("@/pages/NoticeEmployeePage/NoticeEmployeePage"), ); @@ -93,12 +96,14 @@ const noticeRoutes: RouteObject[] = [ { path: ROUTES.NOTICE.NOTICE_ID.EMPLOYER, Component: NoticeEmployerPage, + loader: noticeEmployerLoader, + hydrateFallbackElement: , }, { path: ROUTES.NOTICE.NOTICE_ID.EMPLOYEE, Component: NoticeEmployeePage, loader: noticeEmployeeLoader, - hydrateFallbackElement: , + hydrateFallbackElement: , }, ]; diff --git a/src/apis/loaders/notice.ts b/src/apis/loaders/notice.ts new file mode 100644 index 0000000..c09b4ab --- /dev/null +++ b/src/apis/loaders/notice.ts @@ -0,0 +1,56 @@ +import { getShopApplications } from "../services/applicationService"; +import { getNotice } from "../services/noticeService"; + +import { PostData } from "@/components/Post/PostList"; +import { getLocalStorageValue } from "@/utils/localStorage"; + +interface LoadNoticeParams { + shopId: string; + noticeId: string; +} + +export const loadNotice = async ({ shopId, noticeId }: LoadNoticeParams) => { + const noticeResult = await getNotice(shopId, noticeId); + + if (noticeResult.status === 200) { + return noticeResult.data.item; + } +}; + +const MAX_VISIBLE_RECENT_NOTICES = 6; + +export const loadRecentNotices = (noticeId: string) => { + const allRecentNotices = + getLocalStorageValue("recentNotices") ?? []; + + const recentNotices = allRecentNotices + .filter(({ id }) => id !== noticeId) + .slice(0, MAX_VISIBLE_RECENT_NOTICES); + + return recentNotices; +}; + +interface LoadNoticeApplicationsParams { + shopId: string; + noticeId: string; + offset?: number; + limit?: number; +} + +export const loadNoticeApplications = async ({ + shopId, + noticeId, + offset, + limit, +}: LoadNoticeApplicationsParams) => { + const noticeApplicationsResult = await getShopApplications( + shopId, + noticeId, + offset, + limit, + ); + + if (noticeApplicationsResult.status === 200) { + return noticeApplicationsResult.data; + } +}; diff --git a/src/components/NoticeDetailInfo/NoticeDetailInfo.tsx b/src/components/NoticeDetailInfo/NoticeDetailInfo.tsx new file mode 100644 index 0000000..f6a5ae8 --- /dev/null +++ b/src/components/NoticeDetailInfo/NoticeDetailInfo.tsx @@ -0,0 +1,97 @@ +import NoticeEmployeeActionButton from "./NoticeEmployeeActionButton"; +import NoticeEmployerActionButton from "./NoticeEmployerActionButton"; + +import PostCard from "@/components/Post/PostCard"; +import { APPLICATION_STATUS } from "@/constants/applicationStatus"; +import { User } from "@/hooks/useUserStore"; +import { NoticeItem } from "@/types/notice"; +import { isPastDate } from "@/utils/datetime"; + +interface NoticeDetailInfoCardProps { + noticeInfo: NoticeItem; + shopId: string; + noticeId: string; + user?: User | null; + isEmployerPage?: boolean; +} + +function NoticeDetailInfoCard({ + shopId, + noticeId, + noticeInfo, + user, + isEmployerPage, +}: NoticeDetailInfoCardProps) { + 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 === APPLICATION_STATUS.CANCELED || + applicationStatus === APPLICATION_STATUS.REJECTED; + + return ( + <> +
+ + 식당 + +

{name}

+
+ {noticeInfo && ( + + ) : ( + + ) + } + /> + )} +
+ 공고 설명 +

{description}

+
+ + ); +} + +export default NoticeDetailInfoCard; diff --git a/src/components/NoticeDetailInfo/NoticeEmployeeActionButton.tsx b/src/components/NoticeDetailInfo/NoticeEmployeeActionButton.tsx new file mode 100644 index 0000000..7e2ec28 --- /dev/null +++ b/src/components/NoticeDetailInfo/NoticeEmployeeActionButton.tsx @@ -0,0 +1,102 @@ +import { useRevalidator } from "react-router-dom"; + +import Button from "../Button"; + +import { + postApplication, + putApplication, +} from "@/apis/services/applicationService"; +import { APPLICATION_STATUS } from "@/constants/applicationStatus"; +import { useToast } from "@/hooks/useToast"; +import { useModalStore } from "@/store/useModalStore"; +import { ApplicationStatus } from "@/types/application"; +import { cn } from "@/utils/cn"; + +interface NoticeEmployeeActionButtonProps { + shopId: string; + noticeId: string; + applicationId: string; + applicationStatus?: ApplicationStatus; + isDisabledNotice: boolean; +} + +function NoticeEmployeeActionButton({ + shopId, + noticeId, + applicationId, + applicationStatus, + isDisabledNotice, +}: NoticeEmployeeActionButtonProps) { + const { revalidate } = useRevalidator(); + const { openModal } = useModalStore(); + const { showToast } = useToast(); + + const getApplicationButtonLabel = () => { + switch (applicationStatus) { + case APPLICATION_STATUS.PENDING: + return "취소하기"; + case APPLICATION_STATUS.ACCEPTED: + return "승낙"; + case APPLICATION_STATUS.REJECTED: + return "거절된 공고입니다."; + case APPLICATION_STATUS.CANCELED: + return "이미 취소하신 공고 입니다."; + default: + return isDisabledNotice ? "신청 불가" : "지원하기"; + } + }; + + const applyNotice = async () => { + const result = await postApplication(shopId, noticeId); + + if (result.status === 201) { + revalidate(); + showToast("신청 완료!"); + } + }; + + const cancelApplication = () => { + openModal({ + type: "confirm", + confirmText: "취소하기", + cancelText: "아니오", + iconType: "warning", + message: "신청을 취소하시겠어요?", + onConfirm: async () => { + const result = await putApplication( + shopId, + noticeId, + applicationId, + APPLICATION_STATUS.CANCELED, + ); + + if (result.status === 200) { + showToast("취소가 완료 되었습니다."); + revalidate(); + } + }, + }); + }; + + return ( + + ); +} + +export default NoticeEmployeeActionButton; diff --git a/src/components/NoticeDetailInfo/NoticeEmployerActionButton.tsx b/src/components/NoticeDetailInfo/NoticeEmployerActionButton.tsx new file mode 100644 index 0000000..0f74f44 --- /dev/null +++ b/src/components/NoticeDetailInfo/NoticeEmployerActionButton.tsx @@ -0,0 +1,35 @@ +import { useNavigate } from "react-router-dom"; + +import Button from "../Button"; + +interface NoticeEmployerActionButtonProps { + userShopId?: string; + noticeShopId: string; + noticeId: string; +} + +function NoticeEmployerActionButton({ + userShopId, + noticeShopId, + noticeId, +}: NoticeEmployerActionButtonProps) { + const navigate = useNavigate(); + + const moveToEditNoticePage = () => { + navigate(`/notice/edit/${noticeId}`); + }; + + return ( + + ); +} + +export default NoticeEmployerActionButton; diff --git a/src/pages/NoticeEmployeePage/components/ShopInfoPostCardSkeleton.tsx b/src/components/NoticeDetailSkeleton.tsx similarity index 96% rename from src/pages/NoticeEmployeePage/components/ShopInfoPostCardSkeleton.tsx rename to src/components/NoticeDetailSkeleton.tsx index b1d66ed..ad53db7 100644 --- a/src/pages/NoticeEmployeePage/components/ShopInfoPostCardSkeleton.tsx +++ b/src/components/NoticeDetailSkeleton.tsx @@ -1,6 +1,6 @@ import { Location, Time } from "@/assets/icon"; -function ShopInfoPostCardSkeleton() { +function NoticeDetailSkeleton() { return (
@@ -49,4 +49,4 @@ function ShopInfoPostCardSkeleton() { ); } -export default ShopInfoPostCardSkeleton; +export default NoticeDetailSkeleton; diff --git a/src/components/Post/PostCard.tsx b/src/components/Post/PostCard.tsx index 960dc61..d73a85f 100644 --- a/src/components/Post/PostCard.tsx +++ b/src/components/Post/PostCard.tsx @@ -59,7 +59,7 @@ export default function PostCard({ className="w-full h-[180px] object-cover md:h-[360px] lg:h-[308px]" /> {isDimmed && ( -
+

{!closed && isPast && "지난 공고"} {closed && !isPast && "마감 완료"} diff --git a/src/components/Post/PostList.tsx b/src/components/Post/PostList.tsx index cd6b31a..efda071 100644 --- a/src/components/Post/PostList.tsx +++ b/src/components/Post/PostList.tsx @@ -1,6 +1,6 @@ import Post from "./Post"; -interface PostData { +export interface PostData { id: string; name: string; imageUrl: string; diff --git a/src/constants/applicationStatus.ts b/src/constants/applicationStatus.ts new file mode 100644 index 0000000..6d30686 --- /dev/null +++ b/src/constants/applicationStatus.ts @@ -0,0 +1,6 @@ +export const APPLICATION_STATUS = { + PENDING: "pending", + ACCEPTED: "accepted", + REJECTED: "rejected", + CANCELED: "canceled", +} as const; diff --git a/src/constants/router.ts b/src/constants/router.ts index 2446bc3..9b83ffa 100644 --- a/src/constants/router.ts +++ b/src/constants/router.ts @@ -16,7 +16,7 @@ const ROUTES = { NOTICE: { ROOT: "/", REGISTER: "/notice/register", - EDIT: "/notice/edit", + EDIT: "/notice/edit/:noticeId", NOTICE_ID: { EMPLOYER: `/notice/:shopId/:noticeId/employer`, EMPLOYEE: `/notice/:shopId/:noticeId/employee`, diff --git a/src/hooks/useUpdateRecentNotices.ts b/src/hooks/useUpdateRecentNotices.ts new file mode 100644 index 0000000..24984cf --- /dev/null +++ b/src/hooks/useUpdateRecentNotices.ts @@ -0,0 +1,56 @@ +import { useEffect } from "react"; + +import { NoticeItem } from "../types"; + +import { PostData } from "@/components/Post/PostList"; +import { + getLocalStorageValue, + setLocalStorageValue, +} from "@/utils/localStorage"; + +const RECENT_NOTICES = "recentNotices"; + +const updateLocalStorageRecentNotices = (candidateNotice: PostData) => { + const storageValue: PostData[] = getLocalStorageValue(RECENT_NOTICES) ?? []; + + if (!storageValue.some(({ id }) => id === candidateNotice.id)) { + if (storageValue.length === 7) { + storageValue.shift(); + } + storageValue.push(candidateNotice); + } + setLocalStorageValue(RECENT_NOTICES, storageValue); +}; + +interface UseRecentNoticesParams { + noticeInfo: NoticeItem; + link: string; +} + +const useUpdateRecentNotices = ({ + noticeInfo, + link, +}: UseRecentNoticesParams) => { + useEffect(() => { + const { id, hourlyPay, startsAt, workhour, closed } = noticeInfo; + const { name, imageUrl, address1, originalHourlyPay } = + noticeInfo.shop!.item; + + const visitedNotice = { + id, + name, + imageUrl, + address1, + originalHourlyPay, + link, + hourlyPay, + startsAt, + workhour, + closed, + }; + + updateLocalStorageRecentNotices(visitedNotice); + }, [noticeInfo, link]); +}; + +export default useUpdateRecentNotices; diff --git a/src/hooks/useUserStore.tsx b/src/hooks/useUserStore.tsx index 12b0e87..3e976cc 100644 --- a/src/hooks/useUserStore.tsx +++ b/src/hooks/useUserStore.tsx @@ -1,7 +1,7 @@ import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; -type User = { +export type User = { id: string; email: string; type: "employer" | "employee"; diff --git a/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx index 0231b57..a684757 100644 --- a/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx +++ b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx @@ -1,16 +1,26 @@ import { useLoaderData, useParams } from "react-router-dom"; -import NoticeDetailInfo from "./components/NoticeDetailInfo"; +import NoticeDetailInfo from "../../components/NoticeDetailInfo/NoticeDetailInfo"; -import PostList from "@/components/Post/PostList"; +import PostList, { PostData } from "@/components/Post/PostList"; +import useUpdateRecentNotices from "@/hooks/useUpdateRecentNotices"; +import { NoticeItem } from "@/types/notice"; export default function NoticeEmployeePage() { - const { noticeInfo, recentNotices } = useLoaderData(); + const { noticeInfo, recentNotices } = useLoaderData<{ + noticeInfo: NoticeItem; + recentNotices: PostData[]; + }>(); const { shopId, noticeId } = useParams() as { shopId: string; noticeId: string; }; + useUpdateRecentNotices({ + noticeInfo, + link: `/notice/${shopId}/${noticeId}/employee`, + }); + return ( <>

@@ -26,7 +36,14 @@ export default function NoticeEmployeePage() {

최근에 본 공고

- + + {recentNotices.length > 0 ? ( + + ) : ( +
+ 최근에 본 공고가 없습니다. +
+ )}
diff --git a/src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx b/src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx deleted file mode 100644 index b45c0be..0000000 --- a/src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx +++ /dev/null @@ -1,131 +0,0 @@ -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/loader/noticeEmployeeLoader.ts b/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts index a25318f..3409d5e 100644 --- a/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts +++ b/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts @@ -1,40 +1,6 @@ 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; - } -}; +import { loadNotice, loadRecentNotices } from "@/apis/loaders/notice"; const noticeEmployeeLoader: LoaderFunction = async ({ params }) => { const { shopId, noticeId } = params as { @@ -42,10 +8,8 @@ const noticeEmployeeLoader: LoaderFunction = async ({ params }) => { noticeId: string; }; - const [noticeInfo, recentNotices] = await Promise.all([ - loadNotice({ shopId, noticeId }), - loadRecentNotices(), - ]); + const noticeInfo = await loadNotice({ shopId, noticeId }); + const recentNotices = loadRecentNotices(noticeId); return { noticeInfo, recentNotices }; }; diff --git a/src/pages/NoticeEmployerPage.tsx b/src/pages/NoticeEmployerPage.tsx deleted file mode 100644 index 8c94a7c..0000000 --- a/src/pages/NoticeEmployerPage.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function NoticeEmployerPage() { - return
NoticeEmployerPage
; -} diff --git a/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx new file mode 100644 index 0000000..5950205 --- /dev/null +++ b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx @@ -0,0 +1,67 @@ +import { useLoaderData, useParams } from "react-router-dom"; + +import NoticeDetailInfo from "../../components/NoticeDetailInfo/NoticeDetailInfo"; + +import NoticeApplicationTableContainer from "./components/NoticeApplicationTableContainer"; + +import PostList, { PostData } from "@/components/Post/PostList"; +import useUpdateRecentNotices from "@/hooks/useUpdateRecentNotices"; +import { useUserStore } from "@/hooks/useUserStore"; +import { NoticeItem } from "@/types/notice"; + +export default function NoticeEmployerPage() { + const { noticeInfo, recentNotices } = useLoaderData<{ + noticeInfo: NoticeItem; + recentNotices: PostData[]; + }>(); + const { shopId, noticeId } = useParams() as { + shopId: string; + noticeId: string; + }; + const { user } = useUserStore(); + + const isMyShop = user?.shopId === shopId; + + useUpdateRecentNotices({ + noticeInfo, + link: `/notice/${shopId}/${noticeId}/employer`, + }); + + return ( + <> +
+
+ +
+
+ +
+
+

+ {isMyShop ? "신청자 목록" : "최근에 본 공고"} +

+ {isMyShop && ( + + )} + + {!isMyShop && recentNotices.length > 0 ? ( + + ) : ( +
+ 최근에 본 공고가 없습니다. +
+ )} +
+
+ + ); +} diff --git a/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx b/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx new file mode 100644 index 0000000..b84d82e --- /dev/null +++ b/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx @@ -0,0 +1,152 @@ +import { putApplication } from "@/apis/services/applicationService"; +import Button from "@/components/Button"; +import Pagination from "@/components/Pagination"; +import StatusBadge from "@/components/StatusBadge"; +import Table from "@/components/Table"; +import { useToast } from "@/hooks/useToast"; +import { useModalStore } from "@/store/useModalStore"; +import { ApplicationItem, ApplicationStatus } from "@/types/application"; + +const applicationStatusMessageMap: { + [key in Exclude]: { + inquiry: string; + success: string; + iconType: "check" | "warning"; + }; +} = { + accepted: { + inquiry: "신청을 승인하시겠어요?", + success: "승인 완료!", + iconType: "check", + }, + rejected: { + inquiry: "신청을 거절하시겠어요?", + success: "거절했어요.", + iconType: "warning", + }, +}; + +interface NoticeApplicationChangeStatusParams { + shopId: string; + noticeId: string; + applicationId: string; + status: Exclude; +} + +interface NoticeApplicationTableProps { + data: ApplicationItem[]; + totalCount: number; + itemCountPerPage?: number; + pageLimit?: number; + refetch: () => void; +} + +function NoticeApplicationTable({ + data, + totalCount, + itemCountPerPage = 5, + pageLimit = 5, + refetch, +}: NoticeApplicationTableProps) { + const { showToast } = useToast(); + const { openModal } = useModalStore(); + + const changeApplicationStatus = async ({ + shopId, + noticeId, + applicationId, + status, + }: NoticeApplicationChangeStatusParams) => { + const { iconType, inquiry, success } = applicationStatusMessageMap[status]; + + openModal({ + type: "confirm", + iconType: iconType, + message: inquiry, + onConfirm: async () => { + const changeApplicationResult = await putApplication( + shopId, + noticeId, + applicationId, + status, + ); + + if (changeApplicationResult.status === 200) { + showToast(success); + refetch(); + } + }, + }); + }; + return ( + ( + + 신청자 + 소개 + + 전화번호 + + 상태 + + )} + bodyRow={({ id, status, user, shop, notice }) => ( + + {user.item.name} + {user.item.bio} + {user.item.phone} + + {status === "pending" ? ( +
+ + +
+ ) : ( + + )} +
+
+ )} + footerRow={() => ( + + + + + + )} + /> + ); +} + +export default NoticeApplicationTable; diff --git a/src/pages/NoticeEmployerPage/components/NoticeApplicationTableContainer.tsx b/src/pages/NoticeEmployerPage/components/NoticeApplicationTableContainer.tsx new file mode 100644 index 0000000..4b3a14f --- /dev/null +++ b/src/pages/NoticeEmployerPage/components/NoticeApplicationTableContainer.tsx @@ -0,0 +1,32 @@ +import useShopApplications from "../hooks/useShopApplications"; + +import NoticeApplicationTable from "./NoticeApplicationTable"; + +interface NoticeApplicationTableContainerProps { + shopId: string; + noticeId: string; +} + +function NoticeApplicationTableContainer({ + shopId, + noticeId, +}: NoticeApplicationTableContainerProps) { + const { + refetch: refetchShopApplications, + shopApplications, + totalCount, + } = useShopApplications({ + shopId, + noticeId, + }); + + return ( + + ); +} + +export default NoticeApplicationTableContainer; diff --git a/src/pages/NoticeEmployerPage/hooks/useShopApplications.ts b/src/pages/NoticeEmployerPage/hooks/useShopApplications.ts new file mode 100644 index 0000000..2bde58c --- /dev/null +++ b/src/pages/NoticeEmployerPage/hooks/useShopApplications.ts @@ -0,0 +1,59 @@ +import { useEffect, useState } from "react"; + +import { useSearchParams } from "react-router-dom"; + +import { getShopApplications } from "@/apis/services/applicationService"; +import { ApplicationItem } from "@/types/application"; + +interface UseShopApplicationsParams { + shopId: string; + noticeId: string; + offset?: number; + limit?: number; +} + +const useShopApplications = ({ + shopId, + noticeId, + offset = 5, + limit = 5, +}: UseShopApplicationsParams) => { + const [searchParams] = useSearchParams(); + const [isLoading, setIsLoading] = useState(false); + const [totalCount, setTotalCount] = useState(0); + const [shopApplications, setShopApplications] = useState( + [], + ); + const page = Number(searchParams.get("page")) || 1; + + const fetchShopApplication = async () => { + setIsLoading(true); + const fetchedShopApplications = await getShopApplications( + shopId, + noticeId, + (page - 1) * offset, + limit, + ); + + const nextShopApplications = fetchedShopApplications.data.items.map( + ({ item }) => item, + ); + + setTotalCount(fetchedShopApplications.data.count); + setShopApplications(nextShopApplications); + setIsLoading(false); + }; + + useEffect(() => { + fetchShopApplication(); + }, [shopId, noticeId, offset, limit, page]); + + return { + refetch: fetchShopApplication, + shopApplications, + isLoading, + totalCount, + }; +}; + +export default useShopApplications; diff --git a/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts new file mode 100644 index 0000000..ec8cb36 --- /dev/null +++ b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts @@ -0,0 +1,23 @@ +import { LoaderFunction } from "react-router-dom"; + +import { loadNotice, loadRecentNotices } from "@/apis/loaders/notice"; +import { useUserStore } from "@/hooks/useUserStore"; + +const noticeEmployerLoader: LoaderFunction = async ({ params }) => { + const user = useUserStore.getState().user; + const { shopId, noticeId } = params as { + shopId: string; + noticeId: string; + }; + + const noticeInfo = await loadNotice({ shopId, noticeId }); + + if (user?.shopId === shopId) { + return { noticeInfo }; + } + + const recentNotices = loadRecentNotices(noticeId); + return { noticeInfo, recentNotices }; +}; + +export default noticeEmployerLoader; diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts new file mode 100644 index 0000000..fbd5957 --- /dev/null +++ b/src/utils/localStorage.ts @@ -0,0 +1,21 @@ +export const getLocalStorageValue = (key: string): T | null => { + const storageValue = localStorage.getItem(key); + if (!storageValue) return null; + + try { + return JSON.parse(storageValue) as T; + } catch (e) { + console.error(`Error parsing localStorage key "${key}":`, e); + return null; + } +}; + +export const setLocalStorageValue = (key: string, value: unknown) => { + if (value === undefined) return; + + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (e) { + console.error(`Error setting localStorage key "${key}":`, e); + } +};