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
Expand Up @@ -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";
Expand All @@ -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"),
);
Expand Down Expand Up @@ -93,12 +96,14 @@ const noticeRoutes: RouteObject[] = [
{
path: ROUTES.NOTICE.NOTICE_ID.EMPLOYER,
Component: NoticeEmployerPage,
loader: noticeEmployerLoader,
hydrateFallbackElement: <NoticeDetailSkeleton />,
},
{
path: ROUTES.NOTICE.NOTICE_ID.EMPLOYEE,
Component: NoticeEmployeePage,
loader: noticeEmployeeLoader,
hydrateFallbackElement: <ShopInfoPostCardSkeleton />,
hydrateFallbackElement: <NoticeDetailSkeleton />,
},
];

Expand Down
56 changes: 56 additions & 0 deletions src/apis/loaders/notice.ts
Original file line number Diff line number Diff line change
@@ -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<PostData[]>("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;
}
};
97 changes: 97 additions & 0 deletions src/components/NoticeDetailInfo/NoticeDetailInfo.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="flex flex-col gap-2 mb-1 md:mb-0">
<span className="inline-block text-sm md:text-base text-primary font-bold leading-5">
식당
</span>
<h2 className="text-xl md:text-[1.625rem] font-bold">{name}</h2>
</div>
{noticeInfo && (
<PostCard
name={name}
imageUrl={imageUrl}
address1={address1}
description={shopDescription}
hourlyPay={hourlyPay}
originalHourlyPay={originalHourlyPay}
startsAt={startsAt}
workhour={workhour}
closed={closed}
buttons={
isEmployerPage ? (
<NoticeEmployerActionButton
userShopId={user?.shopId}
noticeShopId={shopId}
noticeId={noticeId}
/>
) : (
<NoticeEmployeeActionButton
shopId={shopId}
noticeId={noticeId}
applicationId={applicationId}
applicationStatus={applicationStatus}
isDisabledNotice={isDisabledNotice}
/>
)
}
/>
)}
<div className="flex flex-col gap-3 p-8 bg-gray-10 text-black rounded-xl">
<span className="font-bold leading-5">공고 설명</span>
<p className="leading-[1.625rem]">{description}</p>
</div>
</>
);
}

export default NoticeDetailInfoCard;
102 changes: 102 additions & 0 deletions src/components/NoticeDetailInfo/NoticeEmployeeActionButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button
fullWidth
disabled={isDisabledNotice}
variant={
applicationStatus === APPLICATION_STATUS.PENDING ? "white" : "primary"
}
className={cn("py-[14px]", {
"cursor-pointer": !isDisabledNotice,
})}
onClick={
applicationStatus === APPLICATION_STATUS.PENDING
? cancelApplication
: applyNotice
}
>
{getApplicationButtonLabel()}
</Button>
);
}

export default NoticeEmployeeActionButton;
35 changes: 35 additions & 0 deletions src/components/NoticeDetailInfo/NoticeEmployerActionButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button
fullWidth
variant="white"
className={"py-[14px]"}
onClick={moveToEditNoticePage}
disabled={userShopId !== noticeShopId}
>
공고 편집하기
</Button>
);
}

export default NoticeEmployerActionButton;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Location, Time } from "@/assets/icon";

function ShopInfoPostCardSkeleton() {
function NoticeDetailSkeleton() {
return (
<section>
<div className="flex flex-col gap-3 md:gap-6 xl:w-[60.25rem] mx-auto px-3 md:px-8 py-10 md:py-[3.75rem]">
Expand Down Expand Up @@ -49,4 +49,4 @@ function ShopInfoPostCardSkeleton() {
);
}

export default ShopInfoPostCardSkeleton;
export default NoticeDetailSkeleton;
2 changes: 1 addition & 1 deletion src/components/Post/PostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function PostCard({
className="w-full h-[180px] object-cover md:h-[360px] lg:h-[308px]"
/>
{isDimmed && (
<div className="absolute inset-0 flex items-center justify-center bg-black opcity-70">
<div className="absolute inset-0 flex items-center justify-center bg-black opacity-70">
<p className="text-[1.75rem] text-gray-30 font-bold">
{!closed && isPast && "지난 공고"}
{closed && !isPast && "마감 완료"}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Post/PostList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Post from "./Post";

interface PostData {
export interface PostData {
id: string;
name: string;
imageUrl: string;
Expand Down
6 changes: 6 additions & 0 deletions src/constants/applicationStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const APPLICATION_STATUS = {
PENDING: "pending",
ACCEPTED: "accepted",
REJECTED: "rejected",
CANCELED: "canceled",
} as const;
2 changes: 1 addition & 1 deletion src/constants/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
Loading