feat: 분할 패널 좌측 테이블에 검색/필터/그룹/컬럼가시성 기능 추가
문제: - 분할 패널에서 검색 컴포넌트의 필터/그룹/컬럼 설정이 동작하지 않음 - 테이블 리스트 컴포넌트에 있던 로직이 분할 패널에는 없었음 해결: 1. 필터 처리: - leftFilters를 searchValues 형식으로 변환 - API 호출 시 필터 조건 전달 - 필터 변경 시 데이터 자동 재로드 2. 컬럼 가시성: - visibleLeftColumns useMemo 추가 - leftColumnVisibility를 적용하여 표시할 컬럼 필터링 - 렌더링 시 가시성 처리된 컬럼만 표시 3. 그룹화: - groupedLeftData useMemo 추가 - leftGrouping 배열로 데이터를 그룹화 - 그룹별 헤더와 카운트 표시 4. 테이블 등록: - columns 속성을 올바르게 참조 (displayColumns → columns) - 객체/문자열 타입 모두 처리 - 화면 설정에 맞게 테이블 등록 테스트: - 거래처 관리 화면에서 검색 컴포넌트 버튼 활성화 - 필터 설정 → 데이터 필터링 동작 - 그룹 설정 → 데이터 그룹화 동작 - 테이블 옵션 → 컬럼 가시성/순서 변경 동작
This commit is contained in:
parent
2dcf2c4c8e
commit
579c4b7387
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in New Issue