diff --git a/frontend/contexts/TableOptionsContext.tsx b/frontend/contexts/TableOptionsContext.tsx index 1e991201..f49e82de 100644 --- a/frontend/contexts/TableOptionsContext.tsx +++ b/frontend/contexts/TableOptionsContext.tsx @@ -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] + [] // 의존성 없음 - 무한 루프 방지 ); /** diff --git a/frontend/lib/hooks/useEntityJoinOptimization.ts b/frontend/lib/hooks/useEntityJoinOptimization.ts index 686211dc..b15036ee 100644 --- a/frontend/lib/hooks/useEntityJoinOptimization.ts +++ b/frontend/lib/hooks/useEntityJoinOptimization.ts @@ -59,6 +59,9 @@ export function useEntityJoinOptimization(columnMeta: Record()); + + // 초기화 완료 플래그 (무한 루프 방지) + const initialLoadDone = useRef(false); // 공통 코드 카테고리 추출 (메모이제이션) const codeCategories = useMemo(() => { @@ -293,24 +296,40 @@ export function useEntityJoinOptimization(columnMeta: Record { + // 이미 초기화되었으면 스킵 (무한 루프 방지) + if (initialLoadDone.current) return; + initialLoadDone.current = true; + preloadCommonCodesOnMount(); - }, [preloadCommonCodesOnMount]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // 컬럼 메타 변경 시 필요한 코드 추가 로딩 + // 이미 로딩 중이면 스킵하여 무한 루프 방지 + const loadedCategoriesRef = useRef>(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(() => { diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 0c62cd8b..fe6bce94 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -416,6 +416,9 @@ export const DynamicComponentRenderer: React.FC = // originalData를 사용 (최초 전달된 값, formData는 계속 변경되므로 사용하면 안됨) _initialData: originalData || formData, _originalData: originalData, + // 🆕 탭 관련 정보 전달 (탭 내부의 테이블 컴포넌트에서 사용) + parentTabId: props.parentTabId, + parentTabsComponentId: props.parentTabsComponentId, }; // 렌더러가 클래스인지 함수인지 확인 diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 811e3ca3..fec61b43 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -1033,6 +1033,7 @@ export const TableListComponent: React.FC = ({ return () => { unregisterTable(tableId); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ tableId, tableConfig.selectedTable, @@ -1044,7 +1045,8 @@ export const TableListComponent: React.FC = ({ data, // 데이터 자체가 변경되면 재등록 (고유 값 조회용) totalItems, // 전체 항목 수가 변경되면 재등록 registerTable, - unregisterTable, + // unregisterTable은 의존성에서 제외 - 무한 루프 방지 + // unregisterTable 함수는 의존성이 없어 안정적임 ]); // 🎯 초기 로드 시 localStorage에서 정렬 상태 불러오기 diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx index e486338a..2bf77042 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx @@ -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(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)]);