o2o-negosium-original/negodata/front/src/lib/useServerList.ts
Mina Choi 6fa024137f [feat] negodata: 견적·카드 검색/필터 서버사이드 전환 + 견적 상세 Sheet 리팩터
- 백엔드: 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>
2026-06-19 17:03:53 +09:00

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 };
}