diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 50f7c41b..869d2c3c 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -33,6 +33,7 @@ import { DialogDescription, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options"; import { useAuth } from "@/hooks/useAuth"; @@ -171,6 +172,12 @@ export const SplitPanelLayoutComponent: React.FC const [rightSearchQuery, setRightSearchQuery] = useState(""); const [isLoadingLeft, setIsLoadingLeft] = useState(false); const [isLoadingRight, setIsLoadingRight] = useState(false); + + // ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ๊ด€๋ จ ์ƒํƒœ + const [activeTabIndex, setActiveTabIndex] = useState(0); // 0 = ๊ธฐ๋ณธ ํƒญ (์šฐ์ธก ํŒจ๋„), 1+ = ์ถ”๊ฐ€ ํƒญ + const [tabsData, setTabsData] = useState>({}); // ํƒญ๋ณ„ ๋ฐ์ดํ„ฐ ์บ์‹œ + const [tabsLoading, setTabsLoading] = useState>({}); // ํƒญ๋ณ„ ๋กœ๋”ฉ ์ƒํƒœ + const [rightTableColumns, setRightTableColumns] = useState([]); // ์šฐ์ธก ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด const [expandedItems, setExpandedItems] = useState>(new Set()); // ํŽผ์ณ์ง„ ํ•ญ๋ชฉ๋“ค const [leftColumnLabels, setLeftColumnLabels] = useState>({}); // ์ขŒ์ธก ์ปฌ๋Ÿผ ๋ผ๋ฒจ @@ -1001,12 +1008,137 @@ export const SplitPanelLayoutComponent: React.FC ], ); + // ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ํ•จ์ˆ˜ + 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 { entityJoinApi } = await import("@/lib/api/entityJoin"); + const searchConditions: Record = {}; + + if (keys && keys.length > 0) { + // ๋ณตํ•ฉํ‚ค + keys.forEach((key) => { + if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { + searchConditions[key.rightColumn] = leftItem[key.leftColumn]; + } + }); + } else { + // ๋‹จ์ผํ‚ค + const leftValue = leftItem[leftColumn]; + if (leftValue !== undefined) { + searchConditions[rightColumn] = leftValue; + } + } + + console.log(`๐Ÿ”— [์ถ”๊ฐ€ํƒญ ${tabIndex}] ์กฐํšŒ ์กฐ๊ฑด:`, searchConditions); + + const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { + search: searchConditions, + enableEntityJoin: true, + size: 1000, + }); + + resultData = result.data || []; + } else { + // ์กฐ์ธ ์กฐ๊ฑด์ด ์—†๋Š” ๊ฒฝ์šฐ: ์ „์ฒด ๋ฐ์ดํ„ฐ ์กฐํšŒ (๋…๋ฆฝ ํƒญ) + const { entityJoinApi } = await import("@/lib/api/entityJoin"); + const result = await entityJoinApi.getTableDataWithJoins(tabTableName, { + enableEntityJoin: true, + size: 1000, + }); + resultData = result.data || []; + } + + // ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์ ์šฉ + const dataFilter = tabConfig.dataFilter; + if (dataFilter?.enabled && dataFilter.conditions?.length > 0) { + resultData = resultData.filter((item: any) => { + return dataFilter.conditions.every((cond: any) => { + const value = item[cond.column]; + const condValue = cond.value; + switch (cond.operator) { + case "equals": + return value === condValue; + case "notEquals": + return value !== condValue; + case "contains": + return String(value).includes(String(condValue)); + default: + return true; + } + }); + }); + } + + // ์ค‘๋ณต ์ œ๊ฑฐ ์ ์šฉ + const deduplication = tabConfig.deduplication; + if (deduplication?.enabled && deduplication.groupByColumn) { + const groupedMap = new Map(); + resultData.forEach((item) => { + const key = String(item[deduplication.groupByColumn] || ""); + const existing = groupedMap.get(key); + if (!existing) { + groupedMap.set(key, item); + } else { + // keepStrategy์— ๋”ฐ๋ผ ์œ ์ง€ํ•  ํ•ญ๋ชฉ ๊ฒฐ์ • + const sortCol = deduplication.sortColumn || "start_date"; + const existingVal = existing[sortCol]; + const newVal = item[sortCol]; + if (deduplication.keepStrategy === "latest" && newVal > existingVal) { + groupedMap.set(key, item); + } else if (deduplication.keepStrategy === "earliest" && newVal < existingVal) { + groupedMap.set(key, item); + } + } + }); + resultData = Array.from(groupedMap.values()); + } + + console.log(`๐Ÿ”— [์ถ”๊ฐ€ํƒญ ${tabIndex}] ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ:`, resultData.length); + 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 handleLeftItemSelect = useCallback( (item: any) => { setSelectedLeftItem(item); setExpandedRightItems(new Set()); // ์ขŒ์ธก ํ•ญ๋ชฉ ๋ณ€๊ฒฝ ์‹œ ์šฐ์ธก ํ™•์žฅ ์ดˆ๊ธฐํ™” - loadRightData(item); + setTabsData({}); // ๐Ÿ†• ๋ชจ๋“  ํƒญ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + + // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๋กœ๋“œ + if (activeTabIndex === 0) { + loadRightData(item); + } else { + loadTabData(activeTabIndex, item); + } // ๐Ÿ†• modalDataStore์— ์„ ํƒ๋œ ์ขŒ์ธก ํ•ญ๋ชฉ ์ €์žฅ (๋‹จ์ผ ์„ ํƒ) const leftTableName = componentConfig.leftPanel?.tableName; @@ -1017,7 +1149,30 @@ export const SplitPanelLayoutComponent: React.FC }); } }, - [loadRightData, componentConfig.leftPanel?.tableName, isDesignMode], + [loadRightData, loadTabData, activeTabIndex, componentConfig.leftPanel?.tableName, isDesignMode], + ); + + // ๐Ÿ†• ํƒญ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ + 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], ); // ์šฐ์ธก ํ•ญ๋ชฉ ํ™•์žฅ/์ถ•์†Œ ํ† ๊ธ€ @@ -2534,6 +2689,34 @@ export const SplitPanelLayoutComponent: React.FC className="flex flex-shrink-0 flex-col" > + {/* ๐Ÿ†• ํƒญ ๋ฐ” (์ถ”๊ฐ€ ํƒญ์ด ์žˆ์„ ๋•Œ๋งŒ ํ‘œ์‹œ) */} + {(componentConfig.rightPanel?.additionalTabs?.length || 0) > 0 && ( +
+ handleTabChange(Number(value))} + className="w-full" + > + + + {componentConfig.rightPanel?.title || "๊ธฐ๋ณธ"} + + {componentConfig.rightPanel?.additionalTabs?.map((tab, index) => ( + + {tab.label || `ํƒญ ${index + 1}`} + + ))} + + +
+ )} >
- {componentConfig.rightPanel?.title || "์šฐ์ธก ํŒจ๋„"} + {activeTabIndex === 0 + ? componentConfig.rightPanel?.title || "์šฐ์ธก ํŒจ๋„" + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.title || + componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.label || + "์šฐ์ธก ํŒจ๋„"} {!isDesignMode && (
- {componentConfig.rightPanel?.showAdd && ( - - )} + {/* ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + {activeTabIndex === 0 + ? componentConfig.rightPanel?.showAdd && ( + + ) + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.showAdd && ( + + )} {/* ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ •/์‚ญ์ œ๋Š” ๊ฐ ์นด๋“œ์—์„œ ์ฒ˜๋ฆฌ */}
)} @@ -2575,20 +2770,231 @@ export const SplitPanelLayoutComponent: React.FC
)} - {/* ์šฐ์ธก ๋ฐ์ดํ„ฐ */} - {isLoadingRight ? ( - // ๋กœ๋”ฉ ์ค‘ -
-
- -

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

-
-
- ) : rightData ? ( - // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ - Array.isArray(rightData) ? ( - // ์กฐ์ธ ๋ชจ๋“œ: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ…Œ์ด๋ธ”/๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œ - (() => { + {/* ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ๋ฐ์ดํ„ฐ ๋ Œ๋”๋ง */} + {activeTabIndex > 0 ? ( + // ์ถ”๊ฐ€ ํƒญ ์ปจํ…์ธ  + (() => { + const currentTabConfig = componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]; + const currentTabData = tabsData[activeTabIndex] || []; + const isTabLoading = tabsLoading[activeTabIndex]; + + if (isTabLoading) { + return ( +
+
+ +

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ ); + } + + if (!selectedLeftItem) { + return ( +
+

์ขŒ์ธก์—์„œ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”

+
+ ); + } + + if (currentTabData.length === 0) { + return ( +
+

๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค

+
+ ); + } + + // ํƒญ ๋ฐ์ดํ„ฐ ๋ Œ๋”๋ง (๋ชฉ๋ก/ํ…Œ์ด๋ธ” ๋ชจ๋“œ) + const isTableMode = currentTabConfig?.displayMode === "table"; + + if (isTableMode) { + // ํ…Œ์ด๋ธ” ๋ชจ๋“œ + const displayColumns = currentTabConfig?.columns || []; + const columnsToShow = + displayColumns.length > 0 + ? displayColumns.map((col) => ({ + ...col, + label: col.label || col.name, + })) + : Object.keys(currentTabData[0] || {}) + .filter(shouldShowField) + .slice(0, 8) + .map((key) => ({ name: key, label: key })); + + return ( +
+ + + + {columnsToShow.map((col: any) => ( + + ))} + {(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && ( + + )} + + + + {currentTabData.map((item: any, idx: number) => ( + + {columnsToShow.map((col: any) => ( + + ))} + {(currentTabConfig?.showEdit || currentTabConfig?.showDelete) && ( + + )} + + ))} + +
+ {col.label} + ์ž‘์—…
+ {formatCellValue(col.name, item[col.name], {}, col.format)} + +
+ {currentTabConfig?.showEdit && ( + + )} + {currentTabConfig?.showDelete && ( + + )} +
+
+
+ ); + } else { + // ๋ชฉ๋ก (์นด๋“œ) ๋ชจ๋“œ + const displayColumns = currentTabConfig?.columns || []; + const summaryCount = currentTabConfig?.summaryColumnCount ?? 3; + const showLabel = currentTabConfig?.summaryShowLabel ?? true; + + return ( +
+ {currentTabData.map((item: any, idx: number) => { + const itemId = item.id || idx; + const isExpanded = expandedRightItems.has(itemId); + + // ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ ๊ฒฐ์ • + const columnsToShow = + displayColumns.length > 0 + ? displayColumns + : Object.keys(item) + .filter(shouldShowField) + .slice(0, 8) + .map((key) => ({ name: key, label: key })); + + const summaryColumns = columnsToShow.slice(0, summaryCount); + const detailColumns = columnsToShow.slice(summaryCount); + + return ( +
+
toggleRightItemExpansion(itemId)} + > +
+
+ {summaryColumns.map((col: any) => ( +
+ {showLabel && ( + {col.label}: + )} + + {formatCellValue(col.name, item[col.name], {}, col.format)} + +
+ ))} +
+
+
+ {currentTabConfig?.showEdit && ( + + )} + {currentTabConfig?.showDelete && ( + + )} + {detailColumns.length > 0 && + (isExpanded ? ( + + ) : ( + + ))} +
+
+ {isExpanded && detailColumns.length > 0 && ( +
+
+ {detailColumns.map((col: any) => ( +
+ {col.label}: + {formatCellValue(col.name, item[col.name], {}, col.format)} +
+ ))} +
+
+ )} +
+ ); + })} +
+ ); + } + })() + ) : ( + /* ๊ธฐ๋ณธ ํƒญ (์šฐ์ธก ํŒจ๋„) ๋ฐ์ดํ„ฐ */ + <> + {isLoadingRight ? ( + // ๋กœ๋”ฉ ์ค‘ +
+
+ +

๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+
+ ) : rightData ? ( + // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ + Array.isArray(rightData) ? ( + // ์กฐ์ธ ๋ชจ๋“œ: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ…Œ์ด๋ธ”/๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œ + (() => { // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋ง const filteredData = rightSearchQuery ? rightData.filter((item) => { @@ -3018,14 +3424,16 @@ export const SplitPanelLayoutComponent: React.FC - ) : ( - // ์„ ํƒ ์—†์Œ -
-
-

์ขŒ์ธก์—์„œ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”

-

์„ ํƒํ•œ ํ•ญ๋ชฉ์˜ ์ƒ์„ธ ์ •๋ณด๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค

-
-
+ ) : ( + // ์„ ํƒ ์—†์Œ +
+
+

์ขŒ์ธก์—์„œ ํ•ญ๋ชฉ์„ ์„ ํƒํ•˜์„ธ์š”

+

์„ ํƒํ•œ ํ•ญ๋ชฉ์˜ ์ƒ์„ธ ์ •๋ณด๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค

+
+
+ )} + )}