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 (