diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 9e0915ee..e1242afd 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -1290,6 +1290,11 @@ export class DynamicFormService { return res.rows; }); + // 삭제된 행이 없으면 레코드를 찾을 수 없는 것 + if (!result || !Array.isArray(result) || result.length === 0) { + throw new Error(`테이블 ${tableName}에서 ID '${id}'에 해당하는 레코드를 찾을 수 없습니다.`); + } + console.log("✅ 서비스: 실제 테이블 삭제 성공:", result); // 🔥 조건부 연결 실행 (DELETE 트리거) diff --git a/frontend/components/screen/widgets/TabsWidget.tsx b/frontend/components/screen/widgets/TabsWidget.tsx index 6b0d0864..8f29e874 100644 --- a/frontend/components/screen/widgets/TabsWidget.tsx +++ b/frontend/components/screen/widgets/TabsWidget.tsx @@ -135,6 +135,8 @@ export function TabsWidget({ const [screenLayouts, setScreenLayouts] = useState>({}); const [screenLoadingStates, setScreenLoadingStates] = useState>({}); const [screenErrors, setScreenErrors] = useState>({}); + // 탭별 화면 정보 (screenId, tableName) 저장 + const [screenInfoMap, setScreenInfoMap] = useState>({}); // 컴포넌트 탭 목록 변경 시 동기화 useEffect(() => { @@ -155,10 +157,21 @@ export function TabsWidget({ ) { setScreenLoadingStates((prev) => ({ ...prev, [tab.id]: true })); try { - const layoutData = await screenApi.getLayout(extTab.screenId); + // 레이아웃과 화면 정보를 병렬로 로드 + const [layoutData, screenDef] = await Promise.all([ + screenApi.getLayout(extTab.screenId), + screenApi.getScreen(extTab.screenId), + ]); if (layoutData && layoutData.components) { setScreenLayouts((prev) => ({ ...prev, [tab.id]: layoutData.components })); } + // 탭의 화면 정보 저장 (tableName 포함) + if (screenDef) { + setScreenInfoMap((prev) => ({ + ...prev, + [tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName }, + })); + } } catch (error) { console.error(`탭 "${tab.label}" 화면 로드 실패:`, error); setScreenErrors((prev) => ({ ...prev, [tab.id]: "화면을 불러올 수 없습니다." })); @@ -172,6 +185,31 @@ export function TabsWidget({ loadScreenLayouts(); }, [visibleTabs, screenLayouts, screenLoadingStates]); + // screenInfoMap이 없는 탭의 화면 정보 보충 로드 + // screenId가 있지만 screenInfoMap에 아직 없는 탭의 화면 정보를 로드 + useEffect(() => { + const loadMissingScreenInfo = async () => { + for (const tab of visibleTabs) { + const extTab = tab as ExtendedTabItem; + // screenId가 있고 screenInfoMap에 아직 없는 경우 로드 + if (extTab.screenId && !screenInfoMap[tab.id]) { + try { + const screenDef = await screenApi.getScreen(extTab.screenId); + if (screenDef) { + setScreenInfoMap((prev) => ({ + ...prev, + [tab.id]: { id: extTab.screenId!, tableName: screenDef.tableName }, + })); + } + } catch (error) { + console.error(`탭 "${tab.label}" 화면 정보 로드 실패:`, error); + } + } + } + }; + loadMissingScreenInfo(); + }, [visibleTabs, screenInfoMap]); + // 선택된 탭 변경 시 localStorage에 저장 + ActiveTab Context 업데이트 useEffect(() => { if (persistSelection && typeof window !== "undefined") { @@ -256,7 +294,7 @@ export function TabsWidget({ // 화면 레이아웃이 로드된 경우 const loadedComponents = screenLayouts[tab.id]; if (loadedComponents && loadedComponents.length > 0) { - return renderScreenComponents(loadedComponents); + return renderScreenComponents(tab, loadedComponents); } // 아직 로드되지 않은 경우 @@ -283,7 +321,7 @@ export function TabsWidget({ }; // screenId로 로드한 화면 컴포넌트 렌더링 - const renderScreenComponents = (components: ComponentData[]) => { + const renderScreenComponents = (tab: ExtendedTabItem, components: ComponentData[]) => { // InteractiveScreenViewerDynamic 동적 로드 const InteractiveScreenViewerDynamic = require("@/components/screen/InteractiveScreenViewerDynamic").InteractiveScreenViewerDynamic; @@ -316,7 +354,10 @@ export function TabsWidget({ allComponents={components} formData={formData} onFormDataChange={onFormDataChange} + screenInfo={screenInfoMap[tab.id]} menuObjid={menuObjid} + parentTabId={tab.id} + parentTabsComponentId={component.id} /> ))} @@ -325,7 +366,7 @@ export function TabsWidget({ }; // 인라인 컴포넌트 렌더링 (v2 방식) - const renderInlineComponents = (tab: TabItem, components: TabInlineComponent[]) => { + const renderInlineComponents = (tab: ExtendedTabItem, components: TabInlineComponent[]) => { // 컴포넌트들의 최대 위치를 계산하여 스크롤 가능한 영역 확보 const maxBottom = Math.max( ...components.map((c) => (c.position?.y || 0) + (c.size?.height || 100)), @@ -386,6 +427,15 @@ export function TabsWidget({ isInteractive={!isDesignMode} selectedRowsData={localSelectedRowsData} onSelectedRowsChange={handleSelectedRowsChange} + parentTabId={tab.id} + parentTabsComponentId={component.id} + // 탭에 screenId가 있으면 해당 화면의 tableName/screenId로 오버라이드 + {...(screenInfoMap[tab.id] + ? { + tableName: screenInfoMap[tab.id].tableName, + screenId: screenInfoMap[tab.id].id, + } + : {})} /> ); diff --git a/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx index 94d0c742..061ac6f0 100644 --- a/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/v2-table-search-widget/TableSearchWidget.tsx @@ -154,15 +154,23 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table // 대상 패널의 첫 번째 테이블 자동 선택 useEffect(() => { - if (!autoSelectFirstTable || tableList.length === 0) { + if (!autoSelectFirstTable) { return; } // 탭 전환 감지: 활성 탭이 변경되었는지 확인 const tabChanged = prevActiveTabIdsRef.current !== activeTabIdsStr; if (tabChanged) { + // 탭이 변경되면 항상 ref를 갱신 (tableList가 비어 있어도) + // 이렇게 해야 비동기로 tableList가 나중에 채워질 때 중복 감지하지 않음 prevActiveTabIdsRef.current = activeTabIdsStr; + if (tableList.length === 0) { + // 테이블이 아직 등록되지 않은 상태 (비동기 로드 중) + // tableList가 나중에 채워지면 아래 폴백 로직에서 처리됨 + return; + } + // 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택 const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId)); const targetTable = activeTabTable || tableList[0]; @@ -173,11 +181,21 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table return; // 탭 전환 시에는 여기서 종료 } - // 현재 선택된 테이블이 대상 패널에 있는지 확인 - const isCurrentTableInTarget = selectedTableId && tableList.some((t) => t.tableId === selectedTableId); + // tableList가 비어있으면 아래 로직 스킵 + if (tableList.length === 0) { + return; + } - // 현재 선택된 테이블이 대상 패널에 없으면 첫 번째 테이블 선택 - if (!selectedTableId || !isCurrentTableInTarget) { + // 현재 선택된 테이블이 활성 탭에 속하는지 확인 + const isCurrentTableInActiveTab = selectedTableId && tableList.some((t) => { + if (t.tableId !== selectedTableId) return false; + // parentTabId가 있는 테이블이면 활성 탭에 속하는지 확인 + if (t.parentTabId) return activeTabIds.includes(t.parentTabId); + return true; // parentTabId 없는 전역 테이블은 항상 유효 + }); + + // 현재 선택된 테이블이 활성 탭에 없거나 미선택이면 첫 번째 테이블 선택 + if (!selectedTableId || !isCurrentTableInActiveTab) { const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId)); const targetTable = activeTabTable || tableList[0]; @@ -223,6 +241,102 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table } }, [currentTableTabId, currentTable?.tableName]); + // 탭 전환 플래그 (탭 복귀 시 필터 재적용을 위해) + const needsFilterReapplyRef = useRef(false); + const prevActiveTabIdsForReapplyRef = useRef(activeTabIdsStr); + + // 탭 전환 감지: 플래그만 설정 (실제 적용은 currentTable이 준비된 후) + useEffect(() => { + if (prevActiveTabIdsForReapplyRef.current !== activeTabIdsStr) { + prevActiveTabIdsForReapplyRef.current = activeTabIdsStr; + needsFilterReapplyRef.current = true; + } + }, [activeTabIdsStr]); + + // 탭 복귀 시 기존 필터값 재적용 + // currentTable이 준비되고 필터값이 있을 때 실행 + useEffect(() => { + if (!needsFilterReapplyRef.current) return; + if (!currentTable?.onFilterChange) return; + + // 플래그 즉시 해제 (중복 실행 방지) + needsFilterReapplyRef.current = false; + + // activeFilters와 filterValues가 있으면 직접 onFilterChange 호출 + // applyFilters 클로저 의존성을 피하고 직접 계산 + if (activeFilters.length === 0) return; + + const hasValues = Object.values(filterValues).some( + (v) => v !== "" && v !== undefined && v !== null, + ); + if (!hasValues) return; + + const filtersWithValues = activeFilters + .map((filter) => { + let filterValue = filterValues[filter.columnName]; + + // 날짜 범위 객체 처리 + if ( + filter.filterType === "date" && + filterValue && + typeof filterValue === "object" && + (filterValue.from || filterValue.to) + ) { + const formatDate = (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + const fromStr = filterValue.from ? formatDate(filterValue.from) : ""; + const toStr = filterValue.to ? formatDate(filterValue.to) : ""; + if (fromStr && toStr) filterValue = `${fromStr}|${toStr}`; + else if (fromStr) filterValue = `${fromStr}|`; + else if (toStr) filterValue = `|${toStr}`; + else filterValue = ""; + } + + // 배열 처리 + if (Array.isArray(filterValue)) { + filterValue = filterValue.join("|"); + } + + let operator = "contains"; + if (filter.filterType === "select") operator = "equals"; + else if (filter.filterType === "number") operator = "equals"; + + return { + ...filter, + value: filterValue || "", + operator, + }; + }) + .filter((f) => { + if (!f.value) return false; + if (typeof f.value === "string" && f.value === "") return false; + return true; + }); + + // 직접 onFilterChange 호출 (applyFilters 클로저 우회) + currentTable.onFilterChange(filtersWithValues); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentTable?.onFilterChange, currentTable?.tableName, activeFilters, filterValues]); + + // 필터 적용을 다음 렌더 사이클로 지연 (activeFilters 업데이트 후 적용 보장) + const pendingFilterApplyRef = useRef<{ values: Record; tableName: string } | null>(null); + + useEffect(() => { + if (pendingFilterApplyRef.current) { + const { values, tableName } = pendingFilterApplyRef.current; + // 현재 테이블이 요청된 테이블과 일치하는지 확인 (탭이 빠르게 전환된 경우 방지) + if (currentTable?.tableName === tableName) { + applyFilters(values); + } + pendingFilterApplyRef.current = null; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeFilters, currentTable?.tableName]); + // 현재 테이블의 저장된 필터 불러오기 (동적 모드) 또는 고정 필터 적용 (고정 모드) useEffect(() => { if (!currentTable?.tableName) return; @@ -246,8 +360,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table try { const parsedValues = JSON.parse(savedValues); setFilterValues(parsedValues); - // 즉시 필터 적용 - setTimeout(() => applyFilters(parsedValues), 100); + // 다음 렌더 사이클에서 필터 적용 (activeFilters 업데이트 후) + pendingFilterApplyRef.current = { values: parsedValues, tableName: currentTable.tableName }; } catch { setFilterValues({}); } @@ -297,8 +411,8 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table try { const parsedValues = JSON.parse(savedValues); setFilterValues(parsedValues); - // 즉시 필터 적용 - setTimeout(() => applyFilters(parsedValues), 100); + // 다음 렌더 사이클에서 필터 적용 (activeFilters 업데이트 후) + pendingFilterApplyRef.current = { values: parsedValues, tableName: currentTable.tableName }; } catch { setFilterValues({}); }