('all')
const {
isLoading,
isError,
isEmpty,
- pending,
- accepted,
- cancelled,
+ sections,
actionsDisabled,
actionModal,
onApproveClick,
onRejectClick,
onActionModalClose,
onActionModalSubmit,
- } = useManagerSubstituteRequestViewModel()
+ } = useManagerSubstituteRequestViewModel({ statusFilter })
return (
@@ -136,52 +168,36 @@ export function ManagerSubstituteRequestPage() {
) : isEmpty ? (
-
-
- 대타 요청이 없습니다.
-
+
+
+
+ {statusFilterLabel(statusFilter)}
+
+
+
+
) : (
<>
- {pending.length > 0 && (
-
- {pending.map(item => (
- onApproveClick(item.id)}
- onReject={() => onRejectClick(item.id)}
- disabled={actionsDisabled}
- />
- ))}
-
- )}
-
- {accepted.length > 0 && (
-
- {accepted.map(item => (
-
- ))}
-
- )}
-
- {cancelled.length > 0 && (
-
- {cancelled.map(item => (
-
- ))}
-
- )}
+ {sections.map((section, index) => (
+
+ ))}
>
)}
diff --git a/src/pages/user/substitute-request/components/SubstituteRequestCard.tsx b/src/pages/user/substitute-request/components/SubstituteRequestCard.tsx
index 0dba642f..c34356dd 100644
--- a/src/pages/user/substitute-request/components/SubstituteRequestCard.tsx
+++ b/src/pages/user/substitute-request/components/SubstituteRequestCard.tsx
@@ -1,9 +1,7 @@
import type { SubstituteDirectionTab } from '@/pages/user/substitute-request/components/SubstituteRequestTabs'
-import { Avatar } from '@/shared/ui/common/Avatar'
import { SubstituteRequestResponseActions } from '@/pages/user/substitute-request/components/SubstituteRequestResponseActions'
import { SubstituteRequestStatusBadge } from '@/pages/user/substitute-request/components/SubstituteRequestStatusBadge'
import type { UserSubstituteListItem } from '@/features/user/substitute/types'
-import { WorkerRoleBadge } from '@/shared/ui/home/WorkerRoleBadge'
interface SubstituteRequestCardProps {
item: UserSubstituteListItem
@@ -45,39 +43,42 @@ export function SubstituteRequestCard({
}
: undefined
}
- className={`flex h-[72px] w-full items-center gap-4 rounded-2xl border border-line-1 bg-white px-5 text-left transition-colors active:bg-bg-light${isClickable ? ' cursor-pointer' : ''}`}
+ className={`w-full rounded-2xl border border-line-1 bg-white p-4 text-left transition-colors active:bg-bg-light${
+ isClickable ? ' cursor-pointer' : ''
+ }`}
>
- {directionTab === 'received' && (
-
- )}
-
-
-
- {item.displayName}
-
-
-
-
- 대타 근무 예정일
- {item.scheduledDateLabel}
-
+
+ {item.storeName}
+
+
+
+
+ {item.scheduleDateTitle}
+
+ · {item.scheduleTimeRangeLabel}
+
+
+
+ {item.positionLabel}
+
+
+
+
+ {item.createdAtLabel}
+
+ {showResponseActions ? (
+
+ ) : (
+
+ )}
- {showResponseActions ? (
-
- ) : (
-
- )}
)
}
diff --git a/src/pages/user/substitute-request/components/SubstituteRequestListSections.tsx b/src/pages/user/substitute-request/components/SubstituteRequestListSections.tsx
index 06447b89..887d114b 100644
--- a/src/pages/user/substitute-request/components/SubstituteRequestListSections.tsx
+++ b/src/pages/user/substitute-request/components/SubstituteRequestListSections.tsx
@@ -1,13 +1,18 @@
-import DownIcon from '@/assets/icons/home/chevron-down.svg?react'
-
import { SubstituteRequestCard } from '@/pages/user/substitute-request/components/SubstituteRequestCard'
+import { SubstituteStatusFilterDropdown } from '@/pages/user/substitute-request/components/SubstituteStatusFilterDropdown'
import type { SubstituteDirectionTab } from '@/pages/user/substitute-request/components/SubstituteRequestTabs'
import type { SubstituteListSection } from '@/features/user/substitute/hooks/useUserSubstituteRequestsViewModel'
+import {
+ statusFilterLabel,
+ type SubstituteListStatusFilter,
+} from '@/shared/types/substituteListFilters'
import type { UserSubstituteListItem } from '@/features/user/substitute/types'
interface SubstituteRequestListSectionsProps {
sections: SubstituteListSection[]
directionTab: SubstituteDirectionTab
+ statusFilter?: SubstituteListStatusFilter
+ onStatusFilterChange?: (value: SubstituteListStatusFilter) => void
onItemClick: (item: UserSubstituteListItem) => void
onAccept?: (item: UserSubstituteListItem) => void
onReject?: (item: UserSubstituteListItem) => void
@@ -17,38 +22,51 @@ interface SubstituteRequestListSectionsProps {
export function SubstituteRequestListSections({
sections,
directionTab,
+ statusFilter,
+ onStatusFilterChange,
onItemClick,
onAccept,
onReject,
actionsDisabled,
}: SubstituteRequestListSectionsProps) {
+ const showStatusFilter = statusFilter != null && onStatusFilterChange != null
+
if (sections.length === 0) {
return (
-
-
- 대타 요청 내역이 없습니다.
-
+
+ {showStatusFilter ? (
+
+
+ {statusFilterLabel(statusFilter)}
+
+
+
+ ) : null}
+
)
}
return (
- {sections.map(section => (
+ {sections.map((section, index) => (
{section.title}
- {section.key === 'pending' ? (
-
+ {showStatusFilter && index === 0 ? (
+
) : null}
diff --git a/src/pages/user/substitute-request/components/SubstituteRequestStatusBadge.tsx b/src/pages/user/substitute-request/components/SubstituteRequestStatusBadge.tsx
index eb49559d..44038dcb 100644
--- a/src/pages/user/substitute-request/components/SubstituteRequestStatusBadge.tsx
+++ b/src/pages/user/substitute-request/components/SubstituteRequestStatusBadge.tsx
@@ -7,20 +7,38 @@ interface SubstituteRequestStatusBadgeProps {
label: string
}
+const BADGE_STYLE_MAP: Record<
+ SubstituteUiStatus,
+ { containerClassName: string; textClassName: string }
+> = {
+ pending: {
+ containerClassName: 'border border-main bg-main-100',
+ textClassName: 'text-main',
+ },
+ accepted: {
+ containerClassName: 'border border-subBlue/30 bg-subBlue/10',
+ textClassName: 'text-subBlue',
+ },
+ cancelled: {
+ containerClassName: 'border border-error/30 bg-white',
+ textClassName: 'text-error',
+ },
+}
+
export function SubstituteRequestStatusBadge({
uiStatus,
label,
}: SubstituteRequestStatusBadgeProps) {
+ const style = BADGE_STYLE_MAP[uiStatus]
+
return (
- {label}
+ {label}
)
}
diff --git a/src/pages/user/substitute-request/components/SubstituteStatusFilterDropdown.tsx b/src/pages/user/substitute-request/components/SubstituteStatusFilterDropdown.tsx
new file mode 100644
index 00000000..d84a79c6
--- /dev/null
+++ b/src/pages/user/substitute-request/components/SubstituteStatusFilterDropdown.tsx
@@ -0,0 +1,80 @@
+import { useEffect, useRef, useState } from 'react'
+
+import DownIcon from '@/assets/icons/home/chevron-down.svg?react'
+
+import {
+ SUBSTITUTE_STATUS_FILTER_OPTIONS,
+ statusFilterLabel,
+ type SubstituteListStatusFilter,
+} from '@/shared/types/substituteListFilters'
+
+interface SubstituteStatusFilterDropdownProps {
+ value: SubstituteListStatusFilter
+ onChange: (value: SubstituteListStatusFilter) => void
+}
+
+export function SubstituteStatusFilterDropdown({
+ value,
+ onChange,
+}: SubstituteStatusFilterDropdownProps) {
+ const [isOpen, setIsOpen] = useState(false)
+ const containerRef = useRef
(null)
+
+ useEffect(() => {
+ function handleOutsideClick(e: MouseEvent) {
+ if (
+ containerRef.current &&
+ !containerRef.current.contains(e.target as Node)
+ ) {
+ setIsOpen(false)
+ }
+ }
+ if (isOpen) {
+ document.addEventListener('mousedown', handleOutsideClick)
+ }
+ return () => document.removeEventListener('mousedown', handleOutsideClick)
+ }, [isOpen])
+
+ return (
+
+
+
+ {isOpen ? (
+
+ {SUBSTITUTE_STATUS_FILTER_OPTIONS.map(option => (
+ -
+
+
+ ))}
+
+ ) : null}
+
+ )
+}
diff --git a/src/pages/user/substitute-request/index.tsx b/src/pages/user/substitute-request/index.tsx
index b04e40e8..a8f198b6 100644
--- a/src/pages/user/substitute-request/index.tsx
+++ b/src/pages/user/substitute-request/index.tsx
@@ -16,6 +16,7 @@ import type {
UserSubstituteListItem,
} from '@/features/user/substitute/types'
import { useUserSubstituteRequestsViewModel } from '@/features/user/substitute/hooks/useUserSubstituteRequestsViewModel'
+import type { SubstituteListStatusFilter } from '@/shared/types/substituteListFilters'
import { useWorkspacesViewModel } from '@/features/user'
import { SubstituteCreateFab } from '@/pages/user/substitute-request/components/SubstituteCreateFab'
import { SubstituteRejectReasonModal } from '@/pages/user/substitute-request/components/SubstituteRejectReasonModal'
@@ -90,6 +91,10 @@ export function SubstituteRequestPage() {
locationState?.directionTab ?? locationState?.direction
) ?? 'sent'
)
+ const [sentStatusFilter, setSentStatusFilter] =
+ useState('all')
+ const [receivedStatusFilter, setReceivedStatusFilter] =
+ useState('all')
const [storePickerOpen, setStorePickerOpen] = useState(false)
const [createFlow, setCreateFlow] = useState(null)
const [rejectRequestId, setRejectRequestId] = useState(null)
@@ -97,12 +102,17 @@ export function SubstituteRequestPage() {
const { workspaces, isLoading: workspacesLoading } = useWorkspacesViewModel()
const apiDirection = directionTab === 'sent' ? 'SENT' : 'RECEIVED'
+ const statusFilter =
+ directionTab === 'sent' ? sentStatusFilter : receivedStatusFilter
+ const setStatusFilter =
+ directionTab === 'sent' ? setSentStatusFilter : setReceivedStatusFilter
+
const {
sections,
isLoading: listLoading,
isError: listError,
refetch,
- } = useUserSubstituteRequestsViewModel(apiDirection)
+ } = useUserSubstituteRequestsViewModel(apiDirection, { statusFilter })
const invalidateLists = async () => {
await queryClient.invalidateQueries({
@@ -257,6 +267,8 @@ export function SubstituteRequestPage() {
void
@@ -136,64 +131,34 @@ export function SubstituteRequestModalFlow({
출근 시간
-
+
:
-
+
@@ -202,70 +167,49 @@ export function SubstituteRequestModalFlow({
퇴근 시간
-
+
:
-
+
+
+ {!flow.hasSelectedSchedule ? (
+
+ 선택한 날짜에 등록된 근무 스케줄이 없습니다.
+
+ ) : null}