feat: Enhance SplitPanelLayoutComponent with improved data loading and filtering logic

- Updated loadRightData function to support loading all data when no leftItem is selected, applying data filters as needed.
- Enhanced loadTabData function to handle data loading for tabs, including support for data filters and entity joins.
- Improved comments for clarity on data loading behavior based on leftItem selection.
- Refactored UI components in SplitPanelLayoutConfigPanel for better styling and organization, including updates to table selection and display settings.
This commit is contained in:
DDD1542
2026-02-11 10:46:47 +09:00
parent 9785f098d8
commit ced25c9a54
2 changed files with 461 additions and 403 deletions

View File

@@ -1091,7 +1091,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
searchValues,
]);
// 우측 데이터 로드
// 우측 데이터 로드 (leftItem이 null이면 전체 데이터 로드)
const loadRightData = useCallback(
async (leftItem: any) => {
const relationshipType = componentConfig.rightPanel?.relation?.type || "detail";
@@ -1099,10 +1099,84 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
if (!rightTableName || isDesignMode) return;
// 좌측 미선택 시: 전체 데이터 로드 (dataFilter 적용)
if (!leftItem && relationshipType === "join") {
setIsLoadingRight(true);
try {
const rightJoinColumns = extractAdditionalJoinColumns(
componentConfig.rightPanel?.columns,
rightTableName,
);
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
enableEntityJoin: true,
size: 1000,
companyCodeOverride: companyCode,
additionalJoinColumns: rightJoinColumns,
dataFilter: componentConfig.rightPanel?.dataFilter,
});
// dataFilter 적용
let filteredData = result.data || [];
const dataFilter = componentConfig.rightPanel?.dataFilter;
if (dataFilter?.enabled && dataFilter.filters?.length > 0) {
filteredData = filteredData.filter((item: any) => {
return dataFilter.filters.every((cond: any) => {
const value = item[cond.columnName];
switch (cond.operator) {
case "equals":
return value === cond.value;
case "notEquals":
return value !== cond.value;
case "contains":
return String(value || "").includes(String(cond.value));
case "is_null":
return value === null || value === undefined || value === "";
case "is_not_null":
return value !== null && value !== undefined && value !== "";
default:
return true;
}
});
});
}
// conditions 형식 dataFilter도 지원 (하위 호환성)
const dataFilterConditions = componentConfig.rightPanel?.dataFilter;
if (dataFilterConditions?.enabled && dataFilterConditions.conditions?.length > 0) {
filteredData = filteredData.filter((item: any) => {
return dataFilterConditions.conditions.every((cond: any) => {
const value = item[cond.column];
switch (cond.operator) {
case "equals":
return value === cond.value;
case "notEquals":
return value !== cond.value;
case "contains":
return String(value || "").includes(String(cond.value));
default:
return true;
}
});
});
}
setRightData(filteredData);
} catch (error) {
console.error("우측 전체 데이터 로드 실패:", error);
} finally {
setIsLoadingRight(false);
}
return;
}
// leftItem이 null이면 join 모드 이외에는 데이터 로드 불가
if (!leftItem) return;
setIsLoadingRight(true);
try {
if (relationshipType === "detail") {
// 상세 모드: 동일 테이블의 상세 정보 (🆕 엔티티 조인 활성화)
// 상세 모드: 동일 테이블의 상세 정보 (엔티티 조인 활성화)
const primaryKey = leftItem.id || leftItem.ID || Object.values(leftItem)[0];
// 🆕 엔티티 조인 API 사용
@@ -1331,11 +1405,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
],
);
// 추가 탭 데이터 로딩 함수
// 추가 탭 데이터 로딩 함수 (leftItem이 null이면 전체 데이터 로드)
const loadTabData = useCallback(
async (tabIndex: number, leftItem: any) => {
const tabConfig = componentConfig.rightPanel?.additionalTabs?.[tabIndex - 1];
if (!tabConfig || !leftItem || isDesignMode) return;
if (!tabConfig || isDesignMode) return;
const tabTableName = tabConfig.tableName;
if (!tabTableName) return;
@@ -1346,7 +1420,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn;
const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn;
// 🆕 탭 config의 Entity 조인 컬럼 추출
// 탭 config의 Entity 조인 컬럼 추출
const tabJoinColumns = extractAdditionalJoinColumns(tabConfig.columns, tabTableName);
if (tabJoinColumns) {
console.log(`🔗 [분할패널] 탭 ${tabIndex} additionalJoinColumns:`, tabJoinColumns);
@@ -1354,7 +1428,20 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
let resultData: any[] = [];
if (leftColumn && rightColumn) {
// 탭의 dataFilter (API 전달용)
const tabDataFilterForApi = (tabConfig as any).dataFilter;
if (!leftItem) {
// 좌측 미선택: 전체 데이터 로드 (dataFilter는 API에 전달)
const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
enableEntityJoin: true,
size: 1000,
companyCodeOverride: companyCode,
additionalJoinColumns: tabJoinColumns,
dataFilter: tabDataFilterForApi,
});
resultData = result.data || [];
} else if (leftColumn && rightColumn) {
const searchConditions: Record<string, any> = {};
if (keys && keys.length > 0) {
@@ -1380,18 +1467,46 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
search: searchConditions,
enableEntityJoin: true,
size: 1000,
additionalJoinColumns: tabJoinColumns, // 🆕 Entity 조인 컬럼 전달
companyCodeOverride: companyCode,
additionalJoinColumns: tabJoinColumns,
dataFilter: tabDataFilterForApi,
});
resultData = result.data || [];
} else {
const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
enableEntityJoin: true,
size: 1000,
additionalJoinColumns: tabJoinColumns, // 🆕 Entity 조인 컬럼 전달
companyCodeOverride: companyCode,
additionalJoinColumns: tabJoinColumns,
dataFilter: tabDataFilterForApi,
});
resultData = result.data || [];
}
// 탭별 dataFilter 적용
const tabDataFilter = (tabConfig as any).dataFilter;
if (tabDataFilter?.enabled && tabDataFilter.filters?.length > 0) {
resultData = resultData.filter((item: any) => {
return tabDataFilter.filters.every((cond: any) => {
const value = item[cond.columnName];
switch (cond.operator) {
case "equals":
return value === cond.value;
case "notEquals":
return value !== cond.value;
case "contains":
return String(value || "").includes(String(cond.value));
case "is_null":
return value === null || value === undefined || value === "";
case "is_not_null":
return value !== null && value !== undefined && value !== "";
default:
return true;
}
});
});
}
setTabsData((prev) => ({ ...prev, [tabIndex]: resultData }));
} catch (error) {
console.error(`추가탭 ${tabIndex} 데이터 로드 실패:`, error);
@@ -1407,29 +1522,55 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
[componentConfig.rightPanel?.additionalTabs, isDesignMode, toast],
);
// 탭 변경 핸들러
// 탭 변경 핸들러 (좌측 미선택 시에도 전체 데이터 로드)
const handleTabChange = useCallback(
(newTabIndex: number) => {
setActiveTabIndex(newTabIndex);
if (selectedLeftItem) {
if (newTabIndex === 0) {
if (!rightData || (Array.isArray(rightData) && rightData.length === 0)) {
loadRightData(selectedLeftItem);
}
} else {
if (!tabsData[newTabIndex]) {
loadTabData(newTabIndex, selectedLeftItem);
}
if (newTabIndex === 0) {
if (!rightData || (Array.isArray(rightData) && rightData.length === 0)) {
loadRightData(selectedLeftItem);
}
} else {
if (!tabsData[newTabIndex]) {
loadTabData(newTabIndex, selectedLeftItem);
}
}
},
[selectedLeftItem, rightData, tabsData, loadRightData, loadTabData],
);
// 좌측 항목 선택 핸들러
// 좌측 항목 선택 핸들러 (동일 항목 재클릭 시 선택 해제 → 전체 데이터 표시)
const handleLeftItemSelect = useCallback(
(item: any) => {
// 동일 항목 클릭 시 선택 해제 (전체 보기로 복귀)
const leftPk = componentConfig.rightPanel?.relation?.leftColumn ||
componentConfig.rightPanel?.relation?.keys?.[0]?.leftColumn;
const isSameItem = selectedLeftItem && leftPk &&
selectedLeftItem[leftPk] === item[leftPk];
if (isSameItem) {
// 선택 해제 → 전체 데이터 로드
setSelectedLeftItem(null);
setExpandedRightItems(new Set());
setTabsData({});
if (activeTabIndex === 0) {
loadRightData(null);
} else {
loadTabData(activeTabIndex, null);
}
// 추가 탭들도 전체 데이터 로드
const tabs = componentConfig.rightPanel?.additionalTabs;
if (tabs && tabs.length > 0) {
tabs.forEach((_: any, idx: number) => {
if (idx + 1 !== activeTabIndex) {
loadTabData(idx + 1, null);
}
});
}
return;
}
setSelectedLeftItem(item);
setExpandedRightItems(new Set()); // 좌측 항목 변경 시 우측 확장 초기화
setTabsData({}); // 모든 탭 데이터 초기화
@@ -1450,7 +1591,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
});
}
},
[loadRightData, loadTabData, activeTabIndex, componentConfig.leftPanel?.tableName, isDesignMode],
[loadRightData, loadTabData, activeTabIndex, componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.relation, componentConfig.rightPanel?.additionalTabs, isDesignMode, selectedLeftItem],
);
// 우측 항목 확장/축소 토글
@@ -2026,10 +2167,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
if (editModalPanel === "left") {
loadLeftData();
// 우측 패널도 새로고침 (FK가 변경되었을 수 있음)
if (selectedLeftItem) {
loadRightData(selectedLeftItem);
}
} else if (editModalPanel === "right" && selectedLeftItem) {
loadRightData(selectedLeftItem);
} else if (editModalPanel === "right") {
loadRightData(selectedLeftItem);
}
} else {
@@ -2160,7 +2299,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
setSelectedLeftItem(null);
setRightData(null);
}
} else if (deleteModalPanel === "right" && selectedLeftItem) {
} else if (deleteModalPanel === "right") {
loadRightData(selectedLeftItem);
}
} else {
@@ -2317,7 +2456,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
if (addModalPanel === "left" || addModalPanel === "left-item") {
// 좌측 패널 데이터 새로고침 (일반 추가 또는 하위 항목 추가)
loadLeftData();
} else if (addModalPanel === "right" && selectedLeftItem) {
} else if (addModalPanel === "right") {
// 우측 패널 데이터 새로고침
loadRightData(selectedLeftItem);
}
@@ -2405,10 +2544,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
}
}, [leftColumnVisibility, componentConfig.leftPanel?.tableName, currentUserId]);
// 초기 데이터 로드
// 초기 데이터 로드 (좌측 + 우측 전체 데이터)
useEffect(() => {
if (!isDesignMode && componentConfig.autoLoad !== false) {
loadLeftData();
// 좌측 미선택 상태에서 우측 전체 데이터 기본 로드
const relationshipType = componentConfig.rightPanel?.relation?.type || "detail";
if (relationshipType === "join") {
loadRightData(null);
// 추가 탭도 전체 데이터 로드
const tabs = componentConfig.rightPanel?.additionalTabs;
if (tabs && tabs.length > 0) {
tabs.forEach((_: any, idx: number) => {
loadTabData(idx + 1, null);
});
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDesignMode, componentConfig.autoLoad]);
@@ -2421,19 +2572,17 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [leftFilters]);
// 🆕 전역 테이블 새로고침 이벤트 리스너
// 전역 테이블 새로고침 이벤트 리스너
useEffect(() => {
const handleRefreshTable = () => {
if (!isDesignMode) {
console.log("🔄 [SplitPanel] refreshTable 이벤트 수신 - 데이터 새로고침");
loadLeftData();
// 선택된 항목이 있으면 현재 활성 탭 데이터 새로고침
if (selectedLeftItem) {
if (activeTabIndex === 0) {
loadRightData(selectedLeftItem);
} else {
loadTabData(activeTabIndex, selectedLeftItem);
}
// 현재 활성 탭 데이터 새로고침 (좌측 미선택 시에도 전체 데이터 로드)
if (activeTabIndex === 0) {
loadRightData(selectedLeftItem);
} else {
loadTabData(activeTabIndex, selectedLeftItem);
}
}
};
@@ -3339,15 +3488,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
);
}
if (!selectedLeftItem) {
return (
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2 py-12 text-sm">
<p> </p>
</div>
);
}
if (currentTabData.length === 0) {
if (currentTabData.length === 0 && !isTabLoading) {
return (
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2 py-12 text-sm">
<p> .</p>
@@ -4107,11 +4248,20 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
</div>
</div>
) : (
// 선택 없음
// 데이터 없음 또는 초기 로딩 대기
<div className="flex h-full items-center justify-center">
<div className="text-muted-foreground text-center text-sm">
<p className="mb-2"> </p>
<p className="text-xs"> </p>
{componentConfig.rightPanel?.relation?.type === "join" ? (
<>
<Loader2 className="text-muted-foreground mx-auto h-6 w-6 animate-spin" />
<p className="mt-2"> ...</p>
</>
) : (
<>
<p className="mb-2"> </p>
<p className="text-xs"> </p>
</>
)}
</div>
</div>
)}