diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index ad7f5302..ac09652e 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -32,6 +32,7 @@ import { DialogFooter, DialogDescription, } from "@/components/ui/dialog"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Label } from "@/components/ui/label"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options"; @@ -162,6 +163,11 @@ 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>({}); // ์ขŒ์ธก ์ปฌ๋Ÿผ ๋ผ๋ฒจ @@ -603,6 +609,41 @@ export const SplitPanelLayoutComponent: React.FC return result; }, []); + // ๐Ÿ†• ๊ฐ„๋‹จํ•œ ๊ฐ’ ํฌ๋งทํŒ… ํ•จ์ˆ˜ (์ถ”๊ฐ€ ํƒญ์šฉ) + const formatValue = useCallback( + ( + value: any, + format?: { + type?: "number" | "currency" | "date" | "text"; + thousandSeparator?: boolean; + decimalPlaces?: number; + prefix?: string; + suffix?: string; + dateFormat?: string; + }, + ): string => { + if (value === null || value === undefined) return "-"; + + // ๋‚ ์งœ ํฌ๋งท + if (format?.type === "date" || format?.dateFormat) { + return formatDateValue(value, format?.dateFormat || "YYYY-MM-DD"); + } + + // ์ˆซ์ž ํฌ๋งท + if ( + format?.type === "number" || + format?.type === "currency" || + format?.thousandSeparator || + format?.decimalPlaces !== undefined + ) { + return formatNumberValue(value, format); + } + + return String(value); + }, + [formatDateValue, formatNumberValue], + ); + // ์…€ ๊ฐ’ ํฌ๋งทํŒ… ํ•จ์ˆ˜ (์นดํ…Œ๊ณ ๋ฆฌ ํƒ€์ž… ์ฒ˜๋ฆฌ + ๋‚ ์งœ/์ˆซ์ž ํฌ๋งท) const formatCellValue = useCallback( ( @@ -988,12 +1029,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; @@ -1004,7 +1170,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], ); // ์šฐ์ธก ํ•ญ๋ชฉ ํ™•์žฅ/์ถ•์†Œ ํ† ๊ธ€ @@ -1401,13 +1590,19 @@ export const SplitPanelLayoutComponent: React.FC // ์ˆ˜์ • ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ const handleEditClick = useCallback( (panel: "left" | "right", item: any) => { + // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์˜ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ + const currentTabConfig = + activeTabIndex === 0 + ? componentConfig.rightPanel + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]; + // ๐Ÿ†• ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ • ๋ฒ„ํŠผ ์„ค์ • ํ™•์ธ - if (panel === "right" && componentConfig.rightPanel?.editButton?.mode === "modal") { - const modalScreenId = componentConfig.rightPanel?.editButton?.modalScreenId; + if (panel === "right" && currentTabConfig?.editButton?.mode === "modal") { + const modalScreenId = currentTabConfig?.editButton?.modalScreenId; if (modalScreenId) { // ์ปค์Šคํ…€ ๋ชจ๋‹ฌ ํ™”๋ฉด ์—ด๊ธฐ - const rightTableName = componentConfig.rightPanel?.tableName || ""; + const rightTableName = currentTabConfig?.tableName || ""; // Primary Key ์ฐพ๊ธฐ (์šฐ์„ ์ˆœ์œ„: id > ID > ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ) let primaryKeyName = "id"; @@ -1440,11 +1635,11 @@ export const SplitPanelLayoutComponent: React.FC }); // ๐Ÿ†• groupByColumns ์ถ”์ถœ - const groupByColumns = componentConfig.rightPanel?.editButton?.groupByColumns || []; + const groupByColumns = currentTabConfig?.editButton?.groupByColumns || []; console.log("๐Ÿ”ง [SplitPanel] ์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ - groupByColumns ํ™•์ธ:", { groupByColumns, - editButtonConfig: componentConfig.rightPanel?.editButton, + editButtonConfig: currentTabConfig?.editButton, hasGroupByColumns: groupByColumns.length > 0, }); @@ -1482,7 +1677,7 @@ export const SplitPanelLayoutComponent: React.FC setEditModalFormData({ ...item }); setShowEditModal(true); }, - [componentConfig], + [componentConfig, activeTabIndex], ); // ์ˆ˜์ • ๋ชจ๋‹ฌ ์ €์žฅ @@ -1582,13 +1777,19 @@ export const SplitPanelLayoutComponent: React.FC // ์‚ญ์ œ ํ™•์ธ const handleDeleteConfirm = useCallback(async () => { + // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์˜ ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ + const currentTabConfig = + activeTabIndex === 0 + ? componentConfig.rightPanel + : componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]; + // ์šฐ์ธก ํŒจ๋„ ์‚ญ์ œ ์‹œ ์ค‘๊ณ„ ํ…Œ์ด๋ธ” ํ™•์ธ let tableName = - deleteModalPanel === "left" ? componentConfig.leftPanel?.tableName : componentConfig.rightPanel?.tableName; + deleteModalPanel === "left" ? componentConfig.leftPanel?.tableName : currentTabConfig?.tableName; // ์šฐ์ธก ํŒจ๋„ + ์ค‘๊ณ„ ํ…Œ์ด๋ธ” ๋ชจ๋“œ์ธ ๊ฒฝ์šฐ - if (deleteModalPanel === "right" && componentConfig.rightPanel?.addConfig?.targetTable) { - tableName = componentConfig.rightPanel.addConfig.targetTable; + if (deleteModalPanel === "right" && currentTabConfig?.addConfig?.targetTable) { + tableName = currentTabConfig.addConfig.targetTable; console.log("๐Ÿ”— ์ค‘๊ณ„ ํ…Œ์ด๋ธ” ๋ชจ๋“œ: ์‚ญ์ œ ๋Œ€์ƒ ํ…Œ์ด๋ธ” =", tableName); } @@ -1676,7 +1877,12 @@ export const SplitPanelLayoutComponent: React.FC setRightData(null); } } else if (deleteModalPanel === "right" && selectedLeftItem) { - loadRightData(selectedLeftItem); + // ๐Ÿ†• ํ˜„์žฌ ํ™œ์„ฑ ํƒญ์— ๋”ฐ๋ผ ์ƒˆ๋กœ๊ณ ์นจ + if (activeTabIndex === 0) { + loadRightData(selectedLeftItem); + } else { + loadTabData(activeTabIndex, selectedLeftItem); + } } } else { toast({ @@ -1700,7 +1906,7 @@ export const SplitPanelLayoutComponent: React.FC variant: "destructive", }); } - }, [deleteModalPanel, componentConfig, deleteModalItem, toast, selectedLeftItem, loadLeftData, loadRightData]); + }, [deleteModalPanel, componentConfig, deleteModalItem, toast, selectedLeftItem, loadLeftData, loadRightData, activeTabIndex, loadTabData]); // ํ•ญ๋ชฉ๋ณ„ ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ (์ขŒ์ธก ํ•ญ๋ชฉ์˜ + ๋ฒ„ํŠผ - ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€) const handleItemAddClick = useCallback( @@ -2521,6 +2727,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 && ( + + )} {/* ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ •/์‚ญ์ œ๋Š” ๊ฐ ์นด๋“œ์—์„œ ์ฒ˜๋ฆฌ */}
)} @@ -2562,16 +2808,228 @@ export const SplitPanelLayoutComponent: React.FC
)} - {/* ์šฐ์ธก ๋ฐ์ดํ„ฐ */} - {isLoadingRight ? ( - // ๋กœ๋”ฉ ์ค‘ -
-
- -

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

-
-
- ) : 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} + ์ž‘์—…
+ {formatValue(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}: + )} + + {formatValue(item[col.name], col.format)} + +
+ ))} +
+
+
+ {currentTabConfig?.showEdit && ( + + )} + {currentTabConfig?.showDelete && ( + + )} + {detailColumns.length > 0 && ( + isExpanded ? ( + + ) : ( + + ) + )} +
+
+ {isExpanded && detailColumns.length > 0 && ( +
+
+ {detailColumns.map((col: any) => ( +
+ {col.label}: + {formatValue(item[col.name], col.format)} +
+ ))} +
+
+ )} +
+ ); + })} +
+ ); + } + })() + ) : ( + /* ๊ธฐ๋ณธ ํƒญ (์šฐ์ธก ํŒจ๋„) ๋ฐ์ดํ„ฐ */ + <> + {isLoadingRight ? ( + // ๋กœ๋”ฉ ์ค‘ +
+
+ +

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

+
+
+ ) : rightData ? ( // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ Array.isArray(rightData) ? ( // ์กฐ์ธ ๋ชจ๋“œ: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฅผ ํ…Œ์ด๋ธ”/๋ฆฌ์ŠคํŠธ๋กœ ํ‘œ์‹œ @@ -3014,6 +3472,8 @@ export const SplitPanelLayoutComponent: React.FC )} + + )}
diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx index 5ca50ffb..f0b02ea2 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -11,10 +11,11 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; // Accordion ์ œ๊ฑฐ - ๋‹จ์ˆœ ์„น์…˜์œผ๋กœ ๋ณ€๊ฒฝ -import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown } from "lucide-react"; +import { Check, ChevronsUpDown, ArrowRight, Plus, X, ArrowUp, ArrowDown, Trash2, GripVertical } from "lucide-react"; import { cn } from "@/lib/utils"; -import { SplitPanelLayoutConfig } from "./types"; +import { SplitPanelLayoutConfig, AdditionalTabConfig } from "./types"; import { TableInfo, ColumnInfo } from "@/types/screen"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { tableTypeApi } from "@/lib/api/screen"; import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel"; @@ -189,6 +190,848 @@ const ScreenSelector: React.FC<{ ); }; +/** + * ์ถ”๊ฐ€ ํƒญ ์„ค์ • ํŒจ๋„ (์šฐ์ธก ํŒจ๋„๊ณผ ๋™์ผํ•œ ๊ตฌ์กฐ) + */ +interface AdditionalTabConfigPanelProps { + tab: AdditionalTabConfig; + tabIndex: number; + config: SplitPanelLayoutConfig; + updateRightPanel: (updates: Partial) => void; + availableRightTables: TableInfo[]; + leftTableColumns: ColumnInfo[]; + menuObjid?: number; + // ๊ณต์œ  ์ปฌ๋Ÿผ ๋กœ๋“œ ์ƒํƒœ + loadedTableColumns: Record; + loadTableColumns: (tableName: string) => Promise; + loadingColumns: Record; +} + +const AdditionalTabConfigPanel: React.FC = ({ + tab, + tabIndex, + config, + updateRightPanel, + availableRightTables, + leftTableColumns, + menuObjid, + loadedTableColumns, + loadTableColumns, + loadingColumns, +}) => { + // ํƒญ ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ ์‹œ ์ปฌ๋Ÿผ ๋กœ๋“œ + useEffect(() => { + if (tab.tableName && !loadedTableColumns[tab.tableName] && !loadingColumns[tab.tableName]) { + loadTableColumns(tab.tableName); + } + }, [tab.tableName, loadedTableColumns, loadingColumns, loadTableColumns]); + + // ํ˜„์žฌ ํƒญ์˜ ์ปฌ๋Ÿผ ๋ชฉ๋ก + const tabColumns = useMemo(() => { + return tab.tableName ? loadedTableColumns[tab.tableName] || [] : []; + }, [tab.tableName, loadedTableColumns]); + + // ๋กœ๋”ฉ ์ƒํƒœ + const loadingTabColumns = tab.tableName ? loadingColumns[tab.tableName] || false : false; + + // ํƒญ ์—…๋ฐ์ดํŠธ ํ—ฌํผ + const updateTab = (updates: Partial) => { + const newTabs = [...(config.rightPanel?.additionalTabs || [])]; + newTabs[tabIndex] = { ...tab, ...updates }; + updateRightPanel({ additionalTabs: newTabs }); + }; + + return ( + + +
+ + + {tab.label || `ํƒญ ${tabIndex + 1}`} + + {tab.tableName && ( + ({tab.tableName}) + )} +
+
+ +
+ {/* ===== 1. ๊ธฐ๋ณธ ์ •๋ณด ===== */} +
+ +
+
+ + updateTab({ label: e.target.value })} + placeholder="ํƒญ ์ด๋ฆ„" + className="h-8 text-xs" + /> +
+
+ + updateTab({ title: e.target.value })} + placeholder="ํŒจ๋„ ์ œ๋ชฉ" + className="h-8 text-xs" + /> +
+
+
+ + updateTab({ panelHeaderHeight: parseInt(e.target.value) || 48 })} + placeholder="48" + className="h-8 w-24 text-xs" + /> +
+
+ + {/* ===== 2. ํ…Œ์ด๋ธ” ์„ ํƒ ===== */} +
+ +
+ + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {availableRightTables.map((table) => ( + updateTab({ tableName: table.tableName, columns: [] })} + > + + {table.displayName || table.tableName} + + ))} + + + + +
+
+ + {/* ===== 3. ํ‘œ์‹œ ๋ชจ๋“œ ===== */} +
+ +
+ + +
+ + {/* ์š”์•ฝ ์„ค์ • (๋ชฉ๋ก ๋ชจ๋“œ) */} + {tab.displayMode === "list" && ( +
+
+ + updateTab({ summaryColumnCount: parseInt(e.target.value) || 3 })} + min={1} + max={10} + className="h-8 text-xs" + /> +
+
+ updateTab({ summaryShowLabel: !!checked })} + /> + +
+
+ )} +
+ + {/* ===== 4. ์ปฌ๋Ÿผ ๋งคํ•‘ (์กฐ์ธ ํ‚ค) ===== */} +
+ +

+ ์ขŒ์ธก ํŒจ๋„ ์„ ํƒ ์‹œ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋งŒ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค +

+
+
+ + +
+
+ + +
+
+
+ + {/* ===== 5. ๊ธฐ๋Šฅ ๋ฒ„ํŠผ ===== */} +
+ +
+
+ updateTab({ showSearch: !!checked })} + /> + +
+
+ updateTab({ showAdd: !!checked })} + /> + +
+
+ updateTab({ showEdit: !!checked })} + /> + +
+
+ updateTab({ showDelete: !!checked })} + /> + +
+
+
+ + {/* ===== 6. ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ค์ • ===== */} +
+
+ + +
+

+ ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”. ์„ ํƒํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋“  ์ปฌ๋Ÿผ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. +

+ + {/* ํ…Œ์ด๋ธ” ๋ฏธ์„ ํƒ ์ƒํƒœ */} + {!tab.tableName && ( +
+

๋จผ์ € ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”

+
+ )} + + {/* ํ…Œ์ด๋ธ” ์„ ํƒ๋จ - ์ปฌ๋Ÿผ ๋ชฉ๋ก */} + {tab.tableName && ( +
+ {/* ๋กœ๋”ฉ ์ƒํƒœ */} + {loadingTabColumns && ( +
+

์ปฌ๋Ÿผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘...

+
+ )} + + {/* ์„ค์ •๋œ ์ปฌ๋Ÿผ์ด ์—†์„ ๋•Œ */} + {!loadingTabColumns && (tab.columns || []).length === 0 && ( +
+

์„ค์ •๋œ ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค

+

์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋“  ์ปฌ๋Ÿผ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค

+
+ )} + + {/* ์„ค์ •๋œ ์ปฌ๋Ÿผ ๋ชฉ๋ก */} + {!loadingTabColumns && (tab.columns || []).length > 0 && ( + (tab.columns || []).map((col, colIndex) => ( +
+ {/* ์ƒ๋‹จ: ์ˆœ์„œ ๋ณ€๊ฒฝ + ์‚ญ์ œ ๋ฒ„ํŠผ */} +
+
+ + + #{colIndex + 1} +
+ +
+ + {/* ์ปฌ๋Ÿผ ์„ ํƒ */} +
+ + +
+ + {/* ๋ผ๋ฒจ + ๋„ˆ๋น„ */} +
+
+ + { + const newColumns = [...(tab.columns || [])]; + newColumns[colIndex] = { ...col, label: e.target.value }; + updateTab({ columns: newColumns }); + }} + placeholder="ํ‘œ์‹œ ๋ผ๋ฒจ" + className="h-8 text-xs" + /> +
+
+ + { + const newColumns = [...(tab.columns || [])]; + newColumns[colIndex] = { ...col, width: parseInt(e.target.value) || 100 }; + updateTab({ columns: newColumns }); + }} + placeholder="100" + className="h-8 text-xs" + /> +
+
+
+ )) + )} +
+ )} +
+ + {/* ===== 7. ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์ปฌ๋Ÿผ ์„ค์ • (showAdd์ผ ๋•Œ) ===== */} + {tab.showAdd && ( +
+
+ + +
+ +
+ {(tab.addModalColumns || []).length === 0 ? ( +
+

์ถ”๊ฐ€ ๋ชจ๋‹ฌ์— ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ์„ ์„ค์ •ํ•˜์„ธ์š”

+
+ ) : ( + (tab.addModalColumns || []).map((col, colIndex) => ( +
+ + { + const newColumns = [...(tab.addModalColumns || [])]; + newColumns[colIndex] = { ...col, label: e.target.value }; + updateTab({ addModalColumns: newColumns }); + }} + placeholder="๋ผ๋ฒจ" + className="h-8 w-24 text-xs" + /> +
+ { + const newColumns = [...(tab.addModalColumns || [])]; + newColumns[colIndex] = { ...col, required: !!checked }; + updateTab({ addModalColumns: newColumns }); + }} + /> + ํ•„์ˆ˜ +
+ +
+ )) + )} +
+
+ )} + + {/* ===== 8. ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง ===== */} +
+ + updateTab({ dataFilter })} + menuObjid={menuObjid} + /> +
+ + {/* ===== 9. ์ค‘๋ณต ๋ฐ์ดํ„ฐ ์ œ๊ฑฐ ===== */} +
+
+ + { + if (checked) { + updateTab({ + deduplication: { + enabled: true, + groupByColumn: "", + keepStrategy: "latest", + sortColumn: "start_date", + }, + }); + } else { + updateTab({ deduplication: undefined }); + } + }} + /> +
+ {tab.deduplication?.enabled && ( +
+
+ + +
+
+ + +
+
+ + +
+
+ )} +
+ + {/* ===== 10. ์ˆ˜์ • ๋ฒ„ํŠผ ์„ค์ • ===== */} + {tab.showEdit && ( +
+ +
+
+ + +
+ + {tab.editButton?.mode === "modal" && ( +
+ + { + updateTab({ + editButton: { ...tab.editButton, enabled: true, mode: "modal", modalScreenId: screenId }, + }); + }} + /> +
+ )} + +
+
+ + { + updateTab({ + editButton: { ...tab.editButton, enabled: true, buttonLabel: e.target.value || undefined }, + }); + }} + placeholder="์ˆ˜์ •" + className="h-7 text-xs" + /> +
+
+ + +
+
+ + {/* ๊ทธ๋ฃนํ•‘ ๊ธฐ์ค€ ์ปฌ๋Ÿผ */} +
+ +

์ˆ˜์ • ์‹œ ๊ฐ™์€ ๊ฐ’์„ ๊ฐ€์ง„ ๋ ˆ์ฝ”๋“œ๋ฅผ ํ•จ๊ป˜ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค

+
+ {tabColumns.map((col) => ( +
+ { + const current = tab.editButton?.groupByColumns || []; + const newColumns = checked + ? [...current, col.columnName] + : current.filter((c) => c !== col.columnName); + updateTab({ + editButton: { ...tab.editButton, enabled: true, groupByColumns: newColumns }, + }); + }} + /> + +
+ ))} +
+
+
+
+ )} + + {/* ===== 11. ์‚ญ์ œ ๋ฒ„ํŠผ ์„ค์ • ===== */} + {tab.showDelete && ( +
+ +
+
+
+ + { + updateTab({ + deleteButton: { ...tab.deleteButton, enabled: true, buttonLabel: e.target.value || undefined }, + }); + }} + placeholder="์‚ญ์ œ" + className="h-7 text-xs" + /> +
+
+ + +
+
+
+ + { + updateTab({ + deleteButton: { ...tab.deleteButton, enabled: true, confirmMessage: e.target.value || undefined }, + }); + }} + placeholder="์ •๋ง ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?" + className="h-7 text-xs" + /> +
+
+
+ )} + + {/* ===== ํƒญ ์‚ญ์ œ ๋ฒ„ํŠผ ===== */} +
+ +
+
+
+
+ ); +}; + /** * SplitPanelLayout ์„ค์ • ํŒจ๋„ */ @@ -2973,6 +3816,72 @@ export const SplitPanelLayoutConfigPanel: React.FC + {/* ======================================== */} + {/* ์ถ”๊ฐ€ ํƒญ ์„ค์ • (์šฐ์ธก ํŒจ๋„๊ณผ ๋™์ผํ•œ ๊ตฌ์กฐ) */} + {/* ======================================== */} +
+
+
+

์ถ”๊ฐ€ ํƒญ

+

+ ์šฐ์ธก ํŒจ๋„์— ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋ฅผ ํƒญ์œผ๋กœ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค +

+
+ +
+ + {/* ์ถ”๊ฐ€๋œ ํƒญ ๋ชฉ๋ก */} + {(config.rightPanel?.additionalTabs?.length || 0) > 0 ? ( + + {config.rightPanel?.additionalTabs?.map((tab, tabIndex) => ( + + ))} + + ) : ( +
+

+ ์ถ”๊ฐ€๋œ ํƒญ์ด ์—†์Šต๋‹ˆ๋‹ค. [ํƒญ ์ถ”๊ฐ€] ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์ƒˆ ํƒญ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +

+
+ )} +
+ {/* ๋ ˆ์ด์•„์›ƒ ์„ค์ • */}
diff --git a/frontend/lib/registry/components/split-panel-layout/types.ts b/frontend/lib/registry/components/split-panel-layout/types.ts index 0e9d6db9..17edc100 100644 --- a/frontend/lib/registry/components/split-panel-layout/types.ts +++ b/frontend/lib/registry/components/split-panel-layout/types.ts @@ -4,6 +4,105 @@ import { DataFilterConfig } from "@/types/screen-management"; +/** + * ์ถ”๊ฐ€ ํƒญ ์„ค์ • (์šฐ์ธก ํŒจ๋„๊ณผ ๋™์ผํ•œ ๊ตฌ์กฐ + tabId, label) + */ +export interface AdditionalTabConfig { + // ํƒญ ๊ณ ์œ  ์ •๋ณด + tabId: string; + label: string; + + // === ์šฐ์ธก ํŒจ๋„๊ณผ ๋™์ผํ•œ ์„ค์ • === + title: string; + panelHeaderHeight?: number; + tableName?: string; + dataSource?: string; + displayMode?: "list" | "table"; + showSearch?: boolean; + showAdd?: boolean; + showEdit?: boolean; + showDelete?: boolean; + summaryColumnCount?: number; + summaryShowLabel?: boolean; + + columns?: Array<{ + name: string; + label: string; + width?: number; + sortable?: boolean; + align?: "left" | "center" | "right"; + bold?: boolean; + format?: { + type?: "number" | "currency" | "date" | "text"; + thousandSeparator?: boolean; + decimalPlaces?: number; + prefix?: string; + suffix?: string; + dateFormat?: string; + }; + }>; + + addModalColumns?: Array<{ + name: string; + label: string; + required?: boolean; + }>; + + relation?: { + type?: "join" | "detail"; + leftColumn?: string; + rightColumn?: string; + foreignKey?: string; + keys?: Array<{ + leftColumn: string; + rightColumn: string; + }>; + }; + + addConfig?: { + targetTable?: string; + autoFillColumns?: Record; + leftPanelColumn?: string; + targetColumn?: string; + }; + + tableConfig?: { + showCheckbox?: boolean; + showRowNumber?: boolean; + rowHeight?: number; + headerHeight?: number; + striped?: boolean; + bordered?: boolean; + hoverable?: boolean; + stickyHeader?: boolean; + }; + + dataFilter?: DataFilterConfig; + + deduplication?: { + enabled: boolean; + groupByColumn: string; + keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; + sortColumn?: string; + }; + + editButton?: { + enabled: boolean; + mode: "auto" | "modal"; + modalScreenId?: number; + buttonLabel?: string; + buttonVariant?: "default" | "outline" | "ghost"; + groupByColumns?: string[]; + }; + + deleteButton?: { + enabled: boolean; + buttonLabel?: string; + buttonVariant?: "default" | "outline" | "ghost" | "destructive"; + confirmMessage?: string; + }; +} + export interface SplitPanelLayoutConfig { // ์ขŒ์ธก ํŒจ๋„ ์„ค์ • leftPanel: { @@ -165,6 +264,9 @@ export interface SplitPanelLayoutConfig { buttonVariant?: "default" | "outline" | "ghost" | "destructive"; // ๋ฒ„ํŠผ ์Šคํƒ€์ผ (๊ธฐ๋ณธ: "ghost") confirmMessage?: string; // ์‚ญ์ œ ํ™•์ธ ๋ฉ”์‹œ์ง€ }; + + // ๐Ÿ†• ์ถ”๊ฐ€ ํƒญ ์„ค์ • (๋ฉ€ํ‹ฐ ํ…Œ์ด๋ธ” ํƒญ) + additionalTabs?: AdditionalTabConfig[]; }; // ๋ ˆ์ด์•„์›ƒ ์„ค์ •