feat: 분할 패널 좌측 테이블에 검색/필터/그룹/컬럼가시성 기능 추가

문제:
- 분할 패널에서 검색 컴포넌트의 필터/그룹/컬럼 설정이 동작하지 않음
- 테이블 리스트 컴포넌트에 있던 로직이 분할 패널에는 없었음

해결:
1. 필터 처리:
   - leftFilters를 searchValues 형식으로 변환
   - API 호출 시 필터 조건 전달
   - 필터 변경 시 데이터 자동 재로드

2. 컬럼 가시성:
   - visibleLeftColumns useMemo 추가
   - leftColumnVisibility를 적용하여 표시할 컬럼 필터링
   - 렌더링 시 가시성 처리된 컬럼만 표시

3. 그룹화:
   - groupedLeftData useMemo 추가
   - leftGrouping 배열로 데이터를 그룹화
   - 그룹별 헤더와 카운트 표시

4. 테이블 등록:
   - columns 속성을 올바르게 참조 (displayColumns → columns)
   - 객체/문자열 타입 모두 처리
   - 화면 설정에 맞게 테이블 등록

테스트:
- 거래처 관리 화면에서 검색 컴포넌트 버튼 활성화
- 필터 설정 → 데이터 필터링 동작
- 그룹 설정 → 데이터 그룹화 동작
- 테이블 옵션 → 컬럼 가시성/순서 변경 동작
This commit is contained in:
kjs 2025-11-12 16:13:26 +09:00
parent 2dcf2c4c8e
commit 579c4b7387
1 changed files with 153 additions and 12 deletions

View File

@ -160,6 +160,66 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
return rootItems;
}, [componentConfig.leftPanel?.itemAddConfig]);
// 🔄 필터를 searchValues 형식으로 변환
const searchValues = useMemo(() => {
if (!leftFilters || leftFilters.length === 0) return {};
const values: Record<string, any> = {};
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<string, any[]>();
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<SplitPanelLayoutComponentProps>
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<SplitPanelLayoutComponentProps>
} 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<SplitPanelLayoutComponentProps>
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<SplitPanelLayoutComponentProps>
});
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<SplitPanelLayoutComponentProps>
// 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<SplitPanelLayoutComponentProps>
</div>
) : (
(() => {
// 🔧 로컬 검색 필터 적용
const filteredData = leftSearchQuery
? leftData.filter((item) => {
const searchLower = leftSearchQuery.toLowerCase();
@ -948,12 +1024,17 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
})
: 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<SplitPanelLayoutComponentProps>
align: "left" as const
}));
// 🔧 그룹화된 데이터 렌더링
if (groupedLeftData.length > 0) {
return (
<div className="overflow-auto">
{groupedLeftData.map((group, groupIdx) => (
<div key={groupIdx} className="mb-4">
<div className="bg-gray-100 px-3 py-2 font-semibold text-sm">
{group.groupKey} ({group.count})
</div>
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
{columnsToShow.map((col, idx) => (
<th
key={idx}
className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
style={{ width: col.width ? `${col.width}px` : 'auto', textAlign: col.align || "left" }}
>
{col.label}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{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 (
<tr
key={itemId}
onClick={() => handleLeftItemSelect(item)}
className={`hover:bg-accent cursor-pointer transition-colors ${
isSelected ? "bg-primary/10" : ""
}`}
>
{columnsToShow.map((col, colIdx) => (
<td
key={colIdx}
className="whitespace-nowrap px-3 py-2 text-sm text-gray-900"
style={{ textAlign: col.align || "left" }}
>
{item[col.name] !== null && item[col.name] !== undefined
? String(item[col.name])
: "-"}
</td>
))}
</tr>
);
})}
</tbody>
</table>
</div>
))}
</div>
);
}
// 🔧 일반 테이블 렌더링 (그룹화 없음)
return (
<div className="overflow-auto">
<table className="min-w-full divide-y divide-gray-200">