diff --git a/src/api/applications.ts b/src/api/applications.ts index 2541112..aaf4d33 100644 --- a/src/api/applications.ts +++ b/src/api/applications.ts @@ -43,8 +43,26 @@ export const putApplication = async (shopId: string, noticeId: string, applicati // 특정 가게 특정 공고 정보 export async function getNoticeById(shopId: string, noticeId: string): Promise { - const { data } = await axiosInstance.get(`/shops/${shopId}/notices/${noticeId}`); - return data; + const { data } = await axiosInstance.get(`/shops/${shopId}/notices/${noticeId}`); + + // API 응답 -> NoticeCard 형태로 매핑 + const noticeCard: NoticeCard = { + id: data.id, + name: data.shop?.name ?? '', + shopId: data.shop?.id ?? shopId, + address1: data.shop?.address1 ?? '', + hourlyPay: data.hourlyPay ?? 0, + originalHourlyPay: data.originalHourlyPay ?? data.hourlyPay ?? 0, + workhour: data.workhour ?? 0, + startsAt: data.startsAt, + closed: data.closed ?? false, + imageUrl: data.imageUrl ?? '', + description: data.description ?? '', + category: data.category ?? '기타', // 누락 필수 + shopDescription: data.shopDescription ?? '', // 누락 필수 + }; + + return noticeCard; } // 특정 가게 특정 공고 지원자 목록 @@ -62,3 +80,12 @@ export async function getApplications( return applications; } + +// 신청 상태 업데이트 +export const updateApplicationStatus = async ( + applicationId: string, + status: 'accepted' | 'rejected' +) => { + const res = await axiosInstance.put(`/applications/${applicationId}/status`, { status }); + return res.data; +}; diff --git a/src/components/features/noticeList/noticeListSection.tsx b/src/components/features/noticeList/noticeListSection.tsx index fa872f7..6d18a56 100644 --- a/src/components/features/noticeList/noticeListSection.tsx +++ b/src/components/features/noticeList/noticeListSection.tsx @@ -13,7 +13,7 @@ interface NoticeListSectionProps { } const NoticeListSection = ({ q, initialFilters }: NoticeListSectionProps) => { - const { notices, isLoading, isInitialized, error, pagination, fetchNotices,reset, filters } = + const { notices, isLoading, isInitialized, error, pagination, fetchNotices, reset, filters } = useNotices(); useEffect(() => { diff --git a/src/components/ui/badge/StatusBadge.tsx b/src/components/ui/badge/StatusBadge.tsx index d3046df..53bb9fd 100644 --- a/src/components/ui/badge/StatusBadge.tsx +++ b/src/components/ui/badge/StatusBadge.tsx @@ -1,16 +1,24 @@ import { Button } from '@/components/ui/button'; +import { UserRole } from '@/types/user'; import Badge from './Badge'; export type StatusType = 'pending' | 'accepted' | 'rejected'; interface StatusBadgeProps { status: StatusType; - variant: 'employer' | 'employee'; + userRole: UserRole; onApprove: () => void; onReject: () => void; + applicationId: string; + onStatusChange: (id: string, status: StatusType) => void; } -export default function StatusBadge({ status, variant, onApprove, onReject }: StatusBadgeProps) { +export default function StatusBadge({ + status, + userRole: variant, + onApprove, + onReject, +}: StatusBadgeProps) { if (status === 'pending' && variant === 'employer') { return (
diff --git a/src/components/ui/badge/statusbadge.stories.tsx b/src/components/ui/badge/statusbadge.stories.tsx index 218eeca..1e90ac0 100644 --- a/src/components/ui/badge/statusbadge.stories.tsx +++ b/src/components/ui/badge/statusbadge.stories.tsx @@ -15,7 +15,7 @@ type Story = StoryObj; export const Accept: Story = { args: { status: 'accepted', - variant: 'employer', + userRole: 'employer', }, }; @@ -23,7 +23,7 @@ export const Accept: Story = { export const Reject: Story = { args: { status: 'rejected', - variant: 'employer', + userRole: 'employer', }, }; @@ -31,7 +31,7 @@ export const Reject: Story = { export const PendingEmployee: Story = { args: { status: 'pending', - variant: 'employee', + userRole: 'employee', }, }; @@ -39,7 +39,7 @@ export const PendingEmployee: Story = { export const PendingEmployer: Story = { args: { status: 'pending', - variant: 'employer', + userRole: 'employer', onApprove: () => alert('승인!'), onReject: () => alert('거절!'), }, diff --git a/src/components/ui/card/post/post.tsx b/src/components/ui/card/post/post.tsx index c2fa836..f05c490 100644 --- a/src/components/ui/card/post/post.tsx +++ b/src/components/ui/card/post/post.tsx @@ -2,6 +2,7 @@ import { cardLayout, CardStatusVariant } from '@/components/ui/card/card.styles' import CardBadge from '@/components/ui/card/cardBadge'; import CardImage from '@/components/ui/card/cardImage'; import CardInfo from '@/components/ui/card/cardInfo'; +import useAuth from '@/hooks/useAuth'; import { getTime } from '@/lib/utils/dateFormatter'; import { formatNumber } from '@/lib/utils/formatNumber'; import { getNoticeStatus } from '@/lib/utils/getNoticeStatus'; @@ -18,6 +19,7 @@ const STATUS_LABEL = { } as const; const Post = ({ notice }: PostProps) => { + const { user } = useAuth(); const { id, hourlyPay, @@ -33,7 +35,8 @@ const Post = ({ notice }: PostProps) => { const status = getNoticeStatus(closed, startsAt); const { date, startTime, endTime } = getTime(startsAt, workhour); const statusVariant: CardStatusVariant = status === 'open' ? 'open' : 'inactive'; - const href = `/notices/${shopId}/${id}`; + const href = + user && user.shop ? `/employer/shops/${shopId}/notices/${id}` : `/notices/${shopId}/${id}`; return ( diff --git a/src/components/ui/table/Table.stories.tsx b/src/components/ui/table/Table.stories.tsx index a77d293..152e2e7 100644 --- a/src/components/ui/table/Table.stories.tsx +++ b/src/components/ui/table/Table.stories.tsx @@ -1,6 +1,6 @@ import Table from '@/components/ui/table/Table'; import { TableRowProps } from '@/components/ui/table/TableRowProps'; -import { UserType } from '@/types/user'; +import { UserRole, UserType } from '@/types/user'; import { Meta, StoryObj } from '@storybook/nextjs'; import { useEffect, useState } from 'react'; import { fetchTableData } from './testApi'; @@ -16,7 +16,7 @@ export default meta; type Story = StoryObj; -function TableWithTestApi({ userType }: { userType: UserType }) { +function TableWithTestApi({ userRole }: { userRole: UserRole }) { const [headers, setHeaders] = useState([]); const [data, setData] = useState([]); const [offset, setOffset] = useState(0); @@ -24,12 +24,12 @@ function TableWithTestApi({ userType }: { userType: UserType }) { useEffect(() => { const getData = async () => { - const res = await fetchTableData(userType); + const res = await fetchTableData(userRole); setHeaders(res.headers); setData(res.data as TableRowProps[]); }; getData(); - }, [userType]); + }, [userRole]); const count = data.length; const paginatedData = data.slice(offset, offset + limit); @@ -37,8 +37,8 @@ function TableWithTestApi({ userType }: { userType: UserType }) { return ( , + render: args => , }; export const EmployeeTable: Story = { args: { - userType: 'employee', + userRole: 'employee', }, - render: args => , + render: args => , }; diff --git a/src/components/ui/table/Table.tsx b/src/components/ui/table/Table.tsx index 1019d86..6ccaedc 100644 --- a/src/components/ui/table/Table.tsx +++ b/src/components/ui/table/Table.tsx @@ -1,25 +1,23 @@ import { Pagination } from '@/components/ui'; import { TableRowProps } from '@/components/ui/table/TableRowProps'; import { cn } from '@/lib/utils/cn'; -import { UserType } from '@/types/user'; +import { UserRole } from '@/types/user'; import TableRow from './TableRow'; interface TableProps { - data: TableRowProps[]; + tableData: TableRowProps[]; + userRole: UserRole; headers: string[]; - userType: UserType; total: number; limit: number; offset: number; onPageChange: (newOffset: number) => void; } -//
type은 확인이 좀 더 필요합니다 - export default function Table({ - data, + tableData, headers, - userType, + userRole, total, limit, offset, @@ -28,7 +26,7 @@ export default function Table({ return (
- {userType === 'employer' ? '신청자 목록' : '신청 목록'} + {userRole === 'employer' ? '신청자 목록' : '신청 목록'}
@@ -52,8 +50,8 @@ export default function Table({
- {data.map(row => ( - + {tableData.map(row => ( + ))}
diff --git a/src/components/ui/table/TableRow.tsx b/src/components/ui/table/TableRow.tsx index b65ea3d..af17530 100644 --- a/src/components/ui/table/TableRow.tsx +++ b/src/components/ui/table/TableRow.tsx @@ -3,27 +3,33 @@ import { StatusType } from '@/components/ui/badge/StatusBadge'; import { TableRowProps } from '@/components/ui/table/TableRowProps'; import { cn } from '@/lib/utils/cn'; import { getTime } from '@/lib/utils/dateFormatter'; +import { UserRole } from '@/types/user'; import { useState } from 'react'; interface TableTypeVariant { rowData: TableRowProps; - variant: 'employer' | 'employee'; + userRole: UserRole; } const TD_BASE = 'border-b border-r px-3 py-5 text-base gap-3 md:border-r-0'; const TD_STATUS = 'border-b px-2 py-[9px]'; -export default function TableRow({ rowData, variant }: TableTypeVariant) { +export default function TableRow({ rowData, userRole: userRole }: TableTypeVariant) { const { date, startTime, endTime, duration } = getTime(rowData.startsAt, rowData.workhour); const [status, setStatus] = useState(rowData.status as StatusType); + const handleStatusChange = (id: string, newStatus: StatusType) => { + setStatus(newStatus); + }; + const handleApprove = () => setStatus('accepted'); const handleReject = () => setStatus('rejected'); return ( {rowData.name} - {variant === 'employee' ? ( + + {userRole === 'employee' ? ( <> {`${date} ${startTime} ~ ${date} ${endTime} (${duration})`} {rowData.hourlyPay} @@ -36,8 +42,10 @@ export default function TableRow({ rowData, variant }: TableTypeVariant) { )} diff --git a/src/components/ui/table/testApi.tsx b/src/components/ui/table/testApi.tsx index 6e6d4ae..137c2c7 100644 --- a/src/components/ui/table/testApi.tsx +++ b/src/components/ui/table/testApi.tsx @@ -1,9 +1,9 @@ -import type { UserType } from '@/types/user'; +import type { UserRole } from '@/types/user'; -export const fetchTableData = async (userType: UserType) => { +export const fetchTableData = async (userRole: UserRole) => { return new Promise<{ headers: string[]; data: unknown[] }>(resolve => { setTimeout(() => { - if (userType === 'employer') { + if (userRole === 'employer') { resolve({ headers: ['신청자', '소개', '전화번호', '상태'], data: [ diff --git a/src/pages/employer/shops/[shopId]/notices/[noticeId]/edit.tsx b/src/pages/employer/shops/[shopId]/notices/[noticeId]/edit.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/employer/shops/[shopId]/notices/[noticeId]/index.tsx b/src/pages/employer/shops/[shopId]/notices/[noticeId]/index.tsx index 75743a2..7af60b9 100644 --- a/src/pages/employer/shops/[shopId]/notices/[noticeId]/index.tsx +++ b/src/pages/employer/shops/[shopId]/notices/[noticeId]/index.tsx @@ -1,90 +1,213 @@ -import { getApplications, getNoticeById } from '@/api/applications'; -import { Button, Notice, Table } from '@/components/ui'; +import { Button, Modal, Notice, Table } from '@/components/ui'; import { TableRowProps } from '@/components/ui/table/TableRowProps'; import useAuth from '@/hooks/useAuth'; -import { NoticeCard } from '@/types/notice'; +import axiosInstance from '@/lib/axios'; +import { getNoticeStatus } from '@/lib/utils/getNoticeStatus'; +import { toNoticeCard } from '@/lib/utils/parse'; +import type { NoticeCard } from '@/types/notice'; +import { Shop } from '@/types/shop'; +import { UserRole } from '@/types/user'; +import type { GetServerSideProps } from 'next'; import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; -const EmployerNoticePage = () => { - const router = useRouter(); - const { query } = router; - const shopId = Array.isArray(query.shopId) ? query.shopId[0] : query.shopId; - const noticeId = Array.isArray(query.noticeId) ? query.noticeId[0] : query.noticeId; +interface ModalItems { + title?: string; + primaryText?: string; + secondaryText?: string; + onPrimary?: () => void; + onSecondary?: () => void; +} +interface EditItems extends ModalItems { + noShop?: ModalItems; + shop?: ModalItems; +} +interface ApplicationTableApiResponse { + item: { + id: string; + status: string; + user?: { + href: string; + item: { + id: string; + name: string; + bio: string; + phone: string; + }; + }; + notice?: { + item?: { + startsAt: string; + workhour: number; + hourlyPay?: number; + }; + }; + }; + links: unknown[]; +} - const { user } = useAuth(); +const EDIT_ITEMS: Record = { + guest: { + title: '로그인이 필요합니다', + primaryText: '로그인하기', + secondaryText: '닫기', + }, + employer: { + shop: {}, + noShop: { + title: '내 가게를 먼저 등록해주세요', + primaryText: '가게 등록', + secondaryText: '닫기', + }, + }, + employee: { + title: '접근할 수 없습니다', + primaryText: '확인', + }, +}; + +function hasShopFields(user: Shop | null) { + if (!user) return false; + return Boolean( + user.name && + user.category && + user.address1 && + user.address2 && + user.description && + user.imageUrl && + user.originalHourlyPay + ); +} - const [notice, setNotice] = useState(); +export const getServerSideProps: GetServerSideProps<{ notice: NoticeCard }> = async ({ + params, +}) => { + const { shopId, noticeId } = params as { shopId: string; noticeId: string }; + try { + const noticeRes = await axiosInstance.get(`shops/${shopId}/notices/${noticeId}`); + return { props: { notice: toNoticeCard(noticeRes.data) } }; + } catch { + return { + notFound: true, + }; + } +}; - const [headers, setHeaders] = useState([]); +const EmployerNoticeDetailPage = ({ notice }: { notice: NoticeCard }) => { + const headers = ['신청자', '소개', '전화번호', '상태']; const [data, setData] = useState([]); const [offset, setOffset] = useState(0); const limit = 5; - // employer만 접근 가능 - useEffect(() => { - if (user && !user.shop) { - router.replace('/'); + const { role, isLogin, user } = useAuth(); + const router = useRouter(); + const [modalOpen, setModalOpen] = useState(false); + const [modal, setModal] = useState(null); + + const status = getNoticeStatus(notice.closed, notice.startsAt); + const canEdit = useMemo(() => status === 'open', [status]); + + // 공고 편집하기 + const handleEditClick = useCallback(() => { + if (!canEdit) return; + + if (!isLogin) { + const items = EDIT_ITEMS.guest; + setModal({ + ...items, + onPrimary: () => router.push('/login'), + onSecondary: () => setModalOpen(false), + }); + setModalOpen(true); + return; } - }, [user, router]); - // 공고 조회 - useEffect(() => { - if (!shopId || !noticeId) return; + if (role === 'employee') { + const items = EDIT_ITEMS.employee; + setModal({ + ...items, + onPrimary: () => setModalOpen(false), + }); + setModalOpen(true); + return; + } - const fetchNotice = async () => { - const result = await getNoticeById(shopId, noticeId); - setNotice(result); - }; + const hasShop = hasShopFields(user?.shop?.item ?? null); + if (!hasShop) { + const items = EDIT_ITEMS.employer.noShop; + setModal({ + ...items, + onPrimary: () => router.push('/my-shop'), + onSecondary: () => setModalOpen(false), + }); + setModalOpen(true); + return; + } - fetchNotice().catch(() => { - setNotice(undefined); - }); - }, [shopId, noticeId]); + router.push(`/employer/shops/${notice.shopId}/notices/${notice.id}/edit`); + }, [canEdit, isLogin, role, user, notice, router]); - // 지원자 목록 조회 - useEffect(() => { - if (!shopId || !noticeId) return; + // 신청자 불러오기 + useEffect(() => { const fetchApplications = async () => { - const applications = await getApplications(shopId, noticeId, offset, limit); - setHeaders(['신청자', '소개', '전화번호', '상태']); - setData( - applications.map(app => ({ - id: app.id, - name: app.user?.name ?? '-', - startsAt: '-', - workhour: 0, - hourlyPay: '-', - status: app.status, - bio: '-', - phone: '-', - })) + const res = await axiosInstance.get<{ items: ApplicationTableApiResponse[] }>( + `/shops/${notice.shopId}/notices/${notice.id}/applications`, + { params: { offset, limit } } ); + + const tableData: TableRowProps[] = res.data.items.map(app => { + const userItem = app.item.user?.item; + const noticeItem = app.item.notice?.item; + + return { + id: app.item.id, + name: userItem?.name ?? '-', + bio: userItem?.bio ?? '-', + phone: userItem?.phone ?? '-', + startsAt: noticeItem?.startsAt ?? '-', + workhour: noticeItem?.workhour ?? 0, + hourlyPay: noticeItem?.hourlyPay + ? `${noticeItem.hourlyPay.toLocaleString()}원` + : '정보 없음', + status: app.item.status, + }; + }); + + setData(tableData); }; fetchApplications(); - }, [shopId, noticeId, offset]); - - if (!notice) return null; + }, [notice.shopId, notice.id, offset, limit]); return ( -
- +
+ + setModalOpen(false)} + variant='warning' + title={modal?.title ?? '유저 정보를 확인해주세요'} + primaryText={modal?.primaryText ?? '확인'} + onPrimary={modal?.onPrimary ?? (() => setModalOpen(false))} + secondaryText={modal?.secondaryText} + onSecondary={modal?.onSecondary} + /> { ); }; -export default EmployerNoticePage; +export default EmployerNoticeDetailPage; diff --git a/src/pages/employer/shops/[shopId]/notices/register/index.tsx b/src/pages/employer/shops/[shopId]/notices/register/index.tsx index 3571d44..ee49651 100644 --- a/src/pages/employer/shops/[shopId]/notices/register/index.tsx +++ b/src/pages/employer/shops/[shopId]/notices/register/index.tsx @@ -22,6 +22,7 @@ const EmployerNoticeRegisterPage = () => { const [description, setDescription] = useState(''); const [modalOpen, setModalOpen] = useState(false); + const [newNoticeId, setNewNoticeId] = useState(null); useEffect(() => { if (user && !user.shop) { @@ -34,6 +35,7 @@ const EmployerNoticeRegisterPage = () => { e.preventDefault(); if (!date || !time || !wage || !workhour || !description) return; + if (!user?.shop) return; const combinedDateTime = new Date(date); combinedDateTime.setHours(time.getHours(), time.getMinutes(), 0, 0); @@ -45,19 +47,21 @@ const EmployerNoticeRegisterPage = () => { description, }; - if (!user?.shop) return; - try { - await axiosInstance.post(`/shops/${user.shop.item.id}/notices`, payload); + const response = await axiosInstance.post(`/shops/${user.shop.item.id}/notices`, payload); + const noticeId = response.data.id; // 새 공고 ID + setNewNoticeId(noticeId); setModalOpen(true); } catch (error) { - alert(error instanceof Error ? error.message : '등록 중 오류 발생'); + alert(error instanceof Error ? error.message : '공고 등록 중 오류 발생'); } }; const handleModalClose = () => { setModalOpen(false); - router.push(`/my-shop`); + if (user?.shop && newNoticeId) { + router.push(`/employer/shops/${user.shop.item.id}/notices/${newNoticeId}`); + } }; if (!user?.shop) return null; @@ -114,7 +118,6 @@ const EmployerNoticeRegisterPage = () => { onChange={(selectedTime: Date | null) => setTime(selectedTime)} /> -
>; + +export type ApplicationTableDataItem = { + id: string; + status: string; + shop: { item: Shop; href: string }; + notice: { item: NoticeCard; href: string }; + user: { item: UserProfile; href: string }; +};