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
8 changes: 7 additions & 1 deletion src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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[] = [
{
Expand Down Expand Up @@ -88,6 +92,8 @@ const noticeRoutes: RouteObject[] = [
{
path: ROUTES.NOTICE.NOTICE_ID.EMPLOYEE,
Component: NoticeEmployeePage,
loader: noticeEmployeeLoader,
hydrateFallbackElement: <ShopInfoPostCardSkeleton />,
},
];

Expand Down
5 changes: 3 additions & 2 deletions src/components/Post/Post.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
41 changes: 30 additions & 11 deletions src/components/Post/PostCard.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,6 +15,7 @@ interface PostCardProps {
isShopInfo?: boolean;
backgroundColor?: string;
buttons?: React.ReactNode;
closed?: boolean;
}

export default function PostCard({
Expand All @@ -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
Expand All @@ -45,30 +52,42 @@ export default function PostCard({
backgroundColor === "#ffffff" && "border border-gray-20 shadow-sm",
)}
>
<div className="w-full overflow-hidden rounded-xl">
<div className="relative w-full overflow-hidden rounded-xl">
<img
src={imageUrl}
alt={name}
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">
<p className="text-[1.75rem] text-gray-30 font-bold">
{!closed && isPast && "지난 공고"}
{closed && !isPast && "마감 완료"}
</p>
</div>
)}
</div>
<div className="flex flex-col justify-between mt-3 h-[251px] md:mt-4 md:h-[252px] lg:h-[292px]">
<div className="space-y-2 md:space-y-3">
{!isShopInfo ? (
<>
<div className="flex flex-col gap-[2px]">
<p className="text-primary body2-bold md:body1-bold">시급</p>
<p className="mb-2 text-primary body2-bold md:body1-bold">
시급
</p>
<div className="flex items-center gap-2">
<h2 className="text-[20px] font-bold md:text-[28px]">
{hourlyPay?.toLocaleString()}원
</h2>
<span className="inline-flex items-center gap-[2px] rounded-[20px] bg-primary px-3 py-1 text-[12px] font-normal leading-[16px] text-white md:body2-bold">
{rateText}
<ArrowUp className="w-4 h-4 md:w-5 md:h-5" />
</span>
{!closed && (
<span className="inline-flex items-center gap-[2px] rounded-[20px] bg-primary px-3 py-1 text-[12px] font-normal leading-[16px] text-white md:body2-bold">
{rateText}
<ArrowUp className="w-4 h-4 md:w-5 md:h-5" />
</span>
)}
</div>
</div>
<div className="flex items-center gap-[6px] text-gray-50 body2-regular md:body1-regular">
<div className="flex items-center gap-[6px] text-gray-50 sm:body2-regular md:body1-regular">
<Time className="w-4 h-4 md:w-5 md:h-5" />
{timeRange}
</div>
Expand All @@ -77,11 +96,11 @@ export default function PostCard({
<p className="text-primary body2-bold md:body1-bold">식당</p>
)}
{isShopInfo && <h2 className="text-[28px] font-bold">{name}</h2>}
<div className="flex items-center gap-[6px] text-gray-50 body2-regular md:body1-regular">
<div className="flex items-center gap-[6px] text-gray-50 sm:body2-regular md:body1-regular">
<Location className="w-4 h-4 md:w-5 md:h-5" />
{address1}
</div>
<p className="text-black body2-regular md:body1-regular">
<p className="text-black sm:body2-regular md:body1-regular">
{description}
</p>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/constants/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 0 additions & 3 deletions src/pages/NoticeEmployeePage.tsx

This file was deleted.

34 changes: 34 additions & 0 deletions src/pages/NoticeEmployeePage/NoticeEmployeePage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<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]">
<NoticeDetailInfo
shopId={shopId}
noticeId={noticeId}
noticeInfo={noticeInfo}
/>
</div>
</section>

<section>
<div className="flex flex-col gap-8 xl:w-[60.25rem] mx-auto mb-[3.75rem] px-3 md:px-8 py-10 md:py-[3.75rem]">
<h2 className="text-[1.625rem] font-bold">최근에 본 공고</h2>
<PostList posts={recentNotices} />
</div>
</section>
</>
);
}
131 changes: 131 additions & 0 deletions src/pages/NoticeEmployeePage/components/NoticeDetailInfo.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<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={
<Button
disabled={isDisabledNotice}
className={cn("py-[14px]", {
"cursor-pointer": !isDisabledNotice,
})}
fullWidth
variant={applicationStatus === "pending" ? "white" : "primary"}
onClick={
applicationStatus === "pending"
? cancelApplication
: applyNotice
}
>
{applicationStatus === "pending" && "취소하기"}
{applicationStatus === "accepted" && "승낙"}
{applicationStatus === "rejected" && "지원 거절"}
{applicationStatus === "canceled" &&
"이미 취소한 지원 공고 입니다."}
{!applicationStatus && isDisabledNotice && "신청 불가"}
{!applicationStatus && !closed && !isPast && "지원하기"}
</Button>
}
/>
)}
<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 NoticeDetailInfo;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Location, Time } from "@/assets/icon";

function ShopInfoPostCardSkeleton() {
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]">
<div className="flex flex-col gap-2 mb-1 md:mb-2">
<span className="inline-block text-sm md:text-base text-primary font-bold leading-5">
식당
</span>
<h2 className="h-5 md:h-[1.75rem] animate-skeleton rounded-md" />
</div>
<article
className={
"flex flex-col lg:flex-row gap-4 lg:gap-[1.875rem] w-full h-[30.25rem] md:h-[44.875rem] lg:h-[22.25rem] rounded-xl border border-gray-20 bg-white p-5 md:p-6 shadow-sm"
}
>
<div className="flex-1 h-full lg:h-[19.25rem]">
<div className="w-full h-full animate-skeleton rounded-xl" />
</div>

<div className="flex flex-col justify-between w-full lg:w-[21.625rem] h-[15.75rem] lg:h-full">
<div className="flex flex-col gap-3 md:gap-4">
<div className="flex flex-col justify-center">
<span className="inline-block w-[1.875rem] h-4 animate-skeleton rounded-sm lg:mt-4 mb-2" />
<span className="inline-block w-[10rem] h-[1.375rem] md:h-7 animate-skeleton rounded-sm" />
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center gap-1.5">
<Time className="h-4 w-4 md:h-5 md:w-5" />
<div className="w-full h-4 animate-skeleton rounded-sm" />
</div>
<div className="flex items-center gap-1.5">
<Location className="h-4 w-4 md:h-5 md:w-5" />
<div className="w-full h-4 animate-skeleton rounded-sm" />
</div>
</div>
</div>
<div className="w-full h-[2.625rem] animate-skeleton rounded-md" />
</div>
</article>

<div className="flex flex-col gap-3 p-8 bg-gray-10 rounded-xl">
<span className="font-bold leading-5">공고 설명</span>
<p className="w-full h-[3.25rem] animate-skeleton rounded-md" />
</div>
</div>
</section>
);
}

export default ShopInfoPostCardSkeleton;
Loading