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

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

View File

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

View File

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

View File

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