Files
vexplor/frontend/lib/registry/components/entity-search-input/useEntitySearch.ts
kjs 64e6fd1920 feat: 수주등록 모달 및 범용 컴포넌트 개발
- 범용 컴포넌트 3종 개발 및 레지스트리 등록:
  * AutocompleteSearchInput: 자동완성 검색 입력 컴포넌트
  * EntitySearchInput: 엔티티 검색 모달 컴포넌트
  * ModalRepeaterTable: 모달 기반 반복 테이블 컴포넌트

- 수주등록 전용 컴포넌트:
  * OrderCustomerSearch: 거래처 검색 (AutocompleteSearchInput 래퍼)
  * OrderItemRepeaterTable: 품목 관리 (ModalRepeaterTable 래퍼)
  * OrderRegistrationModal: 수주등록 메인 모달

- 백엔드 API:
  * Entity 검색 API (멀티테넌시 지원)
  * 수주 등록 API (자동 채번)

- 화면 편집기 통합:
  * 컴포넌트 레지스트리에 등록
  * ConfigPanel을 통한 설정 기능
  * 드래그앤드롭으로 배치 가능

- 개발 문서:
  * 수주등록_화면_개발_계획서.md (상세 설계 문서)
2025-11-14 14:43:53 +09:00

111 lines
3.1 KiB
TypeScript

import { useState, useCallback, useEffect, useRef } from "react";
import { apiClient } from "@/lib/api/client";
import { EntitySearchResult, EntitySearchResponse } from "./types";
interface UseEntitySearchProps {
tableName: string;
searchFields?: string[];
filterCondition?: Record<string, any>;
}
export function useEntitySearch({
tableName,
searchFields = [],
filterCondition = {},
}: UseEntitySearchProps) {
const [searchText, setSearchText] = useState("");
const [results, setResults] = useState<EntitySearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] = useState({
total: 0,
page: 1,
limit: 20,
});
// searchFields와 filterCondition을 ref로 관리하여 useCallback 의존성 문제 해결
const searchFieldsRef = useRef(searchFields);
const filterConditionRef = useRef(filterCondition);
useEffect(() => {
searchFieldsRef.current = searchFields;
filterConditionRef.current = filterCondition;
}, [searchFields, filterCondition]);
const search = useCallback(
async (text: string, page: number = 1) => {
try {
setLoading(true);
setError(null);
const params = new URLSearchParams({
searchText: text,
searchFields: searchFieldsRef.current.join(","),
filterCondition: JSON.stringify(filterConditionRef.current),
page: page.toString(),
limit: pagination.limit.toString(),
});
const response = await apiClient.get<EntitySearchResponse>(
`/entity-search/${tableName}?${params.toString()}`
);
if (response.data.success) {
setResults(response.data.data);
if (response.data.pagination) {
setPagination(response.data.pagination);
}
} else {
setError(response.data.error || "검색에 실패했습니다");
}
} catch (err: any) {
console.error("Entity search error:", err);
setError(err.response?.data?.message || "검색 중 오류가 발생했습니다");
} finally {
setLoading(false);
}
},
[tableName, pagination.limit]
);
// 디바운스된 검색
useEffect(() => {
// searchText가 명시적으로 설정되지 않은 경우(null/undefined)만 건너뛰기
if (searchText === null || searchText === undefined) {
return;
}
const timer = setTimeout(() => {
// 빈 문자열("")도 검색 (전체 목록 조회)
search(searchText.trim(), 1);
}, 300); // 300ms 디바운스
return () => clearTimeout(timer);
}, [searchText, search]);
const clearSearch = useCallback(() => {
setSearchText("");
setResults([]);
setError(null);
}, []);
const loadMore = useCallback(() => {
if (pagination.page * pagination.limit < pagination.total) {
search(searchText, pagination.page + 1);
}
}, [search, searchText, pagination]);
return {
searchText,
setSearchText,
results,
loading,
error,
pagination,
search,
clearSearch,
loadMore,
};
}