탭 컴포넌트 외부 검색필터 동작 구현

This commit is contained in:
kjs
2025-12-18 09:53:26 +09:00
parent 3589e4a5b9
commit ff3c51c457
5 changed files with 128 additions and 23 deletions

View File

@@ -43,25 +43,24 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
/**
* 테이블 등록 해제
* 주의:
* 1. selectedTableId를 의존성으로 사용하면 무한 루프 발생 가능
* 2. 재등록 시에도 unregister가 호출되므로 selectedTableId를 변경하면 안됨
*/
const unregisterTable = useCallback(
(tableId: string) => {
setRegisteredTables((prev) => {
const newMap = new Map(prev);
const removed = newMap.delete(tableId);
if (removed) {
// 선택된 테이블이 제거되면 첫 번째 테이블 선택
if (selectedTableId === tableId) {
const firstTableId = newMap.keys().next().value;
setSelectedTableId(firstTableId || null);
}
}
newMap.delete(tableId);
return newMap;
});
// 🚫 selectedTableId를 변경하지 않음
// 이유: useEffect 재실행 시 cleanup → register 순서로 호출되는데,
// cleanup에서 selectedTableId를 null로 만들면 필터 설정이 초기화됨
// 다른 테이블이 선택되어야 하면 TableSearchWidget에서 자동 선택함
},
[selectedTableId]
[] // 의존성 없음 - 무한 루프 방지
);
/**

View File

@@ -59,6 +59,9 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
// 변환된 값 캐시 (중복 변환 방지)
const convertedCache = useRef(new Map<string, string>());
// 초기화 완료 플래그 (무한 루프 방지)
const initialLoadDone = useRef(false);
// 공통 코드 카테고리 추출 (메모이제이션)
const codeCategories = useMemo(() => {
@@ -293,24 +296,40 @@ export function useEntityJoinOptimization(columnMeta: Record<string, ColumnMetaI
[codeCategories, batchLoadCodes, updateMetrics],
);
// 초기화 시 공통 코드 프리로딩
// 초기화 시 공통 코드 프리로딩 (한 번만 실행)
useEffect(() => {
// 이미 초기화되었으면 스킵 (무한 루프 방지)
if (initialLoadDone.current) return;
initialLoadDone.current = true;
preloadCommonCodesOnMount();
}, [preloadCommonCodesOnMount]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 컬럼 메타 변경 시 필요한 코드 추가 로딩
// 이미 로딩 중이면 스킵하여 무한 루프 방지
const loadedCategoriesRef = useRef<Set<string>>(new Set());
useEffect(() => {
// 이미 최적화 중이거나 초기화 전이면 스킵
if (isOptimizing) return;
if (codeCategories.length > 0) {
const unloadedCategories = codeCategories.filter((category) => {
// 이미 로드 요청을 보낸 카테고리는 스킵
if (loadedCategoriesRef.current.has(category)) return false;
return codeCache.getCodeSync(category) === null;
});
if (unloadedCategories.length > 0) {
// 로딩 요청 카테고리 기록
unloadedCategories.forEach(cat => loadedCategoriesRef.current.add(cat));
console.log(`🔄 새로운 코드 카테고리 감지, 추가 로딩: ${unloadedCategories.join(", ")}`);
batchLoadCodes(unloadedCategories);
}
}
}, [codeCategories, batchLoadCodes]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [codeCategories.join(",")]); // 배열 내용 기반 의존성
// 주기적으로 메트릭 업데이트
useEffect(() => {

View File

@@ -416,6 +416,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
// originalData를 사용 (최초 전달된 값, formData는 계속 변경되므로 사용하면 안됨)
_initialData: originalData || formData,
_originalData: originalData,
// 🆕 탭 관련 정보 전달 (탭 내부의 테이블 컴포넌트에서 사용)
parentTabId: props.parentTabId,
parentTabsComponentId: props.parentTabsComponentId,
};
// 렌더러가 클래스인지 함수인지 확인

View File

@@ -1033,6 +1033,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
return () => {
unregisterTable(tableId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
tableId,
tableConfig.selectedTable,
@@ -1044,7 +1045,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
data, // 데이터 자체가 변경되면 재등록 (고유 값 조회용)
totalItems, // 전체 항목 수가 변경되면 재등록
registerTable,
unregisterTable,
// unregisterTable은 의존성에서 제외 - 무한 루프 방지
// unregisterTable 함수는 의존성이 없어 안정적임
]);
// 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기

View File

@@ -138,33 +138,84 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// currentTable은 tableList(필터링된 목록)에서 가져와야 함
const currentTable = useMemo(() => {
console.log("🔍 [TableSearchWidget] currentTable 계산:", {
selectedTableId,
tableListLength: tableList.length,
tableList: tableList.map(t => ({ id: t.tableId, name: t.tableName, parentTabId: t.parentTabId }))
});
if (!selectedTableId) return undefined;
// 먼저 tableList(필터링된 목록)에서 찾기
const tableFromList = tableList.find(t => t.tableId === selectedTableId);
if (tableFromList) {
console.log("✅ [TableSearchWidget] 테이블 찾음 (tableList):", tableFromList.tableName);
return tableFromList;
}
// tableList에 없으면 전체에서 찾기 (폴백)
return getTable(selectedTableId);
const tableFromAll = getTable(selectedTableId);
console.log("🔄 [TableSearchWidget] 테이블 찾음 (전체):", tableFromAll?.tableName);
return tableFromAll;
}, [selectedTableId, tableList, getTable]);
// 🆕 활성 탭 ID 문자열 (변경 감지용)
const activeTabIdsStr = useMemo(() => activeTabIds.join(","), [activeTabIds]);
// 🆕 이전 활성 탭 ID 추적 (탭 전환 감지용)
const prevActiveTabIdsRef = useRef<string>(activeTabIdsStr);
// 대상 패널의 첫 번째 테이블 자동 선택
useEffect(() => {
if (!autoSelectFirstTable || tableList.length === 0) {
return;
}
// 🆕 탭 전환 감지: 활성 탭이 변경되었는지 확인
const tabChanged = prevActiveTabIdsRef.current !== activeTabIdsStr;
if (tabChanged) {
console.log("🔄 [TableSearchWidget] 탭 전환 감지:", {
이전탭: prevActiveTabIdsRef.current,
현재탭: activeTabIdsStr,
가용테이블: tableList.map(t => ({ id: t.tableId, tableName: t.tableName, parentTabId: t.parentTabId })),
현재선택테이블: selectedTableId
});
prevActiveTabIdsRef.current = activeTabIdsStr;
// 🆕 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택
const activeTabTable = tableList.find(t => t.parentTabId && activeTabIds.includes(t.parentTabId));
const targetTable = activeTabTable || tableList[0];
if (targetTable) {
console.log("✅ [TableSearchWidget] 탭 전환으로 테이블 강제 선택:", {
테이블ID: targetTable.tableId,
테이블명: targetTable.tableName,
탭ID: targetTable.parentTabId,
이전테이블: selectedTableId
});
setSelectedTableId(targetTable.tableId);
}
return; // 탭 전환 시에는 여기서 종료
}
// 현재 선택된 테이블이 대상 패널에 있는지 확인
const isCurrentTableInTarget = selectedTableId && tableList.some(t => t.tableId === selectedTableId);
// 현재 선택된 테이블이 대상 패널에 없으면 대상 패널의 첫 번째 테이블 선택
// 현재 선택된 테이블이 대상 패널에 없으면 첫 번째 테이블 선택
if (!selectedTableId || !isCurrentTableInTarget) {
const targetTable = tableList[0];
setSelectedTableId(targetTable.tableId);
const activeTabTable = tableList.find(t => t.parentTabId && activeTabIds.includes(t.parentTabId));
const targetTable = activeTabTable || tableList[0];
if (targetTable && targetTable.tableId !== selectedTableId) {
console.log("✅ [TableSearchWidget] 테이블 자동 선택 (초기):", {
테이블ID: targetTable.tableId,
테이블명: targetTable.tableName,
탭ID: targetTable.parentTabId
});
setSelectedTableId(targetTable.tableId);
}
}
}, [tableList, selectedTableId, autoSelectFirstTable, setSelectedTableId, targetPanelPosition]);
}, [tableList, selectedTableId, autoSelectFirstTable, setSelectedTableId, targetPanelPosition, activeTabIdsStr, activeTabIds]);
// 현재 선택된 테이블의 탭 ID (탭별 필터 저장용)
const currentTableTabId = currentTable?.parentTabId;
@@ -196,6 +247,13 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드)
useEffect(() => {
console.log("📋 [TableSearchWidget] 필터 설정 useEffect 실행:", {
currentTable: currentTable?.tableName,
currentTableTabId,
filterMode,
selectedTableId,
컬럼수: currentTable?.columns?.length
});
if (!currentTable?.tableName) return;
// 고정 모드: presetFilters를 activeFilters로 설정
@@ -229,12 +287,20 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
return;
}
// 동적 모드: 화면별 + 탭별로 독립적인 필터 설정 불러오기
// 동적 모드: 화면별로 독립적인 필터 설정 불러오기
// 참고: FilterPanel.tsx에서도 screenId만 사용하여 저장하므로 키가 일치해야 함
const filterConfigKey = screenId
? `table_filters_${currentTable.tableName}_screen_${screenId}${currentTableTabId ? `_tab_${currentTableTabId}` : ''}`
? `table_filters_${currentTable.tableName}_screen_${screenId}`
: `table_filters_${currentTable.tableName}`;
const savedFilters = localStorage.getItem(filterConfigKey);
console.log("🔑 [TableSearchWidget] 필터 설정 키 확인:", {
filterConfigKey,
savedFilters: savedFilters ? `${savedFilters.substring(0, 100)}...` : null,
screenId,
tableName: currentTable.tableName
});
if (savedFilters) {
try {
const parsed = JSON.parse(savedFilters) as Array<{
@@ -257,6 +323,13 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
width: f.width || 200,
}));
console.log("📌 [TableSearchWidget] 필터 설정 로드:", {
filterConfigKey,
총필터수: parsed.length,
활성화필터수: activeFiltersList.length,
활성화필터: activeFiltersList.map(f => f.columnName)
});
setActiveFilters(activeFiltersList);
// 탭별 저장된 필터 값 복원
@@ -280,10 +353,19 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
}
} catch (error) {
console.error("저장된 필터 불러오기 실패:", error);
// 파싱 에러 시 필터 초기화
setActiveFilters([]);
setFilterValues({});
}
} else {
// 필터 설정이 없으면 초기화
// 필터 설정이 없으면 activeFilters와 filterValues 모두 초기화
console.log("⚠️ [TableSearchWidget] 저장된 필터 설정 없음 - 필터 초기화:", {
tableName: currentTable.tableName,
filterConfigKey
});
setActiveFilters([]);
setFilterValues({});
setSelectOptions({});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTable?.tableName, filterMode, screenId, currentTableTabId, JSON.stringify(presetFilters)]);