feat: Enhance screen management with conditional layer and zone handling
- Updated the ScreenManagementService to allow general companies to query both their own zones and common zones. - Improved the ScreenViewPage to include detailed logging for loaded conditional layers and zones. - Added functionality to ignore empty targetComponentId in condition evaluations. - Enhanced the EditModal and LayerManagerPanel to support loading conditional layers and dynamic options based on selected zones. - Implemented additional tab configurations to manage entity join columns effectively in the SplitPanelLayout components.
This commit is contained in:
@@ -199,6 +199,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
const [isLoadingRight, setIsLoadingRight] = useState(false);
|
||||
const [rightTableColumns, setRightTableColumns] = useState<any[]>([]); // 우측 테이블 컬럼 정보
|
||||
const [expandedItems, setExpandedItems] = useState<Set<any>>(new Set()); // 펼쳐진 항목들
|
||||
|
||||
// 추가 탭 관련 상태
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0); // 0 = 기본 탭, 1+ = 추가 탭
|
||||
const [tabsData, setTabsData] = useState<Record<number, any[]>>({}); // 탭별 데이터
|
||||
const [tabsLoading, setTabsLoading] = useState<Record<number, boolean>>({}); // 탭별 로딩 상태
|
||||
const [leftColumnLabels, setLeftColumnLabels] = useState<Record<string, string>>({}); // 좌측 컬럼 라벨
|
||||
const [rightColumnLabels, setRightColumnLabels] = useState<Record<string, string>>({}); // 우측 컬럼 라벨
|
||||
const [leftCategoryMappings, setLeftCategoryMappings] = useState<
|
||||
@@ -1255,14 +1260,109 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
],
|
||||
);
|
||||
|
||||
// 추가 탭 데이터 로딩 함수
|
||||
const loadTabData = useCallback(
|
||||
async (tabIndex: number, leftItem: any) => {
|
||||
const tabConfig = componentConfig.rightPanel?.additionalTabs?.[tabIndex - 1];
|
||||
if (!tabConfig || !leftItem || isDesignMode) return;
|
||||
|
||||
const tabTableName = tabConfig.tableName;
|
||||
if (!tabTableName) return;
|
||||
|
||||
setTabsLoading((prev) => ({ ...prev, [tabIndex]: true }));
|
||||
try {
|
||||
const keys = tabConfig.relation?.keys;
|
||||
const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn;
|
||||
const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn;
|
||||
|
||||
let resultData: any[] = [];
|
||||
|
||||
if (leftColumn && rightColumn) {
|
||||
const searchConditions: Record<string, any> = {};
|
||||
|
||||
if (keys && keys.length > 0) {
|
||||
keys.forEach((key: any) => {
|
||||
if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) {
|
||||
searchConditions[key.rightColumn] = {
|
||||
value: leftItem[key.leftColumn],
|
||||
operator: "equals",
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const leftValue = leftItem[leftColumn];
|
||||
if (leftValue !== undefined) {
|
||||
searchConditions[rightColumn] = {
|
||||
value: leftValue,
|
||||
operator: "equals",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
|
||||
search: searchConditions,
|
||||
enableEntityJoin: true,
|
||||
size: 1000,
|
||||
});
|
||||
resultData = result.data || [];
|
||||
} else {
|
||||
const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
|
||||
enableEntityJoin: true,
|
||||
size: 1000,
|
||||
});
|
||||
resultData = result.data || [];
|
||||
}
|
||||
|
||||
setTabsData((prev) => ({ ...prev, [tabIndex]: resultData }));
|
||||
} catch (error) {
|
||||
console.error(`추가탭 ${tabIndex} 데이터 로드 실패:`, error);
|
||||
toast({
|
||||
title: "데이터 로드 실패",
|
||||
description: `탭 데이터를 불러올 수 없습니다.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTabsLoading((prev) => ({ ...prev, [tabIndex]: false }));
|
||||
}
|
||||
},
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedLeftItem, rightData, tabsData, loadRightData, loadTabData],
|
||||
);
|
||||
|
||||
// 좌측 항목 선택 핸들러
|
||||
const handleLeftItemSelect = useCallback(
|
||||
(item: any) => {
|
||||
setSelectedLeftItem(item);
|
||||
setExpandedRightItems(new Set()); // 좌측 항목 변경 시 우측 확장 초기화
|
||||
loadRightData(item);
|
||||
setTabsData({}); // 모든 탭 데이터 초기화
|
||||
|
||||
// 🆕 modalDataStore에 선택된 좌측 항목 저장 (단일 선택)
|
||||
// 현재 활성 탭에 따라 데이터 로드
|
||||
if (activeTabIndex === 0) {
|
||||
loadRightData(item);
|
||||
} else {
|
||||
loadTabData(activeTabIndex, item);
|
||||
}
|
||||
|
||||
// modalDataStore에 선택된 좌측 항목 저장 (단일 선택)
|
||||
const leftTableName = componentConfig.leftPanel?.tableName;
|
||||
if (leftTableName && !isDesignMode) {
|
||||
import("@/stores/modalDataStore").then(({ useModalDataStore }) => {
|
||||
@@ -1271,7 +1371,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
});
|
||||
}
|
||||
},
|
||||
[loadRightData, componentConfig.leftPanel?.tableName, isDesignMode],
|
||||
[loadRightData, loadTabData, activeTabIndex, componentConfig.leftPanel?.tableName, isDesignMode],
|
||||
);
|
||||
|
||||
// 우측 항목 확장/축소 토글
|
||||
@@ -1574,6 +1674,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
}
|
||||
});
|
||||
|
||||
// 🆕 추가 탭의 테이블도 카테고리 로드 대상에 포함
|
||||
const additionalTabs = componentConfig.rightPanel?.additionalTabs || [];
|
||||
additionalTabs.forEach((tab: any) => {
|
||||
if (tab.tableName) {
|
||||
tablesToLoad.add(tab.tableName);
|
||||
}
|
||||
// 추가 탭 컬럼에서 조인된 테이블 추출
|
||||
(tab.columns || []).forEach((col: any) => {
|
||||
const colName = col.name || col.columnName;
|
||||
if (colName && colName.includes(".")) {
|
||||
const joinTableName = colName.split(".")[0];
|
||||
tablesToLoad.add(joinTableName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log("🔍 우측 패널 카테고리 로드 대상 테이블:", Array.from(tablesToLoad));
|
||||
|
||||
// 각 테이블에 대해 카테고리 매핑 로드
|
||||
@@ -1625,7 +1741,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
};
|
||||
|
||||
loadRightCategoryMappings();
|
||||
}, [componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.columns, isDesignMode]);
|
||||
}, [componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.columns, componentConfig.rightPanel?.additionalTabs, isDesignMode]);
|
||||
|
||||
// 항목 펼치기/접기 토글
|
||||
const toggleExpand = useCallback((itemId: any) => {
|
||||
@@ -1668,13 +1784,24 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
// 수정 버튼 핸들러
|
||||
const handleEditClick = useCallback(
|
||||
(panel: "left" | "right", item: any) => {
|
||||
// 🆕 우측 패널 수정 버튼 설정 확인
|
||||
if (panel === "right" && componentConfig.rightPanel?.editButton?.mode === "modal") {
|
||||
const modalScreenId = componentConfig.rightPanel?.editButton?.modalScreenId;
|
||||
// 우측 패널 수정 버튼 설정 확인 (탭별 설정 지원)
|
||||
if (panel === "right") {
|
||||
const editButtonConfig =
|
||||
activeTabIndex === 0
|
||||
? componentConfig.rightPanel?.editButton
|
||||
: (componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1] as any)?.editButton;
|
||||
|
||||
if (modalScreenId) {
|
||||
// 커스텀 모달 화면 열기
|
||||
const rightTableName = componentConfig.rightPanel?.tableName || "";
|
||||
const currentTableName =
|
||||
activeTabIndex === 0
|
||||
? componentConfig.rightPanel?.tableName || ""
|
||||
: (componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1] as any)?.tableName || "";
|
||||
|
||||
if (editButtonConfig?.mode === "modal") {
|
||||
const modalScreenId = editButtonConfig?.modalScreenId;
|
||||
|
||||
if (modalScreenId) {
|
||||
// 커스텀 모달 화면 열기
|
||||
const rightTableName = currentTableName;
|
||||
|
||||
// Primary Key 찾기 (우선순위: id > ID > user_id > {table}_id > 첫 번째 필드)
|
||||
let primaryKeyName = "id";
|
||||
@@ -1750,7 +1877,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
groupByColumns: groupByColumns.length > 0 ? JSON.stringify(groupByColumns) : "없음",
|
||||
});
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1760,7 +1888,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
setEditModalFormData({ ...item });
|
||||
setShowEditModal(true);
|
||||
},
|
||||
[componentConfig],
|
||||
[componentConfig, activeTabIndex],
|
||||
);
|
||||
|
||||
// 수정 모달 저장
|
||||
@@ -2220,9 +2348,13 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
if (!isDesignMode) {
|
||||
console.log("🔄 [SplitPanel] refreshTable 이벤트 수신 - 데이터 새로고침");
|
||||
loadLeftData();
|
||||
// 선택된 항목이 있으면 우측 패널도 새로고침
|
||||
// 선택된 항목이 있으면 현재 활성 탭 데이터 새로고침
|
||||
if (selectedLeftItem) {
|
||||
loadRightData(selectedLeftItem);
|
||||
if (activeTabIndex === 0) {
|
||||
loadRightData(selectedLeftItem);
|
||||
} else {
|
||||
loadTabData(activeTabIndex, selectedLeftItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2232,7 +2364,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
return () => {
|
||||
window.removeEventListener("refreshTable", handleRefreshTable);
|
||||
};
|
||||
}, [isDesignMode, loadLeftData, loadRightData, selectedLeftItem]);
|
||||
}, [isDesignMode, loadLeftData, loadRightData, loadTabData, activeTabIndex, selectedLeftItem]);
|
||||
|
||||
// 리사이저 드래그 핸들러
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
@@ -3021,24 +3153,63 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
style={{
|
||||
height: componentConfig.rightPanel?.panelHeaderHeight || 48,
|
||||
minHeight: componentConfig.rightPanel?.panelHeaderHeight || 48,
|
||||
padding: "0 1rem",
|
||||
padding: "0 0.75rem",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{componentConfig.rightPanel?.title || "우측 패널"}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-0">
|
||||
{/* 탭이 없으면 제목만, 있으면 탭으로 전환 */}
|
||||
{(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 ? (
|
||||
<div className="flex items-center gap-0">
|
||||
<button
|
||||
onClick={() => handleTabChange(0)}
|
||||
className={cn(
|
||||
"px-3 py-1 text-sm font-medium transition-colors",
|
||||
activeTabIndex === 0
|
||||
? "text-foreground border-b-2 border-primary"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{componentConfig.rightPanel?.title || "기본"}
|
||||
</button>
|
||||
{componentConfig.rightPanel?.additionalTabs?.map((tab: any, index: number) => (
|
||||
<button
|
||||
key={tab.tabId || `tab-${index}`}
|
||||
onClick={() => handleTabChange(index + 1)}
|
||||
className={cn(
|
||||
"px-3 py-1 text-sm font-medium transition-colors",
|
||||
activeTabIndex === index + 1
|
||||
? "text-foreground border-b-2 border-primary"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{tab.label || `탭 ${index + 1}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{componentConfig.rightPanel?.title || "우측 패널"}
|
||||
</CardTitle>
|
||||
)}
|
||||
</div>
|
||||
{!isDesignMode && (
|
||||
<div className="flex items-center gap-2">
|
||||
{componentConfig.rightPanel?.showAdd && (
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddClick("right")}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
추가
|
||||
</Button>
|
||||
)}
|
||||
{/* 우측 패널 수정/삭제는 각 카드에서 처리 */}
|
||||
{activeTabIndex === 0
|
||||
? componentConfig.rightPanel?.showAdd && (
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddClick("right")}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
추가
|
||||
</Button>
|
||||
)
|
||||
: (componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1] as any)?.showAdd && (
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddClick("right")}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
추가
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -3057,8 +3228,139 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
</div>
|
||||
)}
|
||||
<CardContent className="flex-1 overflow-auto p-4">
|
||||
{/* 우측 데이터/커스텀 */}
|
||||
{componentConfig.rightPanel?.displayMode === "custom" ? (
|
||||
{/* 추가 탭 컨텐츠 */}
|
||||
{activeTabIndex > 0 ? (
|
||||
(() => {
|
||||
const currentTabConfig = componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1] as any;
|
||||
const currentTabData = tabsData[activeTabIndex] || [];
|
||||
const isTabLoading = tabsLoading[activeTabIndex];
|
||||
|
||||
if (isTabLoading) {
|
||||
return (
|
||||
<div className="flex h-32 items-center justify-center">
|
||||
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-2 py-12 text-sm">
|
||||
<p>관련 데이터가 없습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 탭 컬럼 설정
|
||||
const tabColumns = currentTabConfig?.columns || [];
|
||||
|
||||
// 테이블 모드로 표시
|
||||
if (currentTabConfig?.displayMode === "table") {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
{tabColumns.map((col: any) => (
|
||||
<th key={col.name} className="text-muted-foreground px-3 py-2 text-left text-xs font-medium">
|
||||
{col.label || col.name}
|
||||
</th>
|
||||
))}
|
||||
{(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && (
|
||||
<th className="text-muted-foreground px-3 py-2 text-right text-xs font-medium">작업</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentTabData.map((item: any, idx: number) => (
|
||||
<tr key={item.id || idx} className="hover:bg-muted/50 border-b">
|
||||
{tabColumns.map((col: any) => (
|
||||
<td key={col.name} className="px-3 py-2 text-xs">
|
||||
{formatCellValue(
|
||||
col.name,
|
||||
getEntityJoinValue(item, col.name),
|
||||
rightCategoryMappings,
|
||||
col.format,
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
{(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && (
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
{currentTabConfig?.showEdit && (
|
||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||
onClick={() => handleEditClick("right", item)}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{currentTabConfig?.showDelete && (
|
||||
<Button size="sm" variant="ghost" className="text-destructive h-7 px-2 text-xs"
|
||||
onClick={() => handleDeleteClick("right", item)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 리스트(카드) 모드로 표시
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{currentTabData.map((item: any, idx: number) => (
|
||||
<div key={item.id || idx} className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs">
|
||||
{tabColumns.map((col: any) => (
|
||||
<span key={col.name}>
|
||||
{formatCellValue(
|
||||
col.name,
|
||||
getEntityJoinValue(item, col.name),
|
||||
rightCategoryMappings,
|
||||
col.format,
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && (
|
||||
<div className="flex items-center gap-1">
|
||||
{currentTabConfig?.showEdit && (
|
||||
<Button size="sm" variant="ghost" className="h-7 px-2 text-xs"
|
||||
onClick={() => handleEditClick("right", item)}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
{currentTabConfig?.showDelete && (
|
||||
<Button size="sm" variant="ghost" className="text-destructive h-7 px-2 text-xs"
|
||||
onClick={() => handleDeleteClick("right", item)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
) : componentConfig.rightPanel?.displayMode === "custom" ? (
|
||||
// 🆕 커스텀 모드: 패널 안에 자유롭게 컴포넌트 배치
|
||||
<div
|
||||
className="relative h-full w-full"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,14 @@ export interface AdditionalTabConfig {
|
||||
suffix?: string;
|
||||
dateFormat?: string;
|
||||
};
|
||||
// Entity 조인 컬럼 정보
|
||||
isEntityJoin?: boolean;
|
||||
joinInfo?: {
|
||||
sourceTable: string;
|
||||
sourceColumn: string;
|
||||
referenceTable: string;
|
||||
joinAlias: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
addModalColumns?: Array<{
|
||||
@@ -145,6 +153,14 @@ export interface SplitPanelLayoutConfig {
|
||||
suffix?: string; // 접미사 (예: "원", "개")
|
||||
dateFormat?: string; // 날짜 포맷 (type: "date")
|
||||
};
|
||||
// Entity 조인 컬럼 정보
|
||||
isEntityJoin?: boolean;
|
||||
joinInfo?: {
|
||||
sourceTable: string;
|
||||
sourceColumn: string;
|
||||
referenceTable: string;
|
||||
joinAlias: string;
|
||||
};
|
||||
}>;
|
||||
// 추가 모달에서 입력받을 컬럼 설정
|
||||
addModalColumns?: Array<{
|
||||
@@ -217,6 +233,14 @@ export interface SplitPanelLayoutConfig {
|
||||
suffix?: string; // 접미사 (예: "원", "개")
|
||||
dateFormat?: string; // 날짜 포맷 (type: "date")
|
||||
};
|
||||
// Entity 조인 컬럼 정보
|
||||
isEntityJoin?: boolean;
|
||||
joinInfo?: {
|
||||
sourceTable: string;
|
||||
sourceColumn: string;
|
||||
referenceTable: string;
|
||||
joinAlias: string;
|
||||
};
|
||||
}>;
|
||||
// 추가 모달에서 입력받을 컬럼 설정
|
||||
addModalColumns?: Array<{
|
||||
|
||||
Reference in New Issue
Block a user