- 백엔드: quotation 목록 search 파라미터(name/number ILIKE) 추가; card 목록 is_wildcard 탭 필터 + total_nego/total_wild 탭 카운트 추가; status/type 코드 int 비교 보정 - 프론트: cards/quotation 검색·상태·유형 필터·페이지네이션을 useServerList 기반 서버사이드로 전환; 엔터 즉시검색 + 디바운스 500ms·최소 2글자; 죽은 client 필터 훅(useCardFilters/useQuotationFilters) 제거 - generated 파라미터/응답 타입 손보강(listCardsParams.is_wildcard, listQuotationsParams.search, resCardList.total_nego/total_wild) — orval 재생성 시 동일 - 견적 상세 Drawer→Sheet 분리, useOverlayParams→useOverlayRouter, CreateQuotationWizard→QuotationCreateModal Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2.6 KiB
TypeScript
65 lines
2.6 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
|
|
// 서버사이드 리스트(검색·필터·페이지네이션)의 UI 상태 단일 출처.
|
|
// 실제 데이터 패칭은 각 도메인 훅(useProducts/usePartners 등)이 이 상태로
|
|
// - 검색/필터가 바뀌면 page 를 1 로 리셋한다(다른 결과셋의 동일 페이지로 점프 방지).
|
|
export type ServerListControls = {
|
|
page: number;
|
|
setPage: (p: number) => void;
|
|
pageSize: number;
|
|
search: string; // input value (controlled)
|
|
setSearch: (v: string) => void;
|
|
submitSearch: () => void; // 엔터/즉시 검색용 (디바운스·최소길이 무시하고 바로 발사)
|
|
debouncedSearch: string; // 쿼리 파라미터용 (디바운스 적용)
|
|
filters: Record<string, string>;
|
|
setFilter: (key: string, value: string) => void;
|
|
totalPages: (total: number) => number; // 서버 total → 페이지 수
|
|
};
|
|
|
|
export function useServerList(opts?: {
|
|
pageSize?: number;
|
|
initialFilters?: Record<string, string>;
|
|
debounceMs?: number;
|
|
minSearchLength?: number;
|
|
}): ServerListControls {
|
|
const pageSize = opts?.pageSize ?? 10;
|
|
const debounceMs = opts?.debounceMs ?? 500;
|
|
const minSearchLength = opts?.minSearchLength ?? 2;
|
|
|
|
const [page, setPage] = useState(1);
|
|
const [search, setSearchInput] = useState('');
|
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
const [filters, setFilters] = useState<Record<string, string>>(() => opts?.initialFilters ?? {});
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
|
|
// 입력 디바운스 → 쿼리용 검색어.
|
|
// 최소 길이 미만은 빈 검색(전체)으로 둬서 1글자 스캔 요청이 서버로 나가지 않게 막는다.
|
|
useEffect(() => {
|
|
timerRef.current = setTimeout(() => {
|
|
const q = search.trim();
|
|
setDebouncedSearch(q.length >= minSearchLength ? q : '');
|
|
}, debounceMs);
|
|
return () => clearTimeout(timerRef.current);
|
|
}, [search, debounceMs, minSearchLength]);
|
|
|
|
const setSearch = (v: string) => {
|
|
setSearchInput(v);
|
|
setPage(1);
|
|
};
|
|
|
|
// 엔터: 대기 중인 디바운스 타이머를 버리고 최소길이 무시하고 즉시 1회 발사(의도적 검색).
|
|
const submitSearch = () => {
|
|
clearTimeout(timerRef.current);
|
|
setDebouncedSearch(search.trim());
|
|
setPage(1);
|
|
};
|
|
const setFilter = (key: string, value: string) => {
|
|
setFilters((f) => ({ ...f, [key]: value }));
|
|
setPage(1);
|
|
};
|
|
|
|
const totalPages = (total: number) => Math.max(1, Math.ceil(total / pageSize));
|
|
|
|
return { page, setPage, pageSize, search, setSearch, submitSearch, debouncedSearch, filters, setFilter, totalPages };
|
|
}
|