From f415606d5733bcbe4e346a92c737bc3fb69073e3 Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sat, 3 May 2025 16:27:06 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20NoticeDetailInfo=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=88=98=EC=A0=95=20(=EA=B3=B5?= =?UTF-8?q?=EA=B3=A0=20=EC=83=81=EC=84=B8=20=EC=82=AC=EC=9E=A5=EB=8B=98=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9A=A9=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Router.tsx | 11 +++- src/apis/loaders/notice.ts | 35 +++++++++++ .../components/NoticeDetailInfo.tsx | 62 ++++++++++++------- .../NoticeDetailSkeleton.tsx} | 4 +- .../NoticeEmployeePage/NoticeEmployeePage.tsx | 2 +- .../loader/noticeEmployeeLoader.ts | 36 +---------- src/pages/NoticeEmployerPage.tsx | 3 - .../NoticeEmployerPage/NoticeEmployerPage.tsx | 32 ++++++++++ .../loader/noticeEmployerLoader.ts | 16 +++++ 9 files changed, 136 insertions(+), 65 deletions(-) create mode 100644 src/apis/loaders/notice.ts rename src/{pages/NoticeEmployeePage => }/components/NoticeDetailInfo.tsx (62%) rename src/{pages/NoticeEmployeePage/components/ShopInfoPostCardSkeleton.tsx => components/NoticeDetailSkeleton.tsx} (96%) delete mode 100644 src/pages/NoticeEmployerPage.tsx create mode 100644 src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx create mode 100644 src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts diff --git a/src/Router.tsx b/src/Router.tsx index 834bb93..536a6f8 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"), ); @@ -88,12 +91,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..ddc1359 --- /dev/null +++ b/src/apis/loaders/notice.ts @@ -0,0 +1,35 @@ +import { getNotice, getNotices } from "../services/noticeService"; + +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; + } +}; + +export 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; + } +}; diff --git a/src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx b/src/components/NoticeDetailInfo.tsx similarity index 62% rename from src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx rename to src/components/NoticeDetailInfo.tsx index 84fd572..8e95389 100644 --- a/src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx +++ b/src/components/NoticeDetailInfo.tsx @@ -1,3 +1,5 @@ +import { useNavigate } from "react-router-dom"; + import { postApplication, putApplication, @@ -5,6 +7,7 @@ import { import Button from "@/components/Button"; import PostCard from "@/components/Post/PostCard"; import { NoticeItem } from "@/types/notice"; +import { UserType } from "@/types/user"; import { cn } from "@/utils/cn"; import { isPastDate } from "@/utils/datetime"; @@ -12,13 +15,17 @@ interface NoticeDetailInfoProps { noticeInfo: NoticeItem; shopId: string; noticeId: string; + type?: UserType; } function NoticeDetailInfo({ shopId, noticeId, noticeInfo, + type = "employee", }: NoticeDetailInfoProps) { + const navigate = useNavigate(); + const { hourlyPay, startsAt, @@ -64,6 +71,8 @@ function NoticeDetailInfo({ } }; + const moveToEditNoticePage = () => navigate("/notice/edit"); + return ( <>
@@ -84,27 +93,38 @@ function NoticeDetailInfo({ workhour={workhour} closed={closed} buttons={ - + type === "employee" ? ( + + ) : ( + + ) } /> )} 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/pages/NoticeEmployeePage/NoticeEmployeePage.tsx b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx index 0231b57..2d678c3 100644 --- a/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx +++ b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx @@ -1,6 +1,6 @@ import { useLoaderData, useParams } from "react-router-dom"; -import NoticeDetailInfo from "./components/NoticeDetailInfo"; +import NoticeDetailInfo from "../../components/NoticeDetailInfo"; import PostList from "@/components/Post/PostList"; diff --git a/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts b/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts index a25318f..eb71776 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 { 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..bfcd9ae --- /dev/null +++ b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx @@ -0,0 +1,32 @@ +import { useLoaderData, useParams } from "react-router-dom"; + +import NoticeDetailInfo from "../../components/NoticeDetailInfo"; + +export default function NoticeEmployerPage() { + const { noticeInfo } = useLoaderData(); + const { shopId, noticeId } = useParams() as { + shopId: string; + noticeId: string; + }; + + return ( + <> +
+
+ +
+
+ +
+
+

신청자 목록

+
+
+ + ); +} diff --git a/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts new file mode 100644 index 0000000..c2b1a46 --- /dev/null +++ b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts @@ -0,0 +1,16 @@ +import { LoaderFunction } from "react-router-dom"; + +import { loadNotice } from "@/apis/loaders/notice"; + +const noticeEmployerLoader: LoaderFunction = async ({ params }) => { + const { shopId, noticeId } = params as { + shopId: string; + noticeId: string; + }; + + const [noticeInfo] = await Promise.all([loadNotice({ shopId, noticeId })]); + + return { noticeInfo }; +}; + +export default noticeEmployerLoader; From ecbc45c344955497d6e2ac0444b69a399360232c Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sat, 3 May 2025 18:32:12 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20NoticeApplicationTable=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/NoticeApplicationTable.tsx | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx diff --git a/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx b/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx new file mode 100644 index 0000000..5950513 --- /dev/null +++ b/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx @@ -0,0 +1,128 @@ +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 { ApplicationItem, ApplicationStatus } from "@/types/application"; + +const changeApplicationStatusSuccessMessageMap: { + [key in Exclude]: string; +} = { + accepted: "승인이 완료되었습니다.", + rejected: "거절이 완료되었습니다.", +}; + +interface NoticeApplicationChangeStatusParams { + shopId: string; + noticeId: string; + applicationId: string; + status: Exclude; +} + +interface NoticeApplicationTableProps { + data: ApplicationItem[]; + totalCount: number; + pageLimit: number; + itemCountPerPage?: number; + refetch: () => void; +} + +function NoticeApplicationTable({ + data, + totalCount, + pageLimit, + itemCountPerPage = 5, + refetch, +}: NoticeApplicationTableProps) { + const changeApplicationStatus = async ({ + shopId, + noticeId, + applicationId, + status, + }: NoticeApplicationChangeStatusParams) => { + const changeApplicationResult = await putApplication( + shopId, + noticeId, + applicationId, + status, + ); + + if (changeApplicationResult.status === 200) { + // TODO: 모달 병합 후 모달로 적용 예정 + alert(changeApplicationStatusSuccessMessageMap[status]); + refetch(); + } + }; + + return ( + ( + + 신청자 + 소개 + + 전화번호 + + 상태 + + )} + bodyRow={({ id, status, user, shop, notice }) => ( + + {user.item.name} + {user.item.bio} + {user.item.phone} + + {status === "pending" ? ( +
+ + +
+ ) : ( + + )} +
+
+ )} + footerRow={() => ( + + + + + + )} + /> + ); +} + +export default NoticeApplicationTable; From 15084386da401387edcb56a117062c36d9802750 Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sat, 3 May 2025 18:32:45 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20useShopApplications=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useShopApplications.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/pages/NoticeEmployerPage/hooks/useShopApplications.ts diff --git a/src/pages/NoticeEmployerPage/hooks/useShopApplications.ts b/src/pages/NoticeEmployerPage/hooks/useShopApplications.ts new file mode 100644 index 0000000..67bb83c --- /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(); + }, [page]); + + return { + refetch: fetchShopApplication, + shopApplications, + isLoading, + totalCount, + }; +}; + +export default useShopApplications; From 353c7c7892b76cdc229549bae6124ca5f11fa8ec Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sat, 3 May 2025 18:33:42 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=EC=82=AC=EC=9E=A5=EB=8B=98=20?= =?UTF-8?q?=EC=95=84=EB=8B=88=EB=A9=B4=20=EC=95=8C=EB=B0=94=EC=83=9D=20?= =?UTF-8?q?=EA=B3=B5=EA=B3=A0=20=EC=83=81=EC=84=B8=EB=A1=9C=20redirect,=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=9A=94=EC=B2=AD=20=EC=84=B1=EA=B3=B5=20?= =?UTF-8?q?=EC=8B=9C,=20=EA=B8=B0=EC=A1=B4=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/loaders/notice.ts | 26 ++++++++++ src/components/NoticeDetailInfo.tsx | 50 +++++++++++-------- src/hooks/useUserStore.tsx | 2 +- .../NoticeEmployerPage/NoticeEmployerPage.tsx | 35 +++++++++++-- 4 files changed, 89 insertions(+), 24 deletions(-) diff --git a/src/apis/loaders/notice.ts b/src/apis/loaders/notice.ts index ddc1359..d2bdfca 100644 --- a/src/apis/loaders/notice.ts +++ b/src/apis/loaders/notice.ts @@ -1,3 +1,4 @@ +import { getShopApplications } from "../services/applicationService"; import { getNotice, getNotices } from "../services/noticeService"; interface LoadNoticeParams { @@ -33,3 +34,28 @@ export const loadRecentNotices = async () => { 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.tsx b/src/components/NoticeDetailInfo.tsx index 8e95389..9dd3435 100644 --- a/src/components/NoticeDetailInfo.tsx +++ b/src/components/NoticeDetailInfo.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router-dom"; +import { useNavigate, useRevalidator } from "react-router-dom"; import { postApplication, @@ -6,8 +6,8 @@ import { } from "@/apis/services/applicationService"; import Button from "@/components/Button"; import PostCard from "@/components/Post/PostCard"; +import { User } from "@/hooks/useUserStore"; import { NoticeItem } from "@/types/notice"; -import { UserType } from "@/types/user"; import { cn } from "@/utils/cn"; import { isPastDate } from "@/utils/datetime"; @@ -15,16 +15,17 @@ interface NoticeDetailInfoProps { noticeInfo: NoticeItem; shopId: string; noticeId: string; - type?: UserType; + user?: User | null; } function NoticeDetailInfo({ shopId, noticeId, noticeInfo, - type = "employee", + user = null, }: NoticeDetailInfoProps) { const navigate = useNavigate(); + const { revalidate } = useRevalidator(); const { hourlyPay, @@ -46,14 +47,19 @@ function NoticeDetailInfo({ const applicationId = currentUserApplication?.item.id; const applicationStatus = currentUserApplication?.item.status; const isPast = isPastDate(startsAt, workhour); - const isDisabledNotice = isPast || closed || applicationStatus === "canceled"; + const isDisabledNotice = + isPast || + closed || + applicationStatus === "canceled" || + applicationStatus === "rejected"; const applyNotice = async () => { const result = await postApplication(shopId, noticeId); if (result.status === 201) { - // Modal 병합 후 모달로 변경 예정 + // TODO: Modal 병합 후 모달로 변경 예정 alert("신청이 완료 되었습니다."); + revalidate(); } }; @@ -66,12 +72,16 @@ function NoticeDetailInfo({ ); if (result.status === 200) { - // Modal 병합 후 모달로 변경 예정 + // TODO: Modal 병합 후 모달로 변경 예정 alert("취소가 완료 되었습니다."); } }; - const moveToEditNoticePage = () => navigate("/notice/edit"); + const moveToEditNoticePage = () => { + if (user?.type === "employer") { + navigate(`/notice/edit/${user.shopId}`); + } + }; return ( <> @@ -93,7 +103,16 @@ function NoticeDetailInfo({ workhour={workhour} closed={closed} buttons={ - type === "employee" ? ( + user?.type === "employer" ? ( + + ) : ( - ) : ( - ) } /> diff --git a/src/hooks/useUserStore.tsx b/src/hooks/useUserStore.tsx index 598888c..6e21c9e 100644 --- a/src/hooks/useUserStore.tsx +++ b/src/hooks/useUserStore.tsx @@ -1,6 +1,6 @@ import { create } from "zustand"; -type User = { +export type User = { id: string; email: string; type: "employer" | "employee"; diff --git a/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx index bfcd9ae..d5ed19d 100644 --- a/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx +++ b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx @@ -1,23 +1,46 @@ -import { useLoaderData, useParams } from "react-router-dom"; +import { Navigate, useLoaderData, useParams } from "react-router-dom"; import NoticeDetailInfo from "../../components/NoticeDetailInfo"; +import NoticeApplicationTable from "./components/NoticeApplicationTable"; +import useShopApplications from "./hooks/useShopApplications"; + +import { useUserStore } from "@/hooks/useUserStore"; +import { NoticeItem } from "@/types/notice"; + +const PAGE_LIMIT = 7; + export default function NoticeEmployerPage() { - const { noticeInfo } = useLoaderData(); + const { noticeInfo } = useLoaderData<{ + noticeInfo: NoticeItem; + }>(); const { shopId, noticeId } = useParams() as { shopId: string; noticeId: string; }; + const { + refetch: refetchShopApplications, + shopApplications, + totalCount, + } = useShopApplications({ + shopId, + noticeId, + }); + const { user } = useUserStore(); + + if (user?.type !== "employer") { + return ; + } return ( <>
@@ -25,6 +48,12 @@ export default function NoticeEmployerPage() {

신청자 목록

+
From c4e6c81162c6c94d087de64d4b550b85181685df Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sat, 3 May 2025 18:38:55 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20ROUTE.NOTICE.EDIT=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(/notice/edit=20->=20/notice/edit/:noticeId)=20-=20?= =?UTF-8?q?=EC=88=98=EB=B9=88=EB=8B=98=20=EC=9A=94=EC=B2=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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`, From 40c4f86adaf31b6cbb22c5d152961f5bb73e3d4f Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sat, 3 May 2025 20:55:45 +0900 Subject: [PATCH 06/12] =?UTF-8?q?style:=20PostCard=20opacity=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Post/PostCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 && "마감 완료"} From d73476171be8b1dfc3ca94d347ee145a81ec2233 Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sat, 3 May 2025 22:31:15 +0900 Subject: [PATCH 07/12] =?UTF-8?q?feat:=20=EB=A6=AC=EB=8B=A4=EC=9D=B4?= =?UTF-8?q?=EB=A0=89=ED=8A=B8=20=EC=82=AD=EC=A0=9C,=20=EB=82=B4=20?= =?UTF-8?q?=EA=B0=80=EA=B2=8C=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EB=B3=80=EA=B2=BD,=20Modal,=20Toast=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/NoticeDetailInfo.tsx | 13 ++-- src/components/Post/PostList.tsx | 2 +- .../NoticeEmployerPage/NoticeEmployerPage.tsx | 42 ++++++------- .../components/NoticeApplicationTable.tsx | 60 +++++++++++++------ .../NoticeApplicationTableContainer.tsx | 32 ++++++++++ .../loader/noticeEmployerLoader.ts | 17 +++++- 6 files changed, 115 insertions(+), 51 deletions(-) create mode 100644 src/pages/NoticeEmployerPage/components/NoticeApplicationTableContainer.tsx diff --git a/src/components/NoticeDetailInfo.tsx b/src/components/NoticeDetailInfo.tsx index 5ab08dd..7fb0d08 100644 --- a/src/components/NoticeDetailInfo.tsx +++ b/src/components/NoticeDetailInfo.tsx @@ -18,13 +18,15 @@ interface NoticeDetailInfoProps { shopId: string; noticeId: string; user?: User | null; + isEmployerPage?: boolean; } function NoticeDetailInfo({ shopId, noticeId, noticeInfo, - user = null, + user, + isEmployerPage, }: NoticeDetailInfoProps) { const navigate = useNavigate(); const { revalidate } = useRevalidator(); @@ -48,7 +50,7 @@ function NoticeDetailInfo({ description: shopDescription, } = noticeInfo.shop!.item; - const applicationId = currentUserApplication?.item.id; + const applicationId = currentUserApplication?.item.id ?? ""; const applicationStatus = currentUserApplication?.item.status; const isPast = isPastDate(startsAt, workhour); const isDisabledNotice = @@ -77,7 +79,7 @@ function NoticeDetailInfo({ const result = await putApplication( shopId, noticeId, - applicationId ?? "", + applicationId, "canceled", ); @@ -90,7 +92,7 @@ function NoticeDetailInfo({ }; const moveToEditNoticePage = () => { - if (user?.type === "employer") { + if (user) { navigate(`/notice/edit/${user.shopId}`); } }; @@ -115,12 +117,13 @@ function NoticeDetailInfo({ workhour={workhour} closed={closed} buttons={ - user?.type === "employer" ? ( + isEmployerPage ? ( 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/pages/NoticeEmployerPage/NoticeEmployerPage.tsx b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx index d5ed19d..9c34597 100644 --- a/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx +++ b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx @@ -1,36 +1,25 @@ -import { Navigate, useLoaderData, useParams } from "react-router-dom"; +import { useLoaderData, useParams } from "react-router-dom"; import NoticeDetailInfo from "../../components/NoticeDetailInfo"; -import NoticeApplicationTable from "./components/NoticeApplicationTable"; -import useShopApplications from "./hooks/useShopApplications"; +import NoticeApplicationTableContainer from "./components/NoticeApplicationTableContainer"; +import PostList, { PostData } from "@/components/Post/PostList"; import { useUserStore } from "@/hooks/useUserStore"; import { NoticeItem } from "@/types/notice"; -const PAGE_LIMIT = 7; - export default function NoticeEmployerPage() { - const { noticeInfo } = useLoaderData<{ + const { noticeInfo, recentNotices } = useLoaderData<{ noticeInfo: NoticeItem; + recentNotices: PostData[]; }>(); const { shopId, noticeId } = useParams() as { shopId: string; noticeId: string; }; - const { - refetch: refetchShopApplications, - shopApplications, - totalCount, - } = useShopApplications({ - shopId, - noticeId, - }); const { user } = useUserStore(); - if (user?.type !== "employer") { - return ; - } + const isMyShop = user?.shopId === shopId; return ( <> @@ -41,19 +30,24 @@ export default function NoticeEmployerPage() { noticeId={noticeId} noticeInfo={noticeInfo} user={user} + isEmployerPage />

-

신청자 목록

- +

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

+ {isMyShop ? ( + + ) : ( + + )}
diff --git a/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx b/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx index 5950513..b84d82e 100644 --- a/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx +++ b/src/pages/NoticeEmployerPage/components/NoticeApplicationTable.tsx @@ -3,13 +3,27 @@ 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 changeApplicationStatusSuccessMessageMap: { - [key in Exclude]: string; +const applicationStatusMessageMap: { + [key in Exclude]: { + inquiry: string; + success: string; + iconType: "check" | "warning"; + }; } = { - accepted: "승인이 완료되었습니다.", - rejected: "거절이 완료되었습니다.", + accepted: { + inquiry: "신청을 승인하시겠어요?", + success: "승인 완료!", + iconType: "check", + }, + rejected: { + inquiry: "신청을 거절하시겠어요?", + success: "거절했어요.", + iconType: "warning", + }, }; interface NoticeApplicationChangeStatusParams { @@ -22,38 +36,48 @@ interface NoticeApplicationChangeStatusParams { interface NoticeApplicationTableProps { data: ApplicationItem[]; totalCount: number; - pageLimit: number; itemCountPerPage?: number; + pageLimit?: number; refetch: () => void; } function NoticeApplicationTable({ data, totalCount, - pageLimit, itemCountPerPage = 5, + pageLimit = 5, refetch, }: NoticeApplicationTableProps) { + const { showToast } = useToast(); + const { openModal } = useModalStore(); + const changeApplicationStatus = async ({ shopId, noticeId, applicationId, status, }: NoticeApplicationChangeStatusParams) => { - const changeApplicationResult = await putApplication( - shopId, - noticeId, - applicationId, - status, - ); + const { iconType, inquiry, success } = applicationStatusMessageMap[status]; - if (changeApplicationResult.status === 200) { - // TODO: 모달 병합 후 모달로 적용 예정 - alert(changeApplicationStatusSuccessMessageMap[status]); - refetch(); - } - }; + 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 (
+ ); +} + +export default NoticeApplicationTableContainer; diff --git a/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts index c2b1a46..8c8cd8f 100644 --- a/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts +++ b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts @@ -1,16 +1,27 @@ import { LoaderFunction } from "react-router-dom"; -import { loadNotice } from "@/apis/loaders/notice"; +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 Promise.all([loadNotice({ shopId, noticeId })]); + if (user?.shopId === shopId) { + const [noticeInfo] = await Promise.all([loadNotice({ shopId, noticeId })]); - return { noticeInfo }; + return { noticeInfo }; + } + + const [noticeInfo, recentNotices] = await Promise.all([ + loadNotice({ shopId, noticeId }), + loadRecentNotices(), + ]); + + return { noticeInfo, recentNotices }; }; export default noticeEmployerLoader; From 6bd733f20087e8fb66e106e422ad5cd42b24263f Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sat, 3 May 2025 23:36:54 +0900 Subject: [PATCH 08/12] =?UTF-8?q?feat:=20=EC=B5=9C=EA=B7=BC=20=EB=B3=B8=20?= =?UTF-8?q?=EA=B3=B5=EA=B3=A0=20-=20=EB=A1=9C=EC=BB=AC=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EC=A7=80=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/loaders/notice.ts | 35 +++++------ src/hooks/useUpdateRecentNotices.ts | 59 +++++++++++++++++++ .../NoticeEmployeePage/NoticeEmployeePage.tsx | 23 +++++++- .../loader/noticeEmployeeLoader.ts | 6 +- .../NoticeEmployerPage/NoticeEmployerPage.tsx | 16 ++++- .../loader/noticeEmployerLoader.ts | 6 +- src/utils/localStorage.ts | 21 +++++++ 7 files changed, 133 insertions(+), 33 deletions(-) create mode 100644 src/hooks/useUpdateRecentNotices.ts create mode 100644 src/utils/localStorage.ts diff --git a/src/apis/loaders/notice.ts b/src/apis/loaders/notice.ts index d2bdfca..291dfa9 100644 --- a/src/apis/loaders/notice.ts +++ b/src/apis/loaders/notice.ts @@ -1,5 +1,8 @@ import { getShopApplications } from "../services/applicationService"; -import { getNotice, getNotices } from "../services/noticeService"; +import { getNotice } from "../services/noticeService"; + +import { PostData } from "@/components/Post/PostList"; +import { getLocalStorageValue } from "@/utils/localStorage"; interface LoadNoticeParams { shopId: string; @@ -14,25 +17,17 @@ export const loadNotice = async ({ shopId, noticeId }: LoadNoticeParams) => { } }; -export 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 MAX_VISIBLE_RECENT_NOTICES = 6; + +export const loadRecentNotices = async (noticeId: string) => { + const allRecentNotices = + getLocalStorageValue("recentNotices") ?? []; + + const recentNotices = allRecentNotices + .filter(({ id }) => id !== noticeId) + .slice(0, MAX_VISIBLE_RECENT_NOTICES); + + return recentNotices; }; interface LoadNoticeApplicationsParams { diff --git a/src/hooks/useUpdateRecentNotices.ts b/src/hooks/useUpdateRecentNotices.ts new file mode 100644 index 0000000..2231a22 --- /dev/null +++ b/src/hooks/useUpdateRecentNotices.ts @@ -0,0 +1,59 @@ +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.length > 0 && + !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/pages/NoticeEmployeePage/NoticeEmployeePage.tsx b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx index 2d678c3..1cfc616 100644 --- a/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx +++ b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx @@ -2,15 +2,25 @@ import { useLoaderData, useParams } from "react-router-dom"; import NoticeDetailInfo from "../../components/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/loader/noticeEmployeeLoader.ts b/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts index eb71776..eb65704 100644 --- a/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts +++ b/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts @@ -8,10 +8,8 @@ const noticeEmployeeLoader: LoaderFunction = async ({ params }) => { noticeId: string; }; - const [noticeInfo, recentNotices] = await Promise.all([ - loadNotice({ shopId, noticeId }), - loadRecentNotices(), - ]); + const [noticeInfo] = await Promise.all([loadNotice({ shopId, noticeId })]); + const recentNotices = loadRecentNotices(noticeId); return { noticeInfo, recentNotices }; }; diff --git a/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx index 9c34597..ffd1389 100644 --- a/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx +++ b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx @@ -5,6 +5,7 @@ import NoticeDetailInfo from "../../components/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"; @@ -21,6 +22,11 @@ export default function NoticeEmployerPage() { const isMyShop = user?.shopId === shopId; + useUpdateRecentNotices({ + noticeInfo, + link: `/notice/${shopId}/${noticeId}/employer`, + }); + return ( <>
@@ -40,13 +46,19 @@ export default function NoticeEmployerPage() {

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

- {isMyShop ? ( + {isMyShop && ( - ) : ( + )} + + {!isMyShop && recentNotices.length > 0 ? ( + ) : ( +
+ 최근에 본 공고가 없습니다. +
)}
diff --git a/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts index 8c8cd8f..80ec369 100644 --- a/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts +++ b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts @@ -16,10 +16,8 @@ const noticeEmployerLoader: LoaderFunction = async ({ params }) => { return { noticeInfo }; } - const [noticeInfo, recentNotices] = await Promise.all([ - loadNotice({ shopId, noticeId }), - loadRecentNotices(), - ]); + const [noticeInfo] = await Promise.all([loadNotice({ shopId, noticeId })]); + const recentNotices = loadRecentNotices(noticeId); return { noticeInfo, recentNotices }; }; 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); + } +}; From ff5e8e70e011f8825bf6b48d6b1706a8d9022ddd Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sun, 4 May 2025 12:57:25 +0900 Subject: [PATCH 09/12] =?UTF-8?q?refactor:=20noticeEmployerLoader,=20notic?= =?UTF-8?q?eEmployeeLoader=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts | 2 +- src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts b/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts index eb65704..3409d5e 100644 --- a/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts +++ b/src/pages/NoticeEmployeePage/loader/noticeEmployeeLoader.ts @@ -8,7 +8,7 @@ const noticeEmployeeLoader: LoaderFunction = async ({ params }) => { noticeId: string; }; - const [noticeInfo] = await Promise.all([loadNotice({ shopId, noticeId })]); + const noticeInfo = await loadNotice({ shopId, noticeId }); const recentNotices = loadRecentNotices(noticeId); return { noticeInfo, recentNotices }; diff --git a/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts index 80ec369..ec8cb36 100644 --- a/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts +++ b/src/pages/NoticeEmployerPage/loader/noticeEmployerLoader.ts @@ -10,15 +10,13 @@ const noticeEmployerLoader: LoaderFunction = async ({ params }) => { noticeId: string; }; - if (user?.shopId === shopId) { - const [noticeInfo] = await Promise.all([loadNotice({ shopId, noticeId })]); + const noticeInfo = await loadNotice({ shopId, noticeId }); + if (user?.shopId === shopId) { return { noticeInfo }; } - const [noticeInfo] = await Promise.all([loadNotice({ shopId, noticeId })]); const recentNotices = loadRecentNotices(noticeId); - return { noticeInfo, recentNotices }; }; From 0afdedca817b02b2a172fd0e8edea9e44c6092b7 Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sun, 4 May 2025 13:29:22 +0900 Subject: [PATCH 10/12] =?UTF-8?q?refactor:=20NoticeDetailInfoCard=EB=A1=9C?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/NoticeDetailInfo.tsx | 164 ------------------ .../NoticeDetailInfo/NoticeDetailInfo.tsx | 97 +++++++++++ .../NoticeEmployeeActionButton.tsx | 102 +++++++++++ .../NoticeEmployerActionButton.tsx | 35 ++++ src/constants/applicationStatus.ts | 6 + .../NoticeEmployeePage/NoticeEmployeePage.tsx | 2 +- .../NoticeEmployerPage/NoticeEmployerPage.tsx | 2 +- 7 files changed, 242 insertions(+), 166 deletions(-) delete mode 100644 src/components/NoticeDetailInfo.tsx create mode 100644 src/components/NoticeDetailInfo/NoticeDetailInfo.tsx create mode 100644 src/components/NoticeDetailInfo/NoticeEmployeeActionButton.tsx create mode 100644 src/components/NoticeDetailInfo/NoticeEmployerActionButton.tsx create mode 100644 src/constants/applicationStatus.ts diff --git a/src/components/NoticeDetailInfo.tsx b/src/components/NoticeDetailInfo.tsx deleted file mode 100644 index 7fb0d08..0000000 --- a/src/components/NoticeDetailInfo.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { useNavigate, useRevalidator } from "react-router-dom"; - -import { - postApplication, - putApplication, -} from "@/apis/services/applicationService"; -import Button from "@/components/Button"; -import PostCard from "@/components/Post/PostCard"; -import { useToast } from "@/hooks/useToast"; -import { User } from "@/hooks/useUserStore"; -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; - user?: User | null; - isEmployerPage?: boolean; -} - -function NoticeDetailInfo({ - shopId, - noticeId, - noticeInfo, - user, - isEmployerPage, -}: NoticeDetailInfoProps) { - const navigate = useNavigate(); - const { revalidate } = useRevalidator(); - 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" || - applicationStatus === "rejected"; - - 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, - "canceled", - ); - - if (result.status === 200) { - showToast("취소가 완료 되었습니다."); - revalidate(); - } - }, - }); - }; - - const moveToEditNoticePage = () => { - if (user) { - navigate(`/notice/edit/${user.shopId}`); - } - }; - - return ( - <> -
- - 식당 - -

{name}

-
- {noticeInfo && ( - - 공고 편집하기 - - ) : ( - - ) - } - /> - )} -
- 공고 설명 -

{description}

-
- - ); -} - -export default NoticeDetailInfo; 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/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/pages/NoticeEmployeePage/NoticeEmployeePage.tsx b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx index 1cfc616..a684757 100644 --- a/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx +++ b/src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx @@ -1,6 +1,6 @@ import { useLoaderData, useParams } from "react-router-dom"; -import NoticeDetailInfo from "../../components/NoticeDetailInfo"; +import NoticeDetailInfo from "../../components/NoticeDetailInfo/NoticeDetailInfo"; import PostList, { PostData } from "@/components/Post/PostList"; import useUpdateRecentNotices from "@/hooks/useUpdateRecentNotices"; diff --git a/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx index ffd1389..5950205 100644 --- a/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx +++ b/src/pages/NoticeEmployerPage/NoticeEmployerPage.tsx @@ -1,6 +1,6 @@ import { useLoaderData, useParams } from "react-router-dom"; -import NoticeDetailInfo from "../../components/NoticeDetailInfo"; +import NoticeDetailInfo from "../../components/NoticeDetailInfo/NoticeDetailInfo"; import NoticeApplicationTableContainer from "./components/NoticeApplicationTableContainer"; From d8dd89a65be520a7d5d513706a6ab468535e8294 Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sun, 4 May 2025 13:34:26 +0900 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20=EC=B5=9C=EA=B7=BC=20=EB=B3=B8=20?= =?UTF-8?q?=EA=B3=B5=EA=B3=A0=20=EB=82=98=ED=83=80=EB=82=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/loaders/notice.ts | 2 +- src/hooks/useUpdateRecentNotices.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/apis/loaders/notice.ts b/src/apis/loaders/notice.ts index 291dfa9..c09b4ab 100644 --- a/src/apis/loaders/notice.ts +++ b/src/apis/loaders/notice.ts @@ -19,7 +19,7 @@ export const loadNotice = async ({ shopId, noticeId }: LoadNoticeParams) => { const MAX_VISIBLE_RECENT_NOTICES = 6; -export const loadRecentNotices = async (noticeId: string) => { +export const loadRecentNotices = (noticeId: string) => { const allRecentNotices = getLocalStorageValue("recentNotices") ?? []; diff --git a/src/hooks/useUpdateRecentNotices.ts b/src/hooks/useUpdateRecentNotices.ts index 2231a22..24984cf 100644 --- a/src/hooks/useUpdateRecentNotices.ts +++ b/src/hooks/useUpdateRecentNotices.ts @@ -13,10 +13,7 @@ const RECENT_NOTICES = "recentNotices"; const updateLocalStorageRecentNotices = (candidateNotice: PostData) => { const storageValue: PostData[] = getLocalStorageValue(RECENT_NOTICES) ?? []; - if ( - storageValue.length > 0 && - !storageValue.some(({ id }) => id === candidateNotice.id) - ) { + if (!storageValue.some(({ id }) => id === candidateNotice.id)) { if (storageValue.length === 7) { storageValue.shift(); } From 7fb0321c871a2842326786322257ae2cbafa4256 Mon Sep 17 00:00:00 2001 From: cozy-ito Date: Sun, 4 May 2025 13:44:47 +0900 Subject: [PATCH 12/12] =?UTF-8?q?refactor:=20useShopApplications=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=9B=85=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EB=B0=B0=EC=97=B4=20=EC=9A=94=EC=86=8C=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/pages/NoticeEmployerPage/hooks/useShopApplications.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/NoticeEmployerPage/hooks/useShopApplications.ts b/src/pages/NoticeEmployerPage/hooks/useShopApplications.ts index 67bb83c..2bde58c 100644 --- a/src/pages/NoticeEmployerPage/hooks/useShopApplications.ts +++ b/src/pages/NoticeEmployerPage/hooks/useShopApplications.ts @@ -46,7 +46,7 @@ const useShopApplications = ({ useEffect(() => { fetchShopApplication(); - }, [page]); + }, [shopId, noticeId, offset, limit, page]); return { refetch: fetchShopApplication,