feat: 수주등록 모달 및 범용 컴포넌트 개발
- 범용 컴포넌트 3종 개발 및 레지스트리 등록: * AutocompleteSearchInput: 자동완성 검색 입력 컴포넌트 * EntitySearchInput: 엔티티 검색 모달 컴포넌트 * ModalRepeaterTable: 모달 기반 반복 테이블 컴포넌트 - 수주등록 전용 컴포넌트: * OrderCustomerSearch: 거래처 검색 (AutocompleteSearchInput 래퍼) * OrderItemRepeaterTable: 품목 관리 (ModalRepeaterTable 래퍼) * OrderRegistrationModal: 수주등록 메인 모달 - 백엔드 API: * Entity 검색 API (멀티테넌시 지원) * 수주 등록 API (자동 채번) - 화면 편집기 통합: * 컴포넌트 레지스트리에 등록 * ConfigPanel을 통한 설정 기능 * 드래그앤드롭으로 배치 가능 - 개발 문서: * 수주등록_화면_개발_계획서.md (상세 설계 문서)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user