[feat] negosium: 협상 리스트 마감기준 정렬 컨트롤

- SortControl(기본순/마감임박순/여유순) 추가, StatusTabs 우측에 배치
- store setDeadline 배선 — 선택값을 서버 정렬(deadlineToOrder)로 전달
This commit is contained in:
Mina Choi 2026-07-29 17:29:50 +09:00
parent 82cca894fc
commit 35d38e0818
3 changed files with 54 additions and 10 deletions

View File

@ -0,0 +1,37 @@
import { cn } from '@/lib'
// 마감 기준 정렬. value(null=기본 그룹정렬)는 store.selectedDeadline 로 그대로 전달되고,
// adapter.deadlineToOrder 가 '남은 시간 적은 순'=asc(임박), 그 외=desc(여유) 로 매핑한다.
const SORT_OPTIONS: { label: string; value: string | null }[] = [
{ label: '기본순', value: null },
{ label: '마감임박순', value: '남은 시간 적은 순' },
{ label: '여유순', value: '남은 시간 긴 순' },
]
interface SortControlProps {
selected: string | null
onSelect: (value: string | null) => void
}
export function SortControl({ selected, onSelect }: SortControlProps) {
return (
<div className="flex items-center gap-0.5 rounded-xl border border-border bg-neutral-10 p-0.5">
{SORT_OPTIONS.map((o) => {
const active = selected === o.value
return (
<button
key={o.label}
type="button"
onClick={() => onSelect(o.value)}
className={cn(
'whitespace-nowrap rounded-[10px] px-2.5 py-[6px] text-xs font-bold transition-all active:scale-[0.98]',
active ? 'bg-white text-brand-700 shadow-sm' : 'text-neutral-60 hover:text-neutral-80',
)}
>
{o.label}
</button>
)
})}
</div>
)
}

View File

@ -15,6 +15,7 @@ import { useList } from '@/features/list/hooks/useList'
import { useListStore } from '@/features/list/stores/useListStore'
import { KpiCards } from '@/features/list/components/KpiCards'
import { StatusTabs } from '@/features/list/components/StatusTabs'
import { SortControl } from '@/features/list/components/SortControl'
import { WorkspaceTable } from '@/features/list/components/WorkspaceTable'
import { WorkspaceCards } from '@/features/list/components/WorkspaceCards'
import { Pagination } from '@/features/list/components/Pagination'
@ -35,19 +36,22 @@ const REJECT_BLOCKED: Record<string, string> = {
export function ListWorkspace() {
const navigate = useNavigate()
const { data: user } = useMeQuery()
// 리스트는 서버 페이징. (검색은 백엔드 미지원 → 프론트 검색은 비워 둠)
// 리스트는 서버 페이징 + 서버 검색(keyword: 견적번호·상품명·상품코드 부분일치).
const { items, isLoading, totalPages, currentPage, setCurrentPage } = useList()
const participate = useParticipateMutation()
const reject = useRejectMutation()
const saveExtra = useSaveExtraInfoMutation()
const requestRenego = useRequestRenegotiationMutation()
const { selectedStatus, selectedResult, setStatus, toggleResult, setKeyword } = useListStore(
const { selectedStatus, selectedResult, selectedDeadline, setStatus, toggleResult, setDeadline, setKeyword } =
useListStore(
useShallow((s) => ({
selectedStatus: s.selectedStatus,
selectedResult: s.selectedResult,
selectedDeadline: s.selectedDeadline,
setStatus: s.setStatus,
toggleResult: s.toggleResult,
setDeadline: s.setDeadline,
setKeyword: s.setKeyword,
})),
)
@ -153,6 +157,7 @@ export function ListWorkspace() {
<div className="flex flex-col gap-3 border-b border-border p-4 lg:flex-row lg:items-center lg:justify-between">
<StatusTabs selected={selectedStatus} onSelect={setStatus} />
<div className="flex w-full items-center gap-2 lg:w-auto">
<SortControl selected={selectedDeadline} onSelect={setDeadline} />
<div className="relative flex-1 lg:flex-none">
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-neutral-50" />
<input

View File

@ -13,6 +13,7 @@ interface ListState {
toggleStatus: (item: string) => void
setStatus: (item: string | null) => void
toggleDeadline: (item: string) => void
setDeadline: (item: string | null) => void
toggleResult: (code: number) => void
setKeyword: (keyword: string) => void
setCurrentPage: (page: number) => void
@ -38,6 +39,7 @@ export const useListStore = create<ListState>((set) => ({
setStatus: (item) => set({ selectedStatus: item, selectedResult: null, currentPage: 1 }),
toggleDeadline: (item) =>
set((s) => ({ selectedDeadline: toggle(s.selectedDeadline, item), currentPage: 1 })),
setDeadline: (item) => set({ selectedDeadline: item, currentPage: 1 }),
toggleResult: (code) =>
set((s) => ({ selectedResult: s.selectedResult === code ? null : code, selectedStatus: null, currentPage: 1 })),
setKeyword: (keyword) => set({ keyword, currentPage: 1 }),