feature/v2-unified-renewal #379

Merged
kjs merged 145 commits from feature/v2-unified-renewal into main 2026-02-03 12:11:19 +09:00
6 changed files with 183 additions and 116 deletions
Showing only changes of commit 5daef415ad - Show all commits

View File

@ -542,10 +542,12 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
if (response.success) { if (response.success) {
// 리피터 데이터 저장 이벤트 발생 (UnifiedRepeater 컴포넌트가 리스닝) // 리피터 데이터 저장 이벤트 발생 (UnifiedRepeater 컴포넌트가 리스닝)
window.dispatchEvent(new CustomEvent("repeaterSave", { window.dispatchEvent(
detail: { parentId: response.data?.id || formData.id } new CustomEvent("repeaterSave", {
})); detail: { parentId: response.data?.id || formData.id },
}),
);
toast.success("데이터가 성공적으로 저장되었습니다."); toast.success("데이터가 성공적으로 저장되었습니다.");
} else { } else {
toast.error(response.message || "저장에 실패했습니다."); toast.error(response.message || "저장에 실패했습니다.");

View File

@ -45,9 +45,6 @@ export const UnifiedList = forwardRef<HTMLDivElement, UnifiedListProps>((props,
[config.columns], [config.columns],
); );
// 디버깅: config.cardConfig 확인
console.log("📋 UnifiedList config.cardConfig:", config.cardConfig);
// TableListComponent에 전달할 component 객체 생성 // TableListComponent에 전달할 component 객체 생성
const componentObj = useMemo( const componentObj = useMemo(
() => ({ () => ({

View File

@ -1,27 +1,11 @@
import React, { import React, { createContext, useContext, useState, useCallback, useMemo, ReactNode } from "react";
createContext, import { TableRegistration, TableOptionsContextValue } from "@/types/table-options";
useContext,
useState,
useCallback,
useMemo,
ReactNode,
} from "react";
import {
TableRegistration,
TableOptionsContextValue,
} from "@/types/table-options";
import { useActiveTab } from "./ActiveTabContext"; import { useActiveTab } from "./ActiveTabContext";
const TableOptionsContext = createContext<TableOptionsContextValue | undefined>( const TableOptionsContext = createContext<TableOptionsContextValue | undefined>(undefined);
undefined
);
export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
children, const [registeredTables, setRegisteredTables] = useState<Map<string, TableRegistration>>(new Map());
}) => {
const [registeredTables, setRegisteredTables] = useState<
Map<string, TableRegistration>
>(new Map());
const [selectedTableId, setSelectedTableId] = useState<string | null>(null); const [selectedTableId, setSelectedTableId] = useState<string | null>(null);
/** /**
@ -43,7 +27,7 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
/** /**
* *
* : * :
* 1. selectedTableId를 * 1. selectedTableId를
* 2. unregister가 selectedTableId를 * 2. unregister가 selectedTableId를
*/ */
@ -54,13 +38,13 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
newMap.delete(tableId); newMap.delete(tableId);
return newMap; return newMap;
}); });
// 🚫 selectedTableId를 변경하지 않음 // 🚫 selectedTableId를 변경하지 않음
// 이유: useEffect 재실행 시 cleanup → register 순서로 호출되는데, // 이유: useEffect 재실행 시 cleanup → register 순서로 호출되는데,
// cleanup에서 selectedTableId를 null로 만들면 필터 설정이 초기화됨 // cleanup에서 selectedTableId를 null로 만들면 필터 설정이 초기화됨
// 다른 테이블이 선택되어야 하면 TableSearchWidget에서 자동 선택함 // 다른 테이블이 선택되어야 하면 TableSearchWidget에서 자동 선택함
}, },
[] // 의존성 없음 - 무한 루프 방지 [], // 의존성 없음 - 무한 루프 방지
); );
/** /**
@ -70,7 +54,7 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
(tableId: string) => { (tableId: string) => {
return registeredTables.get(tableId); return registeredTables.get(tableId);
}, },
[registeredTables] [registeredTables],
); );
/** /**
@ -99,25 +83,26 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
const getActiveTabTables = useCallback(() => { const getActiveTabTables = useCallback(() => {
const allTables = Array.from(registeredTables.values()); const allTables = Array.from(registeredTables.values());
const activeTabIds = activeTabContext.getAllActiveTabIds(); const activeTabIds = activeTabContext.getAllActiveTabIds();
// 활성 탭이 없으면 탭에 속하지 않은 테이블만 반환 // 활성 탭이 없으면 탭에 속하지 않은 테이블만 반환
if (activeTabIds.length === 0) { if (activeTabIds.length === 0) {
return allTables.filter(table => !table.parentTabId); return allTables.filter((table) => !table.parentTabId);
} }
// 활성 탭에 속한 테이블 + 탭에 속하지 않은 테이블 // 활성 탭에 속한 테이블 + 탭에 속하지 않은 테이블
return allTables.filter(table => return allTables.filter((table) => !table.parentTabId || activeTabIds.includes(table.parentTabId));
!table.parentTabId || activeTabIds.includes(table.parentTabId)
);
}, [registeredTables, activeTabContext]); }, [registeredTables, activeTabContext]);
/** /**
* *
*/ */
const getTablesForTab = useCallback((tabId: string) => { const getTablesForTab = useCallback(
const allTables = Array.from(registeredTables.values()); (tabId: string) => {
return allTables.filter(table => table.parentTabId === tabId); const allTables = Array.from(registeredTables.values());
}, [registeredTables]); return allTables.filter((table) => table.parentTabId === tabId);
},
[registeredTables],
);
return ( return (
<TableOptionsContext.Provider <TableOptionsContext.Provider
@ -142,10 +127,12 @@ export const TableOptionsProvider: React.FC<{ children: ReactNode }> = ({
* Context Hook * Context Hook
*/ */
export const useTableOptions = () => { export const useTableOptions = () => {
console.log("🔍🔍🔍 [useTableOptions] Hook 호출됨");
const context = useContext(TableOptionsContext); const context = useContext(TableOptionsContext);
console.log("🔍 [useTableOptions] context 확인:", { hasContext: !!context });
if (!context) { if (!context) {
console.error("❌ [useTableOptions] Context가 없습니다! TableOptionsProvider 외부에서 호출됨");
throw new Error("useTableOptions must be used within TableOptionsProvider"); throw new Error("useTableOptions must be used within TableOptionsProvider");
} }
return context; return context;
}; };

View File

@ -720,17 +720,53 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
}; };
// 렌더러가 클래스인지 함수인지 확인 // 렌더러가 클래스인지 함수인지 확인
if ( const isClass =
typeof NewComponentRenderer === "function" && typeof NewComponentRenderer === "function" &&
NewComponentRenderer.prototype && NewComponentRenderer.prototype &&
NewComponentRenderer.prototype.render NewComponentRenderer.prototype.render;
) {
if (componentType === "table-search-widget") {
console.log("🔍 [DynamicComponentRenderer] table-search-widget 렌더링 분기:", {
isClass,
hasPrototype: !!NewComponentRenderer.prototype,
hasRender: !!NewComponentRenderer.prototype?.render,
componentName: NewComponentRenderer.name,
componentProp: rendererProps.component,
screenId: rendererProps.screenId,
});
}
if (isClass) {
// 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속) // 클래스 기반 렌더러 (AutoRegisteringComponentRenderer 상속)
const rendererInstance = new NewComponentRenderer(rendererProps); const rendererInstance = new NewComponentRenderer(rendererProps);
return rendererInstance.render(); return rendererInstance.render();
} else { } else {
// 함수형 컴포넌트 // 함수형 컴포넌트
// refreshKey를 React key로 전달하여 컴포넌트 리마운트 강제 // refreshKey를 React key로 전달하여 컴포넌트 리마운트 강제
// 🔧 디버깅: table-search-widget인 경우 직접 호출 후 반환
if (componentType === "table-search-widget") {
console.log("🔧🔧🔧 [DynamicComponentRenderer] TableSearchWidget 직접 호출 반환");
console.log("🔧 [DynamicComponentRenderer] NewComponentRenderer 함수 확인:", {
name: NewComponentRenderer.name,
toString: NewComponentRenderer.toString().substring(0, 200),
});
try {
const result = NewComponentRenderer(rendererProps);
console.log("🔧 [DynamicComponentRenderer] TableSearchWidget 결과 상세:", {
resultType: typeof result,
type: result?.type?.name || result?.type || "unknown",
propsKeys: result?.props ? Object.keys(result.props) : [],
propsStyle: result?.props?.style,
propsChildren: typeof result?.props?.children,
});
// 직접 호출 결과를 반환
return result;
} catch (directCallError) {
console.error("❌ [DynamicComponentRenderer] TableSearchWidget 직접 호출 실패:", directCallError);
}
}
return <NewComponentRenderer key={refreshKey} {...rendererProps} />; return <NewComponentRenderer key={refreshKey} {...rendererProps} />;
} }
} }

View File

@ -342,14 +342,19 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const newSearchValues: Record<string, any> = {}; const newSearchValues: Record<string, any> = {};
filters.forEach((filter) => { filters.forEach((filter) => {
if (filter.value) { if (filter.value) {
newSearchValues[filter.columnName] = filter.value; // operator 정보도 함께 전달 (백엔드에서 equals/contains 구분)
newSearchValues[filter.columnName] = {
value: filter.value,
operator: filter.operator || "contains",
};
} }
}); });
// console.log("🔍 [TableListComponent] filters → searchValues:", { console.log("🔍 [TableListComponent] filters → searchValues:", {
// filters: filters.length, filtersCount: filters.length,
// searchValues: newSearchValues, filters: filters.map((f) => ({ col: f.columnName, op: f.operator, val: f.value })),
// }); searchValues: newSearchValues,
});
setSearchValues(newSearchValues); setSearchValues(newSearchValues);
setCurrentPage(1); // 필터 변경 시 첫 페이지로 setCurrentPage(1); // 필터 변경 시 첫 페이지로

View File

@ -50,7 +50,24 @@ interface TableSearchWidgetProps {
} }
export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) { export function TableSearchWidget({ component, screenId, onHeightChange }: TableSearchWidgetProps) {
const { registeredTables, selectedTableId, setSelectedTableId, getTable, getActiveTabTables } = useTableOptions(); console.log("🎯🎯🎯 [TableSearchWidget] 함수 시작!", { componentId: component?.id, screenId });
// 🔧 직접 useTableOptions 호출 (에러 발생 시 catch하지 않고 그대로 throw)
const tableOptionsContext = useTableOptions();
console.log("✅ [TableSearchWidget] useTableOptions 성공", { hasContext: !!tableOptionsContext });
const { registeredTables, selectedTableId, setSelectedTableId, getTable, getActiveTabTables } = tableOptionsContext;
// 등록된 테이블 확인 로그
console.log("🔍 [TableSearchWidget] 등록된 테이블:", {
count: registeredTables.size,
tables: Array.from(registeredTables.entries()).map(([id, t]) => ({
id,
tableName: t.tableName,
hasOnFilterChange: typeof t.onFilterChange === "function",
})),
selectedTableId,
});
const { isPreviewMode } = useScreenPreview(); // 미리보기 모드 확인 const { isPreviewMode } = useScreenPreview(); // 미리보기 모드 확인
const { getAllActiveTabIds, activeTabs } = useActiveTab(); // 활성 탭 정보 const { getAllActiveTabIds, activeTabs } = useActiveTab(); // 활성 탭 정보
@ -65,7 +82,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// Context가 없으면 (디자이너 모드) 무시 // Context가 없으면 (디자이너 모드) 무시
setWidgetHeight = undefined; setWidgetHeight = undefined;
} }
// 탭별 필터 값 저장 (탭 ID -> 필터 값) // 탭별 필터 값 저장 (탭 ID -> 필터 값)
const [tabFilterValues, setTabFilterValues] = useState<Record<string, Record<string, any>>>({}); const [tabFilterValues, setTabFilterValues] = useState<Record<string, Record<string, any>>>({});
@ -92,16 +109,16 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// Map을 배열로 변환 // Map을 배열로 변환
const allTableList = Array.from(registeredTables.values()); const allTableList = Array.from(registeredTables.values());
// 현재 활성 탭 ID 목록 // 현재 활성 탭 ID 목록
const activeTabIds = useMemo(() => getAllActiveTabIds(), [activeTabs]); const activeTabIds = useMemo(() => getAllActiveTabIds(), [activeTabs]);
// 대상 패널 위치 + 활성 탭에 따라 테이블 필터링 // 대상 패널 위치 + 활성 탭에 따라 테이블 필터링
const tableList = useMemo(() => { const tableList = useMemo(() => {
// 1단계: 활성 탭 기반 필터링 // 1단계: 활성 탭 기반 필터링
// - 활성 탭에 속한 테이블만 표시 // - 활성 탭에 속한 테이블만 표시
// - 탭에 속하지 않은 테이블(parentTabId가 없는)도 포함 // - 탭에 속하지 않은 테이블(parentTabId가 없는)도 포함
let filteredByTab = allTableList.filter(table => { let filteredByTab = allTableList.filter((table) => {
// 탭에 속하지 않는 테이블은 항상 표시 // 탭에 속하지 않는 테이블은 항상 표시
if (!table.parentTabId) return true; if (!table.parentTabId) return true;
// 활성 탭에 속한 테이블만 표시 // 활성 탭에 속한 테이블만 표시
@ -110,9 +127,9 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// 2단계: 대상 패널 위치에 따라 추가 필터링 // 2단계: 대상 패널 위치에 따라 추가 필터링
if (targetPanelPosition !== "auto") { if (targetPanelPosition !== "auto") {
filteredByTab = filteredByTab.filter(table => { filteredByTab = filteredByTab.filter((table) => {
const tableId = table.tableId.toLowerCase(); const tableId = table.tableId.toLowerCase();
if (targetPanelPosition === "left") { if (targetPanelPosition === "left") {
// 좌측 패널 대상: card-display만 // 좌측 패널 대상: card-display만
return tableId.includes("card-display") || tableId.includes("card"); return tableId.includes("card-display") || tableId.includes("card");
@ -121,16 +138,14 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
const isCardDisplay = tableId.includes("card-display") || tableId.includes("card"); const isCardDisplay = tableId.includes("card-display") || tableId.includes("card");
return !isCardDisplay; return !isCardDisplay;
} }
return true; return true;
}); });
} }
// 필터링된 결과가 없으면 탭 기반 필터링 결과만 반환 // 필터링된 결과가 없으면 탭 기반 필터링 결과만 반환
if (filteredByTab.length === 0) { if (filteredByTab.length === 0) {
return allTableList.filter(table => return allTableList.filter((table) => !table.parentTabId || activeTabIds.includes(table.parentTabId));
!table.parentTabId || activeTabIds.includes(table.parentTabId)
);
} }
return filteredByTab; return filteredByTab;
@ -141,18 +156,18 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
console.log("🔍 [TableSearchWidget] currentTable 계산:", { console.log("🔍 [TableSearchWidget] currentTable 계산:", {
selectedTableId, selectedTableId,
tableListLength: tableList.length, tableListLength: tableList.length,
tableList: tableList.map(t => ({ id: t.tableId, name: t.tableName, parentTabId: t.parentTabId })) 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); console.log("✅ [TableSearchWidget] 테이블 찾음 (tableList):", tableFromList.tableName);
return tableFromList; return tableFromList;
} }
// tableList에 없으면 전체에서 찾기 (폴백) // tableList에 없으면 전체에서 찾기 (폴백)
const tableFromAll = getTable(selectedTableId); const tableFromAll = getTable(selectedTableId);
console.log("🔄 [TableSearchWidget] 테이블 찾음 (전체):", tableFromAll?.tableName); console.log("🔄 [TableSearchWidget] 테이블 찾음 (전체):", tableFromAll?.tableName);
@ -161,10 +176,10 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// 🆕 활성 탭 ID 문자열 (변경 감지용) // 🆕 활성 탭 ID 문자열 (변경 감지용)
const activeTabIdsStr = useMemo(() => activeTabIds.join(","), [activeTabIds]); const activeTabIdsStr = useMemo(() => activeTabIds.join(","), [activeTabIds]);
// 🆕 이전 활성 탭 ID 추적 (탭 전환 감지용) // 🆕 이전 활성 탭 ID 추적 (탭 전환 감지용)
const prevActiveTabIdsRef = useRef<string>(activeTabIdsStr); const prevActiveTabIdsRef = useRef<string>(activeTabIdsStr);
// 대상 패널의 첫 번째 테이블 자동 선택 // 대상 패널의 첫 번째 테이블 자동 선택
useEffect(() => { useEffect(() => {
if (!autoSelectFirstTable || tableList.length === 0) { if (!autoSelectFirstTable || tableList.length === 0) {
@ -177,21 +192,21 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
console.log("🔄 [TableSearchWidget] 탭 전환 감지:", { console.log("🔄 [TableSearchWidget] 탭 전환 감지:", {
이전탭: prevActiveTabIdsRef.current, 이전탭: prevActiveTabIdsRef.current,
현재탭: activeTabIdsStr, 현재탭: activeTabIdsStr,
가용테이블: tableList.map(t => ({ id: t.tableId, tableName: t.tableName, parentTabId: t.parentTabId })), 가용테이블: tableList.map((t) => ({ id: t.tableId, tableName: t.tableName, parentTabId: t.parentTabId })),
현재선택테이블: selectedTableId 현재선택테이블: selectedTableId,
}); });
prevActiveTabIdsRef.current = activeTabIdsStr; prevActiveTabIdsRef.current = activeTabIdsStr;
// 🆕 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택 // 🆕 탭 전환 시: 해당 탭에 속한 테이블 중 첫 번째 강제 선택
const activeTabTable = tableList.find(t => t.parentTabId && activeTabIds.includes(t.parentTabId)); const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
const targetTable = activeTabTable || tableList[0]; const targetTable = activeTabTable || tableList[0];
if (targetTable) { if (targetTable) {
console.log("✅ [TableSearchWidget] 탭 전환으로 테이블 강제 선택:", { console.log("✅ [TableSearchWidget] 탭 전환으로 테이블 강제 선택:", {
테이블ID: targetTable.tableId, 테이블ID: targetTable.tableId,
테이블명: targetTable.tableName, 테이블명: targetTable.tableName,
탭ID: targetTable.parentTabId, 탭ID: targetTable.parentTabId,
이전테이블: selectedTableId 이전테이블: selectedTableId,
}); });
setSelectedTableId(targetTable.tableId); setSelectedTableId(targetTable.tableId);
} }
@ -199,30 +214,38 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
} }
// 현재 선택된 테이블이 대상 패널에 있는지 확인 // 현재 선택된 테이블이 대상 패널에 있는지 확인
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 activeTabTable = tableList.find(t => t.parentTabId && activeTabIds.includes(t.parentTabId)); const activeTabTable = tableList.find((t) => t.parentTabId && activeTabIds.includes(t.parentTabId));
const targetTable = activeTabTable || tableList[0]; const targetTable = activeTabTable || tableList[0];
if (targetTable && targetTable.tableId !== selectedTableId) { if (targetTable && targetTable.tableId !== selectedTableId) {
console.log("✅ [TableSearchWidget] 테이블 자동 선택 (초기):", { console.log("✅ [TableSearchWidget] 테이블 자동 선택 (초기):", {
테이블ID: targetTable.tableId, 테이블ID: targetTable.tableId,
테이블명: targetTable.tableName, 테이블명: targetTable.tableName,
탭ID: targetTable.parentTabId 탭ID: targetTable.parentTabId,
}); });
setSelectedTableId(targetTable.tableId); setSelectedTableId(targetTable.tableId);
} }
} }
}, [tableList, selectedTableId, autoSelectFirstTable, setSelectedTableId, targetPanelPosition, activeTabIdsStr, activeTabIds]); }, [
tableList,
selectedTableId,
autoSelectFirstTable,
setSelectedTableId,
targetPanelPosition,
activeTabIdsStr,
activeTabIds,
]);
// 현재 선택된 테이블의 탭 ID (탭별 필터 저장용) // 현재 선택된 테이블의 탭 ID (탭별 필터 저장용)
const currentTableTabId = currentTable?.parentTabId; const currentTableTabId = currentTable?.parentTabId;
// 탭별 필터 값 저장 키 생성 // 탭별 필터 값 저장 키 생성
const getTabFilterStorageKey = (tableName: string, tabId?: string) => { const getTabFilterStorageKey = (tableName: string, tabId?: string) => {
const baseKey = screenId const baseKey = screenId
? `table_filter_values_${tableName}_screen_${screenId}` ? `table_filter_values_${tableName}_screen_${screenId}`
: `table_filter_values_${tableName}`; : `table_filter_values_${tableName}`;
return tabId ? `${baseKey}_tab_${tabId}` : baseKey; return tabId ? `${baseKey}_tab_${tabId}` : baseKey;
@ -231,16 +254,16 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// 탭 변경 시 이전 탭의 필터 값 저장 + 새 탭의 필터 값 복원 // 탭 변경 시 이전 탭의 필터 값 저장 + 새 탭의 필터 값 복원
useEffect(() => { useEffect(() => {
if (!currentTable?.tableName) return; if (!currentTable?.tableName) return;
// 현재 필터 값이 있으면 탭별로 저장 // 현재 필터 값이 있으면 탭별로 저장
if (Object.keys(filterValues).length > 0 && currentTableTabId) { if (Object.keys(filterValues).length > 0 && currentTableTabId) {
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
localStorage.setItem(storageKey, JSON.stringify(filterValues)); localStorage.setItem(storageKey, JSON.stringify(filterValues));
// 메모리 캐시에도 저장 // 메모리 캐시에도 저장
setTabFilterValues(prev => ({ setTabFilterValues((prev) => ({
...prev, ...prev,
[currentTableTabId]: filterValues [currentTableTabId]: filterValues,
})); }));
} }
}, [currentTableTabId, currentTable?.tableName]); }, [currentTableTabId, currentTable?.tableName]);
@ -252,7 +275,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
currentTableTabId, currentTableTabId,
filterMode, filterMode,
selectedTableId, selectedTableId,
컬럼수: currentTable?.columns?.length 컬럼수: currentTable?.columns?.length,
}); });
if (!currentTable?.tableName) return; if (!currentTable?.tableName) return;
@ -266,7 +289,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
width: f.width || 200, width: f.width || 200,
})); }));
setActiveFilters(activeFiltersList); setActiveFilters(activeFiltersList);
// 탭별 저장된 필터 값 복원 // 탭별 저장된 필터 값 복원
if (currentTableTabId) { if (currentTableTabId) {
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
@ -289,7 +312,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// 동적 모드: 화면별로 독립적인 필터 설정 불러오기 // 동적 모드: 화면별로 독립적인 필터 설정 불러오기
// 참고: FilterPanel.tsx에서도 screenId만 사용하여 저장하므로 키가 일치해야 함 // 참고: FilterPanel.tsx에서도 screenId만 사용하여 저장하므로 키가 일치해야 함
const filterConfigKey = screenId const filterConfigKey = screenId
? `table_filters_${currentTable.tableName}_screen_${screenId}` ? `table_filters_${currentTable.tableName}_screen_${screenId}`
: `table_filters_${currentTable.tableName}`; : `table_filters_${currentTable.tableName}`;
const savedFilters = localStorage.getItem(filterConfigKey); const savedFilters = localStorage.getItem(filterConfigKey);
@ -298,7 +321,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
filterConfigKey, filterConfigKey,
savedFilters: savedFilters ? `${savedFilters.substring(0, 100)}...` : null, savedFilters: savedFilters ? `${savedFilters.substring(0, 100)}...` : null,
screenId, screenId,
tableName: currentTable.tableName tableName: currentTable.tableName,
}); });
if (savedFilters) { if (savedFilters) {
@ -327,11 +350,11 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
filterConfigKey, filterConfigKey,
총필터수: parsed.length, 총필터수: parsed.length,
활성화필터수: activeFiltersList.length, 활성화필터수: activeFiltersList.length,
활성화필터: activeFiltersList.map(f => f.columnName) 활성화필터: activeFiltersList.map((f) => f.columnName),
}); });
setActiveFilters(activeFiltersList); setActiveFilters(activeFiltersList);
// 탭별 저장된 필터 값 복원 // 탭별 저장된 필터 값 복원
if (currentTableTabId) { if (currentTableTabId) {
const valuesStorageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); const valuesStorageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
@ -361,7 +384,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
// 필터 설정이 없으면 activeFilters와 filterValues 모두 초기화 // 필터 설정이 없으면 activeFilters와 filterValues 모두 초기화
console.log("⚠️ [TableSearchWidget] 저장된 필터 설정 없음 - 필터 초기화:", { console.log("⚠️ [TableSearchWidget] 저장된 필터 설정 없음 - 필터 초기화:", {
tableName: currentTable.tableName, tableName: currentTable.tableName,
filterConfigKey filterConfigKey,
}); });
setActiveFilters([]); setActiveFilters([]);
setFilterValues({}); setFilterValues({});
@ -482,21 +505,26 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
const filtersWithValues = activeFilters const filtersWithValues = activeFilters
.map((filter) => { .map((filter) => {
let filterValue = values[filter.columnName]; let filterValue = values[filter.columnName];
// 날짜 범위 객체를 처리 // 날짜 범위 객체를 처리
if (filter.filterType === "date" && filterValue && typeof filterValue === "object" && (filterValue.from || filterValue.to)) { if (
filter.filterType === "date" &&
filterValue &&
typeof filterValue === "object" &&
(filterValue.from || filterValue.to)
) {
// 날짜 범위 객체를 문자열 형식으로 변환 (백엔드 재시작 불필요) // 날짜 범위 객체를 문자열 형식으로 변환 (백엔드 재시작 불필요)
const formatDate = (date: Date) => { const formatDate = (date: Date) => {
const year = date.getFullYear(); const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}; };
// "YYYY-MM-DD|YYYY-MM-DD" 형식으로 변환 // "YYYY-MM-DD|YYYY-MM-DD" 형식으로 변환
const fromStr = filterValue.from ? formatDate(filterValue.from) : ""; const fromStr = filterValue.from ? formatDate(filterValue.from) : "";
const toStr = filterValue.to ? formatDate(filterValue.to) : ""; const toStr = filterValue.to ? formatDate(filterValue.to) : "";
if (fromStr && toStr) { if (fromStr && toStr) {
// 둘 다 있으면 파이프로 연결 // 둘 다 있으면 파이프로 연결
filterValue = `${fromStr}|${toStr}`; filterValue = `${fromStr}|${toStr}`;
@ -510,12 +538,12 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
filterValue = ""; filterValue = "";
} }
} }
// 다중선택 배열을 처리 (파이프로 연결된 문자열로 변환) // 다중선택 배열을 처리 (파이프로 연결된 문자열로 변환)
if (filter.filterType === "select" && Array.isArray(filterValue)) { if (filter.filterType === "select" && Array.isArray(filterValue)) {
filterValue = filterValue.join("|"); filterValue = filterValue.join("|");
} }
return { return {
...filter, ...filter,
value: filterValue || "", value: filterValue || "",
@ -529,7 +557,23 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
return true; return true;
}); });
currentTable?.onFilterChange(filtersWithValues); console.log("🔍 [TableSearchWidget] applyFilters 호출:", {
currentTableId: currentTable?.tableId,
currentTableName: currentTable?.tableName,
hasOnFilterChange: !!currentTable?.onFilterChange,
filtersCount: filtersWithValues.length,
filters: filtersWithValues.map((f) => ({
col: f.columnName,
op: f.operator,
val: f.value,
})),
});
if (currentTable?.onFilterChange) {
currentTable.onFilterChange(filtersWithValues);
} else {
console.warn("⚠️ [TableSearchWidget] onFilterChange가 없음!", { currentTable });
}
}; };
// 필터 초기화 // 필터 초기화
@ -537,7 +581,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
setFilterValues({}); setFilterValues({});
setSelectedLabels({}); setSelectedLabels({});
currentTable?.onFilterChange([]); currentTable?.onFilterChange([]);
// 탭별 저장된 필터 값도 초기화 // 탭별 저장된 필터 값도 초기화
if (currentTable?.tableName && currentTableTabId) { if (currentTable?.tableName && currentTableTabId) {
const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId); const storageKey = getTabFilterStorageKey(currentTable.tableName, currentTableTabId);
@ -557,7 +601,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
<div style={{ width: `${width}px` }}> <div style={{ width: `${width}px` }}>
<ModernDatePicker <ModernDatePicker
label={column?.columnLabel || filter.columnName} label={column?.columnLabel || filter.columnName}
value={value ? (typeof value === 'string' ? { from: new Date(value), to: new Date(value) } : value) : {}} value={value ? (typeof value === "string" ? { from: new Date(value), to: new Date(value) } : value) : {}}
onChange={(dateRange) => { onChange={(dateRange) => {
if (dateRange.from && dateRange.to) { if (dateRange.from && dateRange.to) {
// 기간이 선택되면 from과 to를 모두 저장 // 기간이 선택되면 from과 to를 모두 저장
@ -584,7 +628,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
); );
case "select": { case "select": {
let options = selectOptions[filter.columnName] || []; const options = selectOptions[filter.columnName] || [];
// 중복 제거 (value 기준) // 중복 제거 (value 기준)
const uniqueOptions = options.reduce( const uniqueOptions = options.reduce(
@ -598,13 +642,13 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
); );
// 항상 다중선택 모드 // 항상 다중선택 모드
const selectedValues: string[] = Array.isArray(value) ? value : (value ? [value] : []); const selectedValues: string[] = Array.isArray(value) ? value : value ? [value] : [];
// 선택된 값들의 라벨 표시 // 선택된 값들의 라벨 표시
const getDisplayText = () => { const getDisplayText = () => {
if (selectedValues.length === 0) return column?.columnLabel || "선택"; if (selectedValues.length === 0) return column?.columnLabel || "선택";
if (selectedValues.length === 1) { if (selectedValues.length === 1) {
const opt = uniqueOptions.find(o => o.value === selectedValues[0]); const opt = uniqueOptions.find((o) => o.value === selectedValues[0]);
return opt?.label || selectedValues[0]; return opt?.label || selectedValues[0];
} }
return `${selectedValues.length}개 선택됨`; return `${selectedValues.length}개 선택됨`;
@ -615,7 +659,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
if (checked) { if (checked) {
newValues = [...selectedValues, optionValue]; newValues = [...selectedValues, optionValue];
} else { } else {
newValues = selectedValues.filter(v => v !== optionValue); newValues = selectedValues.filter((v) => v !== optionValue);
} }
handleFilterChange(filter.columnName, newValues.length > 0 ? newValues : ""); handleFilterChange(filter.columnName, newValues.length > 0 ? newValues : "");
}; };
@ -628,7 +672,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
role="combobox" role="combobox"
className={cn( className={cn(
"h-9 min-h-9 justify-between text-xs font-normal focus:ring-0 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 sm:text-sm", "h-9 min-h-9 justify-between text-xs font-normal focus:ring-0 focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 sm:text-sm",
selectedValues.length === 0 && "text-muted-foreground" selectedValues.length === 0 && "text-muted-foreground",
)} )}
style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }} style={{ width: `${width}px`, height: "36px", minHeight: "36px", outline: "none", boxShadow: "none" }}
> >
@ -636,11 +680,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent className="p-0" style={{ width: `${width}px` }} align="start">
className="p-0"
style={{ width: `${width}px` }}
align="start"
>
<div className="max-h-60 overflow-auto"> <div className="max-h-60 overflow-auto">
{uniqueOptions.length === 0 ? ( {uniqueOptions.length === 0 ? (
<div className="text-muted-foreground px-3 py-2 text-xs"> </div> <div className="text-muted-foreground px-3 py-2 text-xs"> </div>
@ -649,7 +689,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
{uniqueOptions.map((option, index) => ( {uniqueOptions.map((option, index) => (
<div <div
key={`${filter.columnName}-multi-${option.value}-${index}`} key={`${filter.columnName}-multi-${option.value}-${index}`}
className="flex items-center space-x-2 rounded-sm px-2 py-1.5 hover:bg-accent cursor-pointer" className="hover:bg-accent flex cursor-pointer items-center space-x-2 rounded-sm px-2 py-1.5"
onClick={() => handleMultiSelectChange(option.value, !selectedValues.includes(option.value))} onClick={() => handleMultiSelectChange(option.value, !selectedValues.includes(option.value))}
> >
<Checkbox <Checkbox
@ -668,7 +708,7 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="w-full h-7 text-xs" className="h-7 w-full text-xs"
onClick={() => handleFilterChange(filter.columnName, "")} onClick={() => handleFilterChange(filter.columnName, "")}
> >