From 579c4b7387bb91e42f6d90b008af4caf2de383b7 Mon Sep 17 00:00:00 2001 From: kjs Date: Wed, 12 Nov 2025 16:13:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B6=84=ED=95=A0=20=ED=8C=A8=EB=84=90?= =?UTF-8?q?=20=EC=A2=8C=EC=B8=A1=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=20?= =?UTF-8?q?=EA=B2=80=EC=83=89/=ED=95=84=ED=84=B0/=EA=B7=B8=EB=A3=B9/?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=EA=B0=80=EC=8B=9C=EC=84=B1=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문제: - 분할 패널에서 검색 컴포넌트의 필터/그룹/컬럼 설정이 동작하지 않음 - 테이블 리스트 컴포넌트에 있던 로직이 분할 패널에는 없었음 해결: 1. 필터 처리: - leftFilters를 searchValues 형식으로 변환 - API 호출 시 필터 조건 전달 - 필터 변경 시 데이터 자동 재로드 2. 컬럼 가시성: - visibleLeftColumns useMemo 추가 - leftColumnVisibility를 적용하여 표시할 컬럼 필터링 - 렌더링 시 가시성 처리된 컬럼만 표시 3. 그룹화: - groupedLeftData useMemo 추가 - leftGrouping 배열로 데이터를 그룹화 - 그룹별 헤더와 카운트 표시 4. 테이블 등록: - columns 속성을 올바르게 참조 (displayColumns → columns) - 객체/문자열 타입 모두 처리 - 화면 설정에 맞게 테이블 등록 테스트: - 거래처 관리 화면에서 검색 컴포넌트 버튼 활성화 - 필터 설정 → 데이터 필터링 동작 - 그룹 설정 → 데이터 그룹화 동작 - 테이블 옵션 → 컬럼 가시성/순서 변경 동작 --- .../SplitPanelLayoutComponent.tsx | 165 ++++++++++++++++-- 1 file changed, 153 insertions(+), 12 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index aaf3587d..85e1f361 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -160,6 +160,66 @@ export const SplitPanelLayoutComponent: React.FC return rootItems; }, [componentConfig.leftPanel?.itemAddConfig]); + // 🔄 필터를 searchValues 형식으로 변환 + const searchValues = useMemo(() => { + if (!leftFilters || leftFilters.length === 0) return {}; + + const values: Record = {}; + leftFilters.forEach(filter => { + if (filter.value !== undefined && filter.value !== null && filter.value !== '') { + values[filter.columnName] = { + value: filter.value, + operator: filter.operator || 'contains', + }; + } + }); + return values; + }, [leftFilters]); + + // 🔄 컬럼 가시성 처리 + const visibleLeftColumns = useMemo(() => { + const displayColumns = componentConfig.leftPanel?.columns || []; + if (displayColumns.length === 0) return []; + + // columnVisibility가 있으면 가시성 적용 + if (leftColumnVisibility.length > 0) { + const visibilityMap = new Map(leftColumnVisibility.map(cv => [cv.columnName, cv.visible])); + return displayColumns.filter((col: any) => { + const colName = typeof col === 'string' ? col : (col.name || col.columnName); + return visibilityMap.get(colName) !== false; + }); + } + + return displayColumns; + }, [componentConfig.leftPanel?.columns, leftColumnVisibility]); + + // 🔄 데이터 그룹화 + const groupedLeftData = useMemo(() => { + if (!leftGrouping || leftGrouping.length === 0 || leftData.length === 0) return []; + + const grouped = new Map(); + + leftData.forEach((item) => { + // 각 그룹 컬럼의 값을 조합하여 그룹 키 생성 + const groupKey = leftGrouping.map(col => { + const value = item[col]; + // null/undefined 처리 + return value === null || value === undefined ? "(비어있음)" : String(value); + }).join(" > "); + + if (!grouped.has(groupKey)) { + grouped.set(groupKey, []); + } + grouped.get(groupKey)!.push(item); + }); + + return Array.from(grouped.entries()).map(([key, items]) => ({ + groupKey: key, + items, + count: items.length, + })); + }, [leftData, leftGrouping]); + // 좌측 데이터 로드 const loadLeftData = useCallback(async () => { const leftTableName = componentConfig.leftPanel?.tableName; @@ -167,10 +227,13 @@ export const SplitPanelLayoutComponent: React.FC setIsLoadingLeft(true); try { + // 🎯 필터 조건을 API에 전달 + const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined; + const result = await dataApi.getTableData(leftTableName, { page: 1, size: 100, - // searchTerm 제거 - 클라이언트 사이드에서 필터링 + search: filters, // 필터 조건 전달 }); // 가나다순 정렬 (좌측 패널의 표시 컬럼 기준) @@ -196,7 +259,7 @@ export const SplitPanelLayoutComponent: React.FC } finally { setIsLoadingLeft(false); } - }, [componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.relation?.leftColumn, isDesignMode, toast, buildHierarchy]); + }, [componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.relation?.leftColumn, isDesignMode, toast, buildHierarchy, searchValues]); // 우측 데이터 로드 const loadRightData = useCallback( @@ -289,10 +352,14 @@ export const SplitPanelLayoutComponent: React.FC if (!leftTableName || isDesignMode) return; const leftTableId = `split-panel-left-${component.id}`; - // 화면에 표시되는 컬럼만 사용 (displayColumns) - const displayColumns = componentConfig.leftPanel?.displayColumns || []; + // 🔧 화면에 표시되는 컬럼 사용 (columns 속성) + const configuredColumns = componentConfig.leftPanel?.columns || []; + const displayColumns = configuredColumns.map((col: any) => { + if (typeof col === 'string') return col; + return col.columnName || col.name || col; + }).filter(Boolean); - // displayColumns가 없으면 등록하지 않음 (화면에 표시되는 컬럼만 설정 가능) + // 화면에 설정된 컬럼이 없으면 등록하지 않음 if (displayColumns.length === 0) return; // 테이블명이 있으면 등록 @@ -315,7 +382,7 @@ export const SplitPanelLayoutComponent: React.FC }); return () => unregisterTable(leftTableId); - }, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.displayColumns, leftColumnLabels, component.title, isDesignMode]); + }, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, leftColumnLabels, component.title, isDesignMode]); // 우측 테이블은 검색 컴포넌트 등록 제외 (좌측 마스터 테이블만 검색 가능) // useEffect(() => { @@ -799,6 +866,14 @@ export const SplitPanelLayoutComponent: React.FC // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDesignMode, componentConfig.autoLoad]); + // 🔄 필터 변경 시 데이터 다시 로드 + useEffect(() => { + if (!isDesignMode && componentConfig.autoLoad !== false) { + loadLeftData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [leftFilters]); + // 리사이저 드래그 핸들러 const handleMouseDown = (e: React.MouseEvent) => { if (!resizable) return; @@ -938,6 +1013,7 @@ export const SplitPanelLayoutComponent: React.FC ) : ( (() => { + // 🔧 로컬 검색 필터 적용 const filteredData = leftSearchQuery ? leftData.filter((item) => { const searchLower = leftSearchQuery.toLowerCase(); @@ -948,12 +1024,17 @@ export const SplitPanelLayoutComponent: React.FC }) : leftData; - const displayColumns = componentConfig.leftPanel?.columns || []; - const columnsToShow = displayColumns.length > 0 - ? displayColumns.map(col => ({ - ...col, - label: leftColumnLabels[col.name] || col.label || col.name - })) + // 🔧 가시성 처리된 컬럼 사용 + const columnsToShow = visibleLeftColumns.length > 0 + ? visibleLeftColumns.map((col: any) => { + const colName = typeof col === 'string' ? col : (col.name || col.columnName); + return { + name: colName, + label: leftColumnLabels[colName] || (typeof col === 'object' ? col.label : null) || colName, + width: typeof col === 'object' ? col.width : 150, + align: (typeof col === 'object' ? col.align : "left") as "left" | "center" | "right" + }; + }) : Object.keys(filteredData[0] || {}).filter(key => key !== 'children' && key !== 'level').slice(0, 5).map(key => ({ name: key, label: leftColumnLabels[key] || key, @@ -961,6 +1042,66 @@ export const SplitPanelLayoutComponent: React.FC align: "left" as const })); + // 🔧 그룹화된 데이터 렌더링 + if (groupedLeftData.length > 0) { + return ( +
+ {groupedLeftData.map((group, groupIdx) => ( +
+
+ {group.groupKey} ({group.count}개) +
+ + + + {columnsToShow.map((col, idx) => ( + + ))} + + + + {group.items.map((item, idx) => { + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; + const itemId = item[sourceColumn] || item.id || item.ID || idx; + const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); + + return ( + handleLeftItemSelect(item)} + className={`hover:bg-accent cursor-pointer transition-colors ${ + isSelected ? "bg-primary/10" : "" + }`} + > + {columnsToShow.map((col, colIdx) => ( + + ))} + + ); + })} + +
+ {col.label} +
+ {item[col.name] !== null && item[col.name] !== undefined + ? String(item[col.name]) + : "-"} +
+
+ ))} +
+ ); + } + + // 🔧 일반 테이블 렌더링 (그룹화 없음) return (