feat(WEB): 자재/영업/품질 모듈 기능 개선 및 문서 컴포넌트 추가
- 입고관리: 상세/목록 UI 개선, actions 로직 강화 - 재고현황: 상세/목록 개선, StockAuditModal 신규 추가 - 영업주문관리: 페이지 구조 개선, OrderSalesDetailEdit 기능 강화 - 주문: OrderRegistration 개선, SalesOrderDocument 신규 추가 - 견적: QuoteTransactionModal 기능 개선 - 품질: InspectionModalV2, ImportInspectionDocument 대폭 개선 - UniversalListPage: 템플릿 기능 확장 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,8 @@ export function UniversalListPage<T>({
|
||||
// UI 상태
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(!initialData);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [searchValue, setSearchValue] = useState(''); // UI 입력용 (즉시 반영)
|
||||
const [debouncedSearchValue, setDebouncedSearchValue] = useState(''); // API 호출용 (debounced)
|
||||
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
|
||||
// 탭이 없으면 IntegratedListTemplateV2의 기본값 'default'와 일치해야 함
|
||||
const [activeTab, setActiveTab] = useState(
|
||||
@@ -86,6 +87,15 @@ export function UniversalListPage<T>({
|
||||
const [serverTotalCount, setServerTotalCount] = useState<number>(initialTotalCount || 0);
|
||||
const [serverTotalPages, setServerTotalPages] = useState<number>(1);
|
||||
|
||||
// ===== 검색 Debounce (300ms) =====
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedSearchValue(searchValue);
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchValue]);
|
||||
|
||||
// ===== ID 추출 헬퍼 =====
|
||||
const getItemId = useCallback(
|
||||
(item: T): string => {
|
||||
@@ -115,10 +125,10 @@ export function UniversalListPage<T>({
|
||||
filtered = filtered.filter((item) => config.tabFilter!(item, activeTab));
|
||||
}
|
||||
|
||||
// 검색 필터
|
||||
if (searchValue && config.searchFilter) {
|
||||
// 검색 필터 (debounced 값 사용)
|
||||
if (debouncedSearchValue && config.searchFilter) {
|
||||
filtered = filtered.filter((item) =>
|
||||
config.searchFilter!(item, searchValue)
|
||||
config.searchFilter!(item, debouncedSearchValue)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,7 +178,7 @@ export function UniversalListPage<T>({
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [rawData, activeTab, searchValue, filters, sortBy, sortOrder, config.clientSideFiltering, config.tabFilter, config.searchFilter, config.customFilterFn, config.customSortFn, config.dateRangeSelector]);
|
||||
}, [rawData, activeTab, debouncedSearchValue, filters, sortBy, sortOrder, config.clientSideFiltering, config.tabFilter, config.searchFilter, config.customFilterFn, config.customSortFn, config.dateRangeSelector]);
|
||||
|
||||
// 클라이언트 사이드 페이지네이션
|
||||
const paginatedData = useMemo(() => {
|
||||
@@ -222,7 +232,7 @@ export function UniversalListPage<T>({
|
||||
: {
|
||||
page: currentPage,
|
||||
pageSize: itemsPerPage,
|
||||
search: searchValue,
|
||||
search: debouncedSearchValue,
|
||||
filters,
|
||||
tab: activeTab,
|
||||
}
|
||||
@@ -249,7 +259,7 @@ export function UniversalListPage<T>({
|
||||
setIsLoading(false);
|
||||
setIsMobileLoading(false);
|
||||
}
|
||||
}, [config.actions, config.clientSideFiltering, currentPage, itemsPerPage, searchValue, filters, activeTab]);
|
||||
}, [config.actions, config.clientSideFiltering, currentPage, itemsPerPage, debouncedSearchValue, filters, activeTab]);
|
||||
|
||||
// 초기 로딩 (initialData가 없거나 빈 배열인 경우)
|
||||
useEffect(() => {
|
||||
@@ -289,6 +299,12 @@ export function UniversalListPage<T>({
|
||||
// 서버 사이드 필터링: 의존성 변경 시 데이터 새로고침
|
||||
// 이전 페이지를 추적하여 모바일 인피니티 스크롤 감지
|
||||
const [prevPage, setPrevPage] = useState(1);
|
||||
|
||||
// 날짜 범위 변경 감지용 (서버 사이드 필터링에서 날짜 변경 시 데이터 새로고침)
|
||||
const dateRangeKey = config.dateRangeSelector?.enabled
|
||||
? `${config.dateRangeSelector.startDate || ''}-${config.dateRangeSelector.endDate || ''}`
|
||||
: '';
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.clientSideFiltering && !isLoading && !isMobileLoading) {
|
||||
// 페이지가 증가하는 경우 = 모바일 인피니티 스크롤
|
||||
@@ -296,7 +312,7 @@ export function UniversalListPage<T>({
|
||||
fetchData(isMobileAppend);
|
||||
setPrevPage(currentPage);
|
||||
}
|
||||
}, [currentPage, searchValue, filters, activeTab]);
|
||||
}, [currentPage, debouncedSearchValue, filters, activeTab, dateRangeKey]);
|
||||
|
||||
// 동적 탭 로딩
|
||||
useEffect(() => {
|
||||
@@ -459,12 +475,14 @@ export function UniversalListPage<T>({
|
||||
|
||||
// ===== 검색 핸들러 =====
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
setSearchValue(value);
|
||||
setCurrentPage(1);
|
||||
setSelectedItems(new Set());
|
||||
// 외부 콜백 호출 (서버 사이드 검색용)
|
||||
onSearchChange?.(value);
|
||||
}, [onSearchChange]);
|
||||
setSearchValue(value); // UI 즉시 반영
|
||||
// 페이지 초기화와 외부 콜백은 debounce 후 실행됨 (아래 useEffect에서 처리)
|
||||
}, []);
|
||||
|
||||
// 외부 콜백에 debounced 값 전달 (자체 loadData 관리하는 컴포넌트용)
|
||||
useEffect(() => {
|
||||
onSearchChange?.(debouncedSearchValue);
|
||||
}, [debouncedSearchValue, onSearchChange]);
|
||||
|
||||
// ===== 필터 핸들러 =====
|
||||
const handleFilterChange = useCallback((key: string, value: string | string[]) => {
|
||||
@@ -514,7 +532,7 @@ export function UniversalListPage<T>({
|
||||
const additionalParams = fetchAllParams({
|
||||
activeTab,
|
||||
filters,
|
||||
searchValue,
|
||||
searchValue: debouncedSearchValue,
|
||||
});
|
||||
Object.entries(additionalParams).forEach(([key, value]) => {
|
||||
if (value) params.append(key, value);
|
||||
@@ -559,7 +577,7 @@ export function UniversalListPage<T>({
|
||||
} finally {
|
||||
setIsExcelDownloading(false);
|
||||
}
|
||||
}, [config.excelDownload, config.clientSideFiltering, filteredData, rawData, activeTab, filters, searchValue]);
|
||||
}, [config.excelDownload, config.clientSideFiltering, filteredData, rawData, activeTab, filters, debouncedSearchValue]);
|
||||
|
||||
// 선택 항목 엑셀 다운로드
|
||||
const handleSelectedExcelDownload = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user