o2o-negosium-original/negodata/front/src/lib/useServerList.ts
2026-06-30 10:07:46 +09:00

73 lines
3.0 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; // 엔터/즉시 검색용 (디바운스·최소길이 무시하고 바로 발사)
clearSearch: () => void; // X 버튼: 입력 비우고 즉시 전체 재검색
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);
};
// X 버튼: 입력·디바운스 검색어를 즉시 비우고(대기 타이머 취소) 1페이지로 → 전체 목록 재조회.
const clearSearch = () => {
clearTimeout(timerRef.current);
setSearchInput('');
setDebouncedSearch('');
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, clearSearch, debouncedSearch, filters, setFilter, totalPages };
}